try to merge again

This commit is contained in:
technillogue 2021-04-24 22:23:52 -04:00
parent 6d18f311e6
commit 685fce477c
184 changed files with 14906 additions and 1705 deletions

View file

@ -15,17 +15,45 @@ public interface Signal extends DBusInterface {
long sendMessage(
String message, List<String> attachments, String recipient
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber;
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity;
long sendMessage(
String message, List<String> attachments, List<String> recipients
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity;
void sendEndSessionMessage(List<String> recipients) throws Error.Failure, Error.InvalidNumber, Error.UnregisteredUser, Error.UntrustedIdentity;
long sendRemoteDeleteMessage(
long targetSentTimestamp, String recipient
) throws Error.Failure, Error.InvalidNumber;
long sendRemoteDeleteMessage(
long targetSentTimestamp, List<String> recipients
) throws Error.Failure, Error.InvalidNumber;
long sendGroupRemoteDeleteMessage(
long targetSentTimestamp, byte[] groupId
) throws Error.Failure, Error.GroupNotFound;
long sendMessageReaction(
String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, String recipient
) throws Error.InvalidNumber, Error.Failure;
long sendMessageReaction(
String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List<String> recipients
) throws Error.InvalidNumber, Error.Failure;
long sendNoteToSelfMessage(
String message, List<String> attachments
) throws Error.AttachmentInvalid, Error.Failure;
void sendEndSessionMessage(List<String> recipients) throws Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity;
long sendGroupMessage(
String message, List<String> attachments, byte[] groupId
) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.UnregisteredUser, Error.UntrustedIdentity;
) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid;
long sendGroupMessageReaction(
String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId
) throws Error.GroupNotFound, Error.Failure, Error.InvalidNumber;
String getContactName(String number) throws Error.InvalidNumber;
@ -43,10 +71,30 @@ public interface Signal extends DBusInterface {
byte[] updateGroup(
byte[] groupId, String name, List<String> members, String avatar
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.UnregisteredUser, Error.UntrustedIdentity;
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound;
boolean isRegistered();
void updateProfile(
String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar
) throws Error.Failure;
String version();
List<String> listNumbers();
List<String> getContactNumber(final String name) throws Error.Failure;
void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure;
boolean isContactBlocked(final String number);
boolean isGroupBlocked(final byte[] groupId);
boolean isMember(final byte[] groupId);
void joinGroup(final String groupLink) throws Error.Failure;
class MessageReceived extends DBusSignal {
private final long timestamp;
@ -194,13 +242,6 @@ public interface Signal extends DBusInterface {
}
}
class UnregisteredUser extends DBusExecutionException {
public UnregisteredUser(final String message) {
super(message);
}
}
class UntrustedIdentity extends DBusExecutionException {
public UntrustedIdentity(final String message) {

View file

@ -0,0 +1,322 @@
package org.asamk.signal;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.commands.MultiLocalCommand;
import org.asamk.signal.commands.ProvisioningCommand;
import org.asamk.signal.commands.RegistrationCommand;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotRegisteredException;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.util.IOUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class App {
private final static Logger logger = LoggerFactory.getLogger(App.class);
private final Namespace ns;
static ArgumentParser buildArgumentParser() {
var parser = ArgumentParsers.newFor("signal-cli")
.build()
.defaultHelp(true)
.description("Commandline interface for Signal.")
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
parser.addArgument("--verbose")
.help("Raise log level and include lib signal logs.")
.action(Arguments.storeTrue());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
parser.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
var mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue());
parser.addArgument("-o", "--output")
.help("Choose to output in plain text or JSON")
.type(Arguments.enumStringType(OutputType.class))
.setDefault(OutputType.PLAIN_TEXT);
var subparsers = parser.addSubparsers().title("subcommands").dest("command");
final var commands = Commands.getCommands();
for (var entry : commands.entrySet()) {
var subparser = subparsers.addParser(entry.getKey());
entry.getValue().attachToSubparser(subparser);
}
return parser;
}
public App(final Namespace ns) {
this.ns = ns;
}
public void init() throws CommandException {
var commandKey = ns.getString("command");
var command = Commands.getCommand(commandKey);
if (command == null) {
throw new UserErrorException("Command not implemented!");
}
OutputType outputType = ns.get("output");
if (!command.getSupportedOutputTypes().contains(outputType)) {
throw new UserErrorException("Command doesn't support output type " + outputType.toString());
}
var username = ns.getString("username");
final boolean useDbus = ns.getBoolean("dbus");
final boolean useDbusSystem = ns.getBoolean("dbus_system");
if (useDbus || useDbusSystem) {
// If username is null, it will connect to the default object path
initDbusClient(command, username, useDbusSystem);
return;
}
final File dataPath;
var config = ns.getString("config");
if (config != null) {
dataPath = new File(config);
} else {
dataPath = getDefaultDataPath();
}
final var serviceEnvironment = ServiceEnvironment.LIVE;
if (!ServiceConfig.getCapabilities().isGv2()) {
logger.warn("WARNING: Support for new group V2 is disabled,"
+ " because the required native library dependency is missing: libzkgroup");
}
if (!ServiceConfig.isSignalClientAvailable()) {
throw new UserErrorException("Missing required native library dependency: libsignal-client");
}
if (command instanceof ProvisioningCommand) {
if (username != null) {
throw new UserErrorException("You cannot specify a username (phone number) when linking");
}
handleProvisioningCommand((ProvisioningCommand) command, dataPath, serviceEnvironment);
return;
}
if (username == null) {
var usernames = Manager.getAllLocalUsernames(dataPath);
if (command instanceof MultiLocalCommand) {
handleMultiLocalCommand((MultiLocalCommand) command, dataPath, serviceEnvironment, usernames);
return;
}
if (usernames.size() == 0) {
throw new UserErrorException("No local users found, you first need to register or link an account");
} else if (usernames.size() > 1) {
throw new UserErrorException(
"Multiple users found, you need to specify a username (phone number) with -u");
}
username = usernames.get(0);
} else if (!PhoneNumberFormatter.isValidNumber(username, null)) {
throw new UserErrorException("Invalid username (phone number), make sure you include the country code.");
}
if (command instanceof RegistrationCommand) {
handleRegistrationCommand((RegistrationCommand) command, username, dataPath, serviceEnvironment);
return;
}
if (!(command instanceof LocalCommand)) {
throw new UserErrorException("Command only works via dbus");
}
handleLocalCommand((LocalCommand) command, username, dataPath, serviceEnvironment);
}
private void handleProvisioningCommand(
final ProvisioningCommand command, final File dataPath, final ServiceEnvironment serviceEnvironment
) throws CommandException {
var pm = ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
command.handleCommand(ns, pm);
}
private void handleRegistrationCommand(
final RegistrationCommand command,
final String username,
final File dataPath,
final ServiceEnvironment serviceEnvironment
) throws CommandException {
final RegistrationManager manager;
try {
manager = RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
} catch (Throwable e) {
throw new UnexpectedErrorException("Error loading or creating state file: "
+ e.getMessage()
+ " ("
+ e.getClass().getSimpleName()
+ ")");
}
try (var m = manager) {
command.handleCommand(ns, m);
} catch (IOException e) {
logger.warn("Cleanup failed", e);
}
}
private void handleLocalCommand(
final LocalCommand command,
final String username,
final File dataPath,
final ServiceEnvironment serviceEnvironment
) throws CommandException {
try (var m = loadManager(username, dataPath, serviceEnvironment)) {
command.handleCommand(ns, m);
} catch (IOException e) {
logger.warn("Cleanup failed", e);
}
}
private void handleMultiLocalCommand(
final MultiLocalCommand command,
final File dataPath,
final ServiceEnvironment serviceEnvironment,
final List<String> usernames
) throws CommandException {
final var managers = new ArrayList<Manager>();
for (String u : usernames) {
try {
managers.add(loadManager(u, dataPath, serviceEnvironment));
} catch (CommandException e) {
logger.warn("Ignoring {}: {}", u, e.getMessage());
}
}
command.handleCommand(ns, managers);
for (var m : managers) {
try {
m.close();
} catch (IOException e) {
logger.warn("Cleanup failed", e);
}
}
}
private Manager loadManager(
final String username, final File dataPath, final ServiceEnvironment serviceEnvironment
) throws CommandException {
Manager manager;
try {
manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
} catch (NotRegisteredException e) {
throw new UserErrorException("User " + username + " is not registered.");
} catch (Throwable e) {
throw new UnexpectedErrorException("Error loading state file for user "
+ username
+ ": "
+ e.getMessage()
+ " ("
+ e.getClass().getSimpleName()
+ ")");
}
try {
manager.checkAccountState();
} catch (IOException e) {
throw new UnexpectedErrorException("Error while checking account " + username + ": " + e.getMessage());
}
return manager;
}
private void initDbusClient(
final Command command, final String username, final boolean systemBus
) throws CommandException {
try {
DBusConnection.DBusBusType busType;
if (systemBus) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (var dBusConn = DBusConnection.getConnection(busType)) {
var ts = dBusConn.getRemoteObject(DbusConfig.getBusname(),
DbusConfig.getObjectPath(username),
Signal.class);
handleCommand(command, ts, dBusConn);
}
} catch (DBusException | IOException e) {
logger.error("Dbus client failed", e);
throw new UnexpectedErrorException("Dbus client failed");
}
}
private void handleCommand(Command command, Signal ts, DBusConnection dBusConn) throws CommandException {
if (command instanceof ExtendedDbusCommand) {
((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
} else if (command instanceof DbusCommand) {
((DbusCommand) command).handleCommand(ns, ts);
} else {
throw new UserErrorException("Command is not yet implemented via dbus");
}
}
/**
* Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist:
* - $HOME/.config/signal
* - $HOME/.config/textsecure
*
* @return the data directory to be used by signal-cli.
*/
private static File getDefaultDataPath() {
var dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli");
if (dataPath.exists()) {
return dataPath;
}
var configPath = new File(System.getProperty("user.home"), ".config");
var legacySettingsPath = new File(configPath, "signal");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
legacySettingsPath = new File(configPath, "textsecure");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
return dataPath;
}
}

View file

@ -2,6 +2,22 @@ package org.asamk.signal;
public class DbusConfig {
public static final String SIGNAL_BUSNAME = "org.asamk.Signal";
public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal";
private static final String SIGNAL_BUSNAME = "org.asamk.Signal";
private static final String SIGNAL_OBJECT_BASE_PATH = "/org/asamk/Signal";
public static String getBusname() {
return SIGNAL_BUSNAME;
}
public static String getObjectPath() {
return getObjectPath(null);
}
public static String getObjectPath(String username) {
if (username == null) {
return SIGNAL_OBJECT_BASE_PATH;
}
return SIGNAL_OBJECT_BASE_PATH + "/" + username.replace('+', '_');
}
}

View file

@ -1,19 +1,14 @@
package org.asamk.signal;
import org.asamk.Signal;
import org.asamk.signal.manager.GroupUtils;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.groups.GroupUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.List;
@ -46,11 +41,11 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
e.printStackTrace();
}
} else if (content != null) {
final SignalServiceAddress sender = !envelope.isUnidentifiedSender() && envelope.hasSource()
final var sender = !envelope.isUnidentifiedSender() && envelope.hasSource()
? envelope.getSourceAddress()
: content.getSender();
if (content.getReceiptMessage().isPresent()) {
final SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get();
final var receiptMessage = content.getReceiptMessage().get();
if (receiptMessage.isDeliveryReceipt()) {
for (long timestamp : receiptMessage.getTimestamps()) {
try {
@ -63,9 +58,9 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
}
}
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
var message = content.getDataMessage().get();
byte[] groupId = getGroupId(message);
var groupId = getGroupId(message);
if (!message.isEndSession() && (
groupId == null
|| message.getGroupContext().get().getGroupV1Type() == null
@ -83,15 +78,15 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
}
}
} else if (content.getSyncMessage().isPresent()) {
SignalServiceSyncMessage sync_message = content.getSyncMessage().get();
var sync_message = content.getSyncMessage().get();
if (sync_message.getSent().isPresent()) {
SentTranscriptMessage transcript = sync_message.getSent().get();
var transcript = sync_message.getSent().get();
if (transcript.getDestination().isPresent() || transcript.getMessage()
.getGroupContext()
.isPresent()) {
SignalServiceDataMessage message = transcript.getMessage();
byte[] groupId = getGroupId(message);
var message = transcript.getMessage();
var groupId = getGroupId(message);
try {
conn.sendMessage(new Signal.SyncMessageReceived(objectPath,
@ -118,9 +113,9 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler {
}
static private List<String> getAttachments(SignalServiceDataMessage message, Manager m) {
List<String> attachments = new ArrayList<>();
var attachments = new ArrayList<String>();
if (message.getAttachments().isPresent()) {
for (SignalServiceAttachment attachment : message.getAttachments().get()) {
for (var attachment : message.getAttachments().get()) {
if (attachment.isPointer()) {
attachments.add(m.getAttachmentFile(attachment.asPointer().getRemoteId()).getAbsolutePath());
}

View file

@ -1,47 +1,37 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.json.JsonError;
import org.asamk.signal.json.JsonMessageEnvelope;
import org.asamk.signal.manager.Manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import java.io.IOException;
import java.util.HashMap;
public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final Manager m;
private final ObjectMapper jsonProcessor;
private final static Logger logger = LoggerFactory.getLogger(JsonReceiveMessageHandler.class);
protected final Manager m;
private final JsonWriter jsonWriter;
public JsonReceiveMessageHandler(Manager m) {
this.m = m;
this.jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
jsonWriter = new JsonWriter(System.out);
}
@Override
public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) {
ObjectNode result = jsonProcessor.createObjectNode();
final var object = new HashMap<String, Object>();
if (exception != null) {
result.putPOJO("error", new JsonError(exception));
object.put("error", new JsonError(exception));
}
if (envelope != null) {
result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content, m));
}
try {
jsonProcessor.writeValue(System.out, result);
System.out.println();
} catch (IOException e) {
e.printStackTrace();
object.put("envelope", new JsonMessageEnvelope(envelope, content, m));
}
jsonWriter.write(object);
}
}

View file

@ -0,0 +1,43 @@
package org.asamk.signal;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
public class JsonWriter {
private final Writer writer;
private final ObjectMapper objectMapper;
public JsonWriter(final OutputStream writer) {
this.writer = new BufferedWriter(new OutputStreamWriter(writer, StandardCharsets.UTF_8));
objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
objectMapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
}
public void write(final Object object) {
try {
try {
objectMapper.writeValue(writer, object);
} catch (JsonProcessingException e) {
// Some issue with json serialization, probably caused by a bug
throw new AssertionError(e);
}
writer.write(System.lineSeparator());
writer.flush();
} catch (IOException e) {
throw new AssertionError(e);
}
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright (C) 2015-2020 AsamK and contributors
Copyright (C) 2015-2021 AsamK and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -18,282 +18,89 @@ package org.asamk.signal;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import net.sourceforge.argparse4j.inf.Subparsers;
import org.asamk.Signal;
import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.commands.ProvisioningCommand;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.ServiceConfig;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.LibSignalLogger;
import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import java.io.File;
import java.io.IOException;
import java.security.Security;
import java.util.Map;
public class Main {
final static Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
installSecurityProviderWorkaround();
Namespace ns = parseArgs(args);
if (ns == null) {
System.exit(1);
}
// Configuring the logger needs to happen before any logger is initialized
configureLogging(isVerbose(args));
int res = init(ns);
System.exit(res);
var parser = App.buildArgumentParser();
var ns = parser.parseArgsOrFail(args);
int status = 0;
try {
new App(ns).init();
} catch (CommandException e) {
System.err.println(e.getMessage());
status = getStatusForError(e);
}
System.exit(status);
}
public static void installSecurityProviderWorkaround() {
private static void installSecurityProviderWorkaround() {
// Register our own security provider
Security.insertProviderAt(new SecurityProvider(), 1);
Security.addProvider(new BouncyCastleProvider());
}
public static int init(Namespace ns) {
if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
return initDbusClient(ns, ns.getBoolean("dbus_system"));
}
final String username = ns.getString("username");
final File dataPath;
String config = ns.getString("config");
if (config != null) {
dataPath = new File(config);
} else {
dataPath = getDefaultDataPath();
}
final SignalServiceConfiguration serviceConfiguration = ServiceConfig.createDefaultServiceConfiguration(
BaseConfig.USER_AGENT);
if (!ServiceConfig.getCapabilities().isGv2()) {
logger.warn("WARNING: Support for new group V2 is disabled,"
+ " because the required native library dependency is missing: libzkgroup");
}
if (username == null) {
ProvisioningManager pm = new ProvisioningManager(dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
return handleCommands(ns, pm);
}
Manager manager;
try {
manager = Manager.init(username, dataPath, serviceConfiguration, BaseConfig.USER_AGENT);
} catch (Throwable e) {
logger.error("Error loading state file: {}", e.getMessage());
return 2;
}
try (Manager m = manager) {
try {
m.checkAccountState();
} catch (AuthorizationFailedException e) {
if (!"register".equals(ns.getString("command"))) {
// Register command should still be possible, if current authorization fails
System.err.println("Authorization failed, was the number registered elsewhere?");
return 2;
}
} catch (IOException e) {
logger.error("Error while checking account: {}", e.getMessage());
return 2;
}
return handleCommands(ns, m);
} catch (IOException e) {
logger.error("Cleanup failed", e);
return 3;
}
}
private static int initDbusClient(final Namespace ns, final boolean systemBus) {
try {
DBusConnection.DBusBusType busType;
if (systemBus) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (DBusConnection dBusConn = DBusConnection.getConnection(busType)) {
Signal ts = dBusConn.getRemoteObject(DbusConfig.SIGNAL_BUSNAME,
DbusConfig.SIGNAL_OBJECTPATH,
Signal.class);
return handleCommands(ns, ts, dBusConn);
}
} catch (DBusException | IOException e) {
logger.error("Dbus client failed", e);
return 3;
}
}
private static int handleCommands(Namespace ns, Signal ts, DBusConnection dBusConn) {
String commandKey = ns.getString("command");
final Map<String, Command> commands = Commands.getCommands();
if (commands.containsKey(commandKey)) {
Command command = commands.get(commandKey);
if (command instanceof ExtendedDbusCommand) {
return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, ts);
} else {
System.err.println(commandKey + " is not yet implemented via dbus");
return 1;
}
}
return 0;
}
private static int handleCommands(Namespace ns, ProvisioningManager pm) {
String commandKey = ns.getString("command");
final Map<String, Command> commands = Commands.getCommands();
if (commands.containsKey(commandKey)) {
Command command = commands.get(commandKey);
if (command instanceof ProvisioningCommand) {
return ((ProvisioningCommand) command).handleCommand(ns, pm);
} else {
System.err.println(commandKey + " only works with a username");
return 1;
}
}
return 0;
}
private static int handleCommands(Namespace ns, Manager m) {
String commandKey = ns.getString("command");
final Map<String, Command> commands = Commands.getCommands();
if (commands.containsKey(commandKey)) {
Command command = commands.get(commandKey);
if (command instanceof LocalCommand) {
return ((LocalCommand) command).handleCommand(ns, m);
} else if (command instanceof DbusCommand) {
return ((DbusCommand) command).handleCommand(ns, new DbusSignalImpl(m));
} else if (command instanceof ExtendedDbusCommand) {
System.err.println(commandKey + " only works via dbus");
}
return 1;
}
return 0;
}
/**
* Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist:
* - $HOME/.config/signal
* - $HOME/.config/textsecure
*
* @return the data directory to be used by signal-cli.
*/
private static File getDefaultDataPath() {
File dataPath = new File(IOUtils.getDataHomeDir(), "signal-cli");
if (dataPath.exists()) {
return dataPath;
}
File configPath = new File(System.getProperty("user.home"), ".config");
File legacySettingsPath = new File(configPath, "signal");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
legacySettingsPath = new File(configPath, "textsecure");
if (legacySettingsPath.exists()) {
return legacySettingsPath;
}
return dataPath;
}
private static Namespace parseArgs(String[] args) {
ArgumentParser parser = buildArgumentParser();
private static boolean isVerbose(String[] args) {
var parser = ArgumentParsers.newFor("signal-cli").build().defaultHelp(false);
parser.addArgument("--verbose").action(Arguments.storeTrue());
Namespace ns;
try {
ns = parser.parseArgs(args);
ns = parser.parseKnownArgs(args, null);
} catch (ArgumentParserException e) {
parser.handleError(e);
return null;
return false;
}
if ("link".equals(ns.getString("command"))) {
if (ns.getString("username") != null) {
parser.printUsage();
System.err.println("You cannot specify a username (phone number) when linking");
System.exit(2);
}
} else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
if (ns.getString("username") == null) {
parser.printUsage();
System.err.println("You need to specify a username (phone number)");
System.exit(2);
}
if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"), null)) {
System.err.println("Invalid username (phone number), make sure you include the country code.");
System.exit(2);
}
}
if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) {
System.err.println("You cannot specify recipients by phone number and groups at the same time");
System.exit(2);
}
return ns;
return ns.getBoolean("verbose");
}
private static ArgumentParser buildArgumentParser() {
ArgumentParser parser = ArgumentParsers.newFor("signal-cli")
.build()
.defaultHelp(true)
.description("Commandline interface for Signal.")
.version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
parser.addArgument("--config")
.help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
mut.addArgument("-u", "--username").help("Specify your phone number, that will be used for verification.");
mut.addArgument("--dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
mut.addArgument("--dbus-system").help("Make request via system dbus.").action(Arguments.storeTrue());
Subparsers subparsers = parser.addSubparsers()
.title("subcommands")
.dest("command")
.description("valid subcommands")
.help("additional help");
final Map<String, Command> commands = Commands.getCommands();
for (Map.Entry<String, Command> entry : commands.entrySet()) {
Subparser subparser = subparsers.addParser(entry.getKey());
entry.getValue().attachToSubparser(subparser);
private static void configureLogging(final boolean verbose) {
if (verbose) {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "debug");
System.setProperty("org.slf4j.simpleLogger.showThreadName", "true");
System.setProperty("org.slf4j.simpleLogger.showShortLogName", "false");
System.setProperty("org.slf4j.simpleLogger.showDateTime", "true");
System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "yyyy-MM-dd'T'HH:mm:ss.SSSXX");
LibSignalLogger.initLogger();
} else {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
System.setProperty("org.slf4j.simpleLogger.showThreadName", "false");
System.setProperty("org.slf4j.simpleLogger.showShortLogName", "true");
System.setProperty("org.slf4j.simpleLogger.showDateTime", "false");
}
}
private static int getStatusForError(final CommandException e) {
if (e instanceof UserErrorException) {
return 1;
} else if (e instanceof UnexpectedErrorException) {
return 2;
} else if (e instanceof IOErrorException) {
return 3;
} else if (e instanceof UntrustedKeyErrorException) {
return 4;
} else {
return 2;
}
return parser;
}
}

View file

@ -0,0 +1,16 @@
package org.asamk.signal;
public enum OutputType {
PLAIN_TEXT {
@Override
public String toString() {
return "plain-text";
}
},
JSON {
@Override
public String toString() {
return "json";
}
},
}

View file

@ -0,0 +1,21 @@
package org.asamk.signal;
public interface PlainTextWriter {
void println(String format, Object... args);
PlainTextWriter indentedWriter();
default void println() {
println("");
}
default void indent(final WriterConsumer subWriter) {
subWriter.consume(indentedWriter());
}
interface WriterConsumer {
void consume(PlainTextWriter writer);
}
}

View file

@ -0,0 +1,75 @@
package org.asamk.signal;
import org.slf4j.helpers.MessageFormatter;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
public final class PlainTextWriterImpl implements PlainTextWriter {
private final Writer writer;
private PlainTextWriter indentedWriter;
public PlainTextWriterImpl(final OutputStream outputStream) {
this.writer = new BufferedWriter(new OutputStreamWriter(outputStream));
}
@Override
public void println(String format, Object... args) {
final var message = MessageFormatter.arrayFormat(format, args).getMessage();
try {
writer.write(message);
writer.write(System.lineSeparator());
writer.flush();
} catch (IOException e) {
throw new AssertionError(e);
}
}
@Override
public PlainTextWriter indentedWriter() {
if (indentedWriter == null) {
indentedWriter = new IndentedPlainTextWriter(this, writer);
}
return indentedWriter;
}
private static final class IndentedPlainTextWriter implements PlainTextWriter {
private final static int INDENTATION = 2;
private final String spaces = " ".repeat(INDENTATION);
private final PlainTextWriter plainTextWriter;
private final Writer writer;
private PlainTextWriter indentedWriter;
private IndentedPlainTextWriter(final PlainTextWriter plainTextWriter, final Writer writer) {
this.plainTextWriter = plainTextWriter;
this.writer = writer;
}
@Override
public void println(final String format, final Object... args) {
try {
writer.write(spaces);
} catch (IOException e) {
throw new AssertionError(e);
}
plainTextWriter.println(format, args);
}
@Override
public PlainTextWriter indentedWriter() {
if (indentedWriter == null) {
indentedWriter = new IndentedPlainTextWriter(this, writer);
}
return indentedWriter;
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,13 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidKeyException;
import java.io.IOException;
@ -14,6 +20,8 @@ import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class AddDeviceCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(AddDeviceCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("--uri")
@ -22,23 +30,20 @@ public class AddDeviceCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
try {
m.addDeviceLink(new URI(ns.getString("uri")));
return 0;
} catch (IOException e) {
e.printStackTrace();
return 3;
} catch (InvalidKeyException | URISyntaxException e) {
e.printStackTrace();
return 2;
logger.error("Add device link failed", e);
throw new IOErrorException("Add device link failed");
} catch (URISyntaxException e) {
throw new UserErrorException("Device link uri has invalid format: {}" + e.getMessage());
} catch (InvalidKeyException e) {
logger.error("Add device link failed", e);
throw new UnexpectedErrorException("Add device link failed.");
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
throw e;
}
}
}

View file

@ -3,15 +3,18 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
public class BlockCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(BlockCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("contact").help("Contact number").nargs("*");
@ -20,31 +23,24 @@ public class BlockCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
for (String contact_number : ns.<String>getList("contact")) {
public void handleCommand(final Namespace ns, final Manager m) {
for (var contact_number : ns.<String>getList("contact")) {
try {
m.setContactBlocked(contact_number, true);
} catch (InvalidNumberException e) {
System.err.println(e.getMessage());
logger.warn("Invalid number {}: {}", contact_number, e.getMessage());
}
}
if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) {
for (var groupIdString : ns.<String>getList("group")) {
try {
GroupId groupId = Util.decodeGroupId(groupIdString);
var groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, true);
} catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage());
logger.warn("Invalid group id {}: {}", groupIdString, e.getMessage());
}
}
}
return 0;
}
}

View file

@ -2,7 +2,15 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.OutputType;
import java.util.Set;
public interface Command {
void attachToSubparser(Subparser subparser);
default Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.PLAIN_TEXT);
}
}

View file

@ -11,7 +11,11 @@ public class Commands {
addCommand("addDevice", new AddDeviceCommand());
addCommand("block", new BlockCommand());
addCommand("daemon", new DaemonCommand());
<<<<<<< HEAD
addCommand("stdio", new StdioCommand());
=======
addCommand("getUserStatus", new GetUserStatusCommand());
>>>>>>> upstream/master
addCommand("link", new LinkCommand());
addCommand("listContacts", new ListContactsCommand());
addCommand("listDevices", new ListDevicesCommand());
@ -22,26 +26,34 @@ public class Commands {
addCommand("receive", new ReceiveCommand());
addCommand("register", new RegisterCommand());
addCommand("removeDevice", new RemoveDeviceCommand());
addCommand("remoteDelete", new RemoteDeleteCommand());
addCommand("removePin", new RemovePinCommand());
addCommand("send", new SendCommand());
addCommand("sendReaction", new SendReactionCommand());
addCommand("sendContacts", new SendContactsCommand());
addCommand("updateContact", new UpdateContactCommand());
addCommand("sendReaction", new SendReactionCommand());
addCommand("setPin", new SetPinCommand());
addCommand("trust", new TrustCommand());
addCommand("unblock", new UnblockCommand());
addCommand("unregister", new UnregisterCommand());
addCommand("updateAccount", new UpdateAccountCommand());
addCommand("updateContact", new UpdateContactCommand());
addCommand("updateGroup", new UpdateGroupCommand());
addCommand("updateProfile", new UpdateProfileCommand());
addCommand("verify", new VerifyCommand());
addCommand("uploadStickerPack", new UploadStickerPackCommand());
addCommand("verify", new VerifyCommand());
}
public static Map<String, Command> getCommands() {
return commands;
}
public static Command getCommand(String commandKey) {
if (!commands.containsKey(commandKey)) {
return null;
}
return commands.get(commandKey);
}
private static void addCommand(String name, Command command) {
commands.put(name, command);
}

View file

@ -4,21 +4,28 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.DbusConfig;
import org.asamk.signal.DbusReceiveMessageHandler;
import org.asamk.signal.JsonDbusReceiveMessageHandler;
import org.asamk.signal.OutputType;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.asamk.signal.DbusConfig.SIGNAL_BUSNAME;
import static org.asamk.signal.DbusConfig.SIGNAL_OBJECTPATH;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class DaemonCommand implements MultiLocalCommand {
public class DaemonCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(DaemonCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
@ -29,56 +36,114 @@ public class DaemonCommand implements LocalCommand {
.help("Dont download attachments of received messages.")
.action(Arguments.storeTrue());
subparser.addArgument("--json")
.help("Output received messages in json format, one json object per line.")
.help("WARNING: This parameter is now deprecated! Please use the global \"--output=json\" option instead.\n\nOutput received messages in json format, one json object per line.")
.action(Arguments.storeTrue());
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
public Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON);
}
@Override
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
var inJson = ns.get("output") == OutputType.JSON || ns.getBoolean("json");
// TODO delete later when "json" variable is removed
if (ns.getBoolean("json")) {
logger.warn("\"--json\" option has been deprecated, please use the global \"--output=json\" instead.");
}
DBusConnection conn = null;
try {
boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
DBusConnection.DBusBusType busType;
if (ns.getBoolean("system")) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (var conn = DBusConnection.getConnection(busType)) {
var objectPath = DbusConfig.getObjectPath();
var t = run(conn, objectPath, m, ignoreAttachments, inJson);
conn.requestBusName(DbusConfig.getBusname());
try {
DBusConnection.DBusBusType busType;
if (ns.getBoolean("system")) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
conn = DBusConnection.getConnection(busType);
conn.exportObject(SIGNAL_OBJECTPATH, new DbusSignalImpl(m));
conn.requestBusName(SIGNAL_BUSNAME);
} catch (UnsatisfiedLinkError e) {
System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
return 1;
} catch (DBusException e) {
e.printStackTrace();
return 2;
}
boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
try {
m.receiveMessages(1,
TimeUnit.HOURS,
false,
ignoreAttachments,
ns.getBoolean("json")
? new JsonDbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH)
: new DbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH));
return 0;
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());
return 3;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
}
} finally {
if (conn != null) {
conn.disconnect();
t.join();
} catch (InterruptedException ignored) {
}
} catch (DBusException | IOException e) {
logger.error("Dbus command failed", e);
throw new UnexpectedErrorException("Dbus command failed");
}
}
@Override
public void handleCommand(final Namespace ns, final List<Manager> managers) throws CommandException {
var inJson = ns.get("output") == OutputType.JSON || ns.getBoolean("json");
// TODO delete later when "json" variable is removed
if (ns.getBoolean("json")) {
logger.warn("\"--json\" option has been deprecated, please use the global \"--output=json\" instead.");
}
boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
DBusConnection.DBusBusType busType;
if (ns.getBoolean("system")) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
}
try (var conn = DBusConnection.getConnection(busType)) {
var receiveThreads = new ArrayList<Thread>();
for (var m : managers) {
var objectPath = DbusConfig.getObjectPath(m.getUsername());
var thread = run(conn, objectPath, m, ignoreAttachments, inJson);
receiveThreads.add(thread);
}
conn.requestBusName(DbusConfig.getBusname());
for (var t : receiveThreads) {
try {
t.join();
} catch (InterruptedException ignored) {
}
}
} catch (DBusException | IOException e) {
logger.error("Dbus command failed", e);
throw new UnexpectedErrorException("Dbus command failed");
}
}
private Thread run(
DBusConnection conn, String objectPath, Manager m, boolean ignoreAttachments, boolean inJson
) throws DBusException {
conn.exportObject(objectPath, new DbusSignalImpl(m));
final var thread = new Thread(() -> {
while (true) {
try {
m.receiveMessages(1,
TimeUnit.HOURS,
false,
ignoreAttachments,
inJson
? new JsonDbusReceiveMessageHandler(m, conn, objectPath)
: new DbusReceiveMessageHandler(m, conn, objectPath));
} catch (IOException e) {
logger.warn("Receiving messages failed, retrying", e);
}
}
});
logger.info("Exported dbus object: " + objectPath);
thread.start();
return thread;
}
}

View file

@ -3,8 +3,15 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
public interface DbusCommand extends Command {
public interface DbusCommand extends LocalCommand {
int handleCommand(Namespace ns, Signal signal);
void handleCommand(Namespace ns, Signal signal) throws CommandException;
default void handleCommand(final Namespace ns, final Manager m) throws CommandException {
handleCommand(ns, new DbusSignalImpl(m));
}
}

View file

@ -3,9 +3,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
import org.asamk.signal.commands.exceptions.CommandException;
import org.freedesktop.dbus.connections.impl.DBusConnection;
public interface ExtendedDbusCommand extends Command {
int handleCommand(Namespace ns, Signal signal, DBusConnection dbusconnection);
void handleCommand(Namespace ns, Signal signal, DBusConnection dbusconnection) throws CommandException;
}

View file

@ -0,0 +1,89 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputType;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.Manager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class GetUserStatusCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(GetUserStatusCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("number").help("Phone number").nargs("+");
subparser.help("Check if the specified phone number/s have been registered");
subparser.addArgument("--json")
.help("WARNING: This parameter is now deprecated! Please use the global \"--output=json\" option instead.\n\nOutput received messages in json format, one json object per line.")
.action(Arguments.storeTrue());
}
@Override
public Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON);
}
@Override
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
// Setup the json object mapper
var inJson = ns.get("output") == OutputType.JSON || ns.getBoolean("json");
// TODO delete later when "json" variable is removed
if (ns.getBoolean("json")) {
logger.warn("\"--json\" option has been deprecated, please use the global \"--output=json\" instead.");
}
// Get a map of registration statuses
Map<String, Boolean> registered;
try {
registered = m.areUsersRegistered(new HashSet<>(ns.getList("number")));
} catch (IOException e) {
logger.debug("Failed to check registered users", e);
throw new IOErrorException("Unable to check if users are registered");
}
// Output
if (inJson) {
final var jsonWriter = new JsonWriter(System.out);
var jsonUserStatuses = registered.entrySet()
.stream()
.map(entry -> new JsonUserStatus(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
jsonWriter.write(jsonUserStatuses);
} else {
final var writer = new PlainTextWriterImpl(System.out);
for (var entry : registered.entrySet()) {
writer.println("{}: {}", entry.getKey(), entry.getValue());
}
}
}
private static final class JsonUserStatus {
public String name;
public boolean isRegistered;
public JsonUserStatus(String name, boolean isRegistered) {
this.name = name;
this.isRegistered = isRegistered;
}
}
}

View file

@ -3,21 +3,20 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
import java.io.IOException;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
public class JoinGroupCommand implements LocalCommand {
@ -28,57 +27,43 @@ public class JoinGroupCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
final GroupInviteLinkUrl linkUrl;
String uri = ns.getString("uri");
var uri = ns.getString("uri");
try {
linkUrl = GroupInviteLinkUrl.fromUri(uri);
} catch (GroupInviteLinkUrl.InvalidGroupLinkException e) {
System.err.println("Group link is invalid: " + e.getMessage());
return 2;
throw new UserErrorException("Group link is invalid: " + e.getMessage());
} catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
System.err.println("Group link was created with an incompatible version: " + e.getMessage());
return 2;
throw new UserErrorException("Group link was created with an incompatible version: " + e.getMessage());
}
if (linkUrl == null) {
System.err.println("Link is not a signal group invitation link");
return 2;
throw new UserErrorException("Link is not a signal group invitation link");
}
try {
final Pair<GroupId, List<SendMessageResult>> results = m.joinGroup(linkUrl);
GroupId newGroupId = results.first();
final var writer = new PlainTextWriterImpl(System.out);
final var results = m.joinGroup(linkUrl);
var newGroupId = results.first();
if (!m.getGroup(newGroupId).isMember(m.getSelfAddress())) {
System.out.println("Requested to join group \"" + newGroupId.toBase64() + "\"");
writer.println("Requested to join group \"{}\"", newGroupId.toBase64());
} else {
System.out.println("Joined group \"" + newGroupId.toBase64() + "\"");
writer.println("Joined group \"{}\"", newGroupId.toBase64());
}
return handleTimestampAndSendMessageResults(0, results.second());
handleTimestampAndSendMessageResults(writer, 0, results.second());
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
throw e;
} catch (GroupPatchNotAcceptedException e) {
System.err.println("Failed to join group, maybe already a member");
return 1;
throw new UserErrorException("Failed to join group, maybe already a member");
} catch (IOException e) {
e.printStackTrace();
handleIOException(e);
return 1;
} catch (Signal.Error.AttachmentInvalid e) {
System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
return 1;
throw new IOErrorException("Failed to send message: " + e.getMessage());
} catch (DBusExecutionException e) {
System.err.println("Failed to send message: " + e.getMessage());
return 1;
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage());
} catch (GroupLinkNotActiveException e) {
System.err.println("Group link is not valid: " + e.getMessage());
return 2;
throw new UserErrorException("Group link is not valid: " + e.getMessage());
}
}
}

View file

@ -3,8 +3,15 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.UserAlreadyExists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.libsignal.InvalidKeyException;
import java.io.IOException;
@ -14,41 +21,42 @@ import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class LinkCommand implements ProvisioningCommand {
private final static Logger logger = LoggerFactory.getLogger(LinkCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-n", "--name").help("Specify a name to describe this new device.");
}
@Override
public int handleCommand(final Namespace ns, final ProvisioningManager m) {
String deviceName = ns.getString("name");
public void handleCommand(final Namespace ns, final ProvisioningManager m) throws CommandException {
final var writer = new PlainTextWriterImpl(System.out);
var deviceName = ns.getString("name");
if (deviceName == null) {
deviceName = "cli";
}
try {
System.out.println(m.getDeviceLinkUri());
String username = m.finishDeviceLink(deviceName);
System.out.println("Associated with: " + username);
writer.println("{}", m.getDeviceLinkUri());
try (var manager = m.finishDeviceLink(deviceName)) {
writer.println("Associated with: {}", manager.getUsername());
}
} catch (TimeoutException e) {
System.err.println("Link request timed out, please try again.");
return 3;
throw new UserErrorException("Link request timed out, please try again.");
} catch (IOException e) {
System.err.println("Link request error: " + e.getMessage());
return 3;
throw new IOErrorException("Link request error: " + e.getMessage());
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
throw e;
} catch (InvalidKeyException e) {
e.printStackTrace();
return 2;
logger.debug("Finish device link failed", e);
throw new UnexpectedErrorException("Invalid key: " + e.getMessage());
} catch (UserAlreadyExists e) {
System.err.println("The user "
throw new UserErrorException("The user "
+ e.getUsername()
+ " already exists\nDelete \""
+ e.getFileName()
+ "\" before trying again.");
return 1;
}
return 0;
}
}

View file

@ -3,10 +3,8 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.contacts.ContactInfo;
import java.util.List;
public class ListContactsCommand implements LocalCommand {
@ -15,15 +13,12 @@ public class ListContactsCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
public void handleCommand(final Namespace ns, final Manager m) {
final var writer = new PlainTextWriterImpl(System.out);
var contacts = m.getContacts();
for (var c : contacts) {
writer.println("Number: {} Name: {} Blocked: {}", c.number, c.name, c.blocked);
}
List<ContactInfo> contacts = m.getContacts();
for (ContactInfo c : contacts) {
System.out.println(String.format("Number: %s Name: %s Blocked: %b", c.number, c.name, c.blocked));
}
return 0;
}
}

View file

@ -3,8 +3,13 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import java.io.IOException;
@ -12,31 +17,31 @@ import java.util.List;
public class ListDevicesCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(ListDevicesCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
final var writer = new PlainTextWriterImpl(System.out);
List<DeviceInfo> devices;
try {
List<DeviceInfo> devices = m.getLinkedDevices();
for (DeviceInfo d : devices) {
System.out.println("Device "
+ d.getId()
+ (d.getId() == m.getDeviceId() ? " (this device)" : "")
+ ":");
System.out.println(" Name: " + d.getName());
System.out.println(" Created: " + DateUtils.formatTimestamp(d.getCreated()));
System.out.println(" Last seen: " + DateUtils.formatTimestamp(d.getLastSeen()));
}
return 0;
devices = m.getLinkedDevices();
} catch (IOException e) {
e.printStackTrace();
return 3;
logger.debug("Failed to get linked devices", e);
throw new IOErrorException("Failed to get linked devices: " + e.getMessage());
}
for (var d : devices) {
writer.println("- Device {}{}:", d.getId(), (d.getId() == m.getDeviceId() ? " (this device)" : ""));
writer.indent(w -> {
w.println("Name: {}", d.getName());
w.println("Created: {}", DateUtils.formatTimestamp(d.getCreated()));
w.println("Last seen: {}", DateUtils.formatTimestamp(d.getLastSeen()));
});
}
}
}

View file

@ -4,77 +4,131 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputType;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.stream.Collectors;
public class ListGroupsCommand implements LocalCommand {
private static void printGroup(Manager m, GroupInfo group, boolean detailed) {
private final static Logger logger = LoggerFactory.getLogger(ListGroupsCommand.class);
private static Set<String> resolveMembers(Manager m, Set<SignalServiceAddress> addresses) {
return addresses.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
}
private static void printGroupPlainText(
PlainTextWriter writer, Manager m, GroupInfo group, boolean detailed
) {
if (detailed) {
Set<String> members = group.getMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
final var groupInviteLink = group.getGroupInviteLink();
Set<String> pendingMembers = group.getPendingMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
Set<String> requestingMembers = group.getRequestingMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink();
System.out.println(String.format(
"Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s",
writer.println(
"Id: {} Name: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Link: {}",
group.getGroupId().toBase64(),
group.getTitle(),
group.isMember(m.getSelfAddress()),
group.isBlocked(),
members,
pendingMembers,
requestingMembers,
groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()),
resolveMembers(m, group.getRequestingMembers()),
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
writer.println("Id: {} Name: {} Active: {} Blocked: {}",
group.getGroupId().toBase64(),
group.getTitle(),
group.isMember(m.getSelfAddress()),
group.isBlocked()));
group.isBlocked());
}
}
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue()).help("List members of each group");
subparser.help("List group name and ids");
subparser.addArgument("-d", "--detailed")
.action(Arguments.storeTrue())
.help("List the members and group invite links of each group. If output=json, then this is always set");
subparser.help("List group information including names, ids, active status, blocked status and members");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON);
}
List<GroupInfo> groups = m.getGroups();
boolean detailed = ns.getBoolean("detailed");
@Override
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
if (ns.get("output") == OutputType.JSON) {
final var jsonWriter = new JsonWriter(System.out);
for (GroupInfo group : groups) {
printGroup(m, group, detailed);
var jsonGroups = new ArrayList<JsonGroup>();
for (var group : m.getGroups()) {
final var groupInviteLink = group.getGroupInviteLink();
jsonGroups.add(new JsonGroup(group.getGroupId().toBase64(),
group.getTitle(),
group.isMember(m.getSelfAddress()),
group.isBlocked(),
resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()),
resolveMembers(m, group.getRequestingMembers()),
groupInviteLink == null ? null : groupInviteLink.getUrl()));
}
jsonWriter.write(jsonGroups);
} else {
final var writer = new PlainTextWriterImpl(System.out);
boolean detailed = ns.getBoolean("detailed");
for (var group : m.getGroups()) {
printGroupPlainText(writer, m, group, detailed);
}
}
}
private static final class JsonGroup {
public String id;
public String name;
public boolean isMember;
public boolean isBlocked;
public Set<String> members;
public Set<String> pendingMembers;
public Set<String> requestingMembers;
public String groupInviteLink;
public JsonGroup(
String id,
String name,
boolean isMember,
boolean isBlocked,
Set<String> members,
Set<String> pendingMembers,
Set<String> requestingMembers,
String groupInviteLink
) {
this.id = id;
this.name = name;
this.isMember = isMember;
this.isBlocked = isBlocked;
this.members = members;
this.pendingMembers = pendingMembers;
this.requestingMembers = requestingMembers;
this.groupInviteLink = groupInviteLink;
}
return 0;
}
}

View file

@ -3,24 +3,32 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
import org.asamk.signal.manager.storage.protocol.IdentityInfo;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.util.List;
public class ListIdentitiesCommand implements LocalCommand {
private static void printIdentityFingerprint(Manager m, JsonIdentityKeyStore.Identity theirId) {
String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s",
private final static Logger logger = LoggerFactory.getLogger(ListIdentitiesCommand.class);
private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) {
var digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirId.getAddress(), theirId.getIdentityKey()));
writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}",
theirId.getAddress().getNumber().orNull(),
theirId.getTrustLevel(),
theirId.getDateAdded(),
Hex.toString(theirId.getFingerprint()),
digits));
digits);
}
@Override
@ -29,26 +37,27 @@ public class ListIdentitiesCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
if (ns.get("number") == null) {
for (JsonIdentityKeyStore.Identity identity : m.getIdentities()) {
printIdentityFingerprint(m, identity);
}
} else {
String number = ns.getString("number");
try {
List<JsonIdentityKeyStore.Identity> identities = m.getIdentities(number);
for (JsonIdentityKeyStore.Identity id : identities) {
printIdentityFingerprint(m, id);
}
} catch (InvalidNumberException e) {
System.err.println("Invalid number: " + e.getMessage());
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
final var writer = new PlainTextWriterImpl(System.out);
var number = ns.getString("number");
if (number == null) {
for (var identity : m.getIdentities()) {
printIdentityFingerprint(writer, m, identity);
}
return;
}
List<IdentityInfo> identities;
try {
identities = m.getIdentities(number);
} catch (InvalidNumberException e) {
throw new UserErrorException("Invalid number: " + e.getMessage());
}
for (var id : identities) {
printIdentityFingerprint(writer, m, id);
}
return 0;
}
}

View file

@ -2,9 +2,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
public interface LocalCommand extends Command {
int handleCommand(Namespace ns, Manager m);
void handleCommand(Namespace ns, Manager m) throws CommandException;
}

View file

@ -0,0 +1,18 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import java.util.List;
public interface MultiLocalCommand extends LocalCommand {
void handleCommand(Namespace ns, List<Manager> m) throws CommandException;
@Override
default void handleCommand(final Namespace ns, final Manager m) throws CommandException {
handleCommand(ns, List.of(m));
}
}

View file

@ -2,9 +2,10 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.ProvisioningManager;
public interface ProvisioningCommand extends Command {
int handleCommand(Namespace ns, ProvisioningManager m);
void handleCommand(Namespace ns, ProvisioningManager m) throws CommandException;
}

View file

@ -3,23 +3,20 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import java.io.IOException;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
public class QuitGroupCommand implements LocalCommand {
@ -30,31 +27,28 @@ public class QuitGroupCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
final var writer = new PlainTextWriterImpl(System.out);
final GroupId groupId;
try {
groupId = Util.decodeGroupId(ns.getString("group"));
} catch (GroupIdFormatException e) {
throw new UserErrorException("Invalid group id:" + e.getMessage());
}
try {
final GroupId groupId = Util.decodeGroupId(ns.getString("group"));
final Pair<Long, List<SendMessageResult>> results = m.sendQuitGroupMessage(groupId);
return handleTimestampAndSendMessageResults(results.first(), results.second());
final var results = m.sendQuitGroupMessage(groupId);
handleTimestampAndSendMessageResults(writer, results.first(), results.second());
} catch (IOException e) {
handleIOException(e);
return 3;
throw new IOErrorException("Failed to send message: " + e.getMessage());
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
throw e;
} catch (GroupNotFoundException e) {
handleGroupNotFoundException(e);
return 1;
throw new UserErrorException("Failed to send to group: " + e.getMessage());
} catch (NotAGroupMemberException e) {
handleNotAGroupMemberException(e);
return 1;
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
throw new UserErrorException("Failed to send to group: " + e.getMessage());
}
}
}

View file

@ -1,32 +1,38 @@
package org.asamk.signal.commands;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.JsonReceiveMessageHandler;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputType;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.ReceiveMessageHandler;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.json.JsonMessageEnvelope;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.DateUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.whispersystems.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Base64;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(ReceiveCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-t", "--timeout")
@ -36,146 +42,136 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
.help("Dont download attachments of received messages.")
.action(Arguments.storeTrue());
subparser.addArgument("--json")
.help("Output received messages in json format, one json object per line.")
.help("WARNING: This parameter is now deprecated! Please use the global \"--output=json\" option instead.\n\nOutput received messages in json format, one json object per line.")
.action(Arguments.storeTrue());
}
public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) {
final ObjectMapper jsonProcessor;
@Override
public Set<OutputType> getSupportedOutputTypes() {
return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON);
}
public void handleCommand(
final Namespace ns, final Signal signal, DBusConnection dbusconnection
) throws CommandException {
var inJson = ns.get("output") == OutputType.JSON || ns.getBoolean("json");
// TODO delete later when "json" variable is removed
if (ns.getBoolean("json")) {
jsonProcessor = new ObjectMapper();
jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect
jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
} else {
jsonProcessor = null;
logger.warn("\"--json\" option has been deprecated, please use the global \"--output=json\" instead.");
}
try {
dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> {
if (jsonProcessor != null) {
JsonMessageEnvelope envelope = new JsonMessageEnvelope(messageReceived);
ObjectNode result = jsonProcessor.createObjectNode();
result.putPOJO("envelope", envelope);
try {
jsonProcessor.writeValue(System.out, result);
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n",
messageReceived.getSender(),
DateUtils.formatTimestamp(messageReceived.getTimestamp()),
messageReceived.getMessage()));
if (inJson) {
final var jsonWriter = new JsonWriter(System.out);
dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> {
var envelope = new JsonMessageEnvelope(messageReceived);
final var object = Map.of("envelope", envelope);
jsonWriter.write(object);
});
dbusconnection.addSigHandler(Signal.ReceiptReceived.class, receiptReceived -> {
var envelope = new JsonMessageEnvelope(receiptReceived);
final var object = Map.of("envelope", envelope);
jsonWriter.write(object);
});
dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> {
var envelope = new JsonMessageEnvelope(syncReceived);
final var object = Map.of("envelope", envelope);
jsonWriter.write(object);
});
} else {
final var writer = new PlainTextWriterImpl(System.out);
dbusconnection.addSigHandler(Signal.MessageReceived.class, messageReceived -> {
writer.println("Envelope from: {}", messageReceived.getSender());
writer.println("Timestamp: {}", DateUtils.formatTimestamp(messageReceived.getTimestamp()));
writer.println("Body: {}", messageReceived.getMessage());
if (messageReceived.getGroupId().length > 0) {
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(messageReceived.getGroupId()));
writer.println("Group info:");
writer.indentedWriter()
.println("Id: {}", Base64.getEncoder().encodeToString(messageReceived.getGroupId()));
}
if (messageReceived.getAttachments().size() > 0) {
System.out.println("Attachments: ");
for (String attachment : messageReceived.getAttachments()) {
System.out.println("- Stored plaintext in: " + attachment);
writer.println("Attachments:");
for (var attachment : messageReceived.getAttachments()) {
writer.println("- Stored plaintext in: {}", attachment);
}
}
System.out.println();
}
});
writer.println();
});
dbusconnection.addSigHandler(Signal.ReceiptReceived.class, receiptReceived -> {
if (jsonProcessor != null) {
JsonMessageEnvelope envelope = new JsonMessageEnvelope(receiptReceived);
ObjectNode result = jsonProcessor.createObjectNode();
result.putPOJO("envelope", envelope);
try {
jsonProcessor.writeValue(System.out, result);
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n",
receiptReceived.getSender(),
DateUtils.formatTimestamp(receiptReceived.getTimestamp())));
}
});
dbusconnection.addSigHandler(Signal.ReceiptReceived.class, receiptReceived -> {
writer.println("Receipt from: {}", receiptReceived.getSender());
writer.println("Timestamp: {}", DateUtils.formatTimestamp(receiptReceived.getTimestamp()));
});
dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> {
if (jsonProcessor != null) {
JsonMessageEnvelope envelope = new JsonMessageEnvelope(syncReceived);
ObjectNode result = jsonProcessor.createObjectNode();
result.putPOJO("envelope", envelope);
try {
jsonProcessor.writeValue(System.out, result);
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
} else {
System.out.print(String.format("Sync Envelope from: %s to: %s\nTimestamp: %s\nBody: %s\n",
dbusconnection.addSigHandler(Signal.SyncMessageReceived.class, syncReceived -> {
writer.println("Sync Envelope from: {} to: {}",
syncReceived.getSource(),
syncReceived.getDestination(),
DateUtils.formatTimestamp(syncReceived.getTimestamp()),
syncReceived.getMessage()));
syncReceived.getDestination());
writer.println("Timestamp: {}", DateUtils.formatTimestamp(syncReceived.getTimestamp()));
writer.println("Body: {}", syncReceived.getMessage());
if (syncReceived.getGroupId().length > 0) {
System.out.println("Group info:");
System.out.println(" Id: " + Base64.encodeBytes(syncReceived.getGroupId()));
writer.println("Group info:");
writer.indentedWriter()
.println("Id: {}", Base64.getEncoder().encodeToString(syncReceived.getGroupId()));
}
if (syncReceived.getAttachments().size() > 0) {
System.out.println("Attachments: ");
for (String attachment : syncReceived.getAttachments()) {
System.out.println("- Stored plaintext in: " + attachment);
writer.println("Attachments:");
for (var attachment : syncReceived.getAttachments()) {
writer.println("- Stored plaintext in: {}", attachment);
}
}
System.out.println();
}
});
} catch (UnsatisfiedLinkError e) {
System.err.println("Missing native library dependency for dbus service: " + e.getMessage());
return 1;
writer.println();
});
}
} catch (DBusException e) {
e.printStackTrace();
return 1;
logger.error("Dbus client failed", e);
throw new UnexpectedErrorException("Dbus client failed");
}
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
return 0;
} catch (InterruptedException ignored) {
return;
}
}
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
var inJson = ns.get("output") == OutputType.JSON || ns.getBoolean("json");
// TODO delete later when "json" variable is removed
if (ns.getBoolean("json")) {
logger.warn("\"--json\" option has been deprecated, please use the global \"--output=json\" instead.");
}
double timeout = 5;
if (ns.getDouble("timeout") != null) {
timeout = ns.getDouble("timeout");
}
boolean returnOnTimeout = true;
var returnOnTimeout = true;
if (timeout < 0) {
returnOnTimeout = false;
timeout = 3600;
}
boolean ignoreAttachments = ns.getBoolean("ignore_attachments");
try {
final Manager.ReceiveMessageHandler handler = ns.getBoolean("json")
? new JsonReceiveMessageHandler(m)
: new ReceiveMessageHandler(m);
final var handler = inJson ? new JsonReceiveMessageHandler(m) : new ReceiveMessageHandler(m);
m.receiveMessages((long) (timeout * 1000),
TimeUnit.MILLISECONDS,
returnOnTimeout,
ignoreAttachments,
handler);
return 0;
} catch (IOException e) {
System.err.println("Error while receiving messages: " + e.getMessage());
return 3;
throw new IOErrorException("Error while receiving messages: " + e.getMessage());
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
throw e;
}
}
}

View file

@ -4,12 +4,15 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.RegistrationManager;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import java.io.IOException;
public class RegisterCommand implements LocalCommand {
public class RegisterCommand implements RegistrationCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
@ -21,18 +24,25 @@ public class RegisterCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
public void handleCommand(final Namespace ns, final RegistrationManager m) throws CommandException {
final boolean voiceVerification = ns.getBoolean("voice");
final var captcha = ns.getString("captcha");
try {
final boolean voiceVerification = ns.getBoolean("voice");
final String captcha = ns.getString("captcha");
m.register(voiceVerification, captcha);
return 0;
} catch (CaptchaRequiredException e) {
System.err.println("Captcha invalid or required for verification (" + e.getMessage() + ")");
return 1;
String message;
if (captcha == null) {
message = "Captcha required for verification, use --captcha CAPTCHA\n"
+ "To get the token, go to https://signalcaptchas.org/registration/generate.html\n"
+ "Check the developer tools (F12) console for a failed redirect to signalcaptcha://\n"
+ "Everything after signalcaptcha:// is the captcha token.";
} else {
message = "Invalid captcha given.";
}
throw new UserErrorException(message);
} catch (IOException e) {
System.err.println("Request verify error: " + e.getMessage());
return 3;
throw new IOErrorException("Request verify error: " + e.getMessage());
}
}
}

View file

@ -0,0 +1,11 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.RegistrationManager;
public interface RegistrationCommand extends Command {
void handleCommand(Namespace ns, RegistrationManager m) throws CommandException;
}

View file

@ -0,0 +1,82 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.errors.UnknownObject;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
public class RemoteDeleteCommand implements DbusCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.help("Remotely delete a previously sent message.");
subparser.addArgument("-t", "--target-timestamp")
.required(true)
.type(long.class)
.help("Specify the timestamp of the message to delete.");
subparser.addArgument("-g", "--group")
.help("Specify the recipient group ID.");
subparser.addArgument("recipient")
.help("Specify the recipients' phone number.").nargs("*");
}
@Override
public void handleCommand(final Namespace ns, final Signal signal) throws CommandException {
final List<String> recipients = ns.getList("recipient");
final var groupIdString = ns.getString("group");
final var noRecipients = recipients == null || recipients.isEmpty();
if (noRecipients && groupIdString == null) {
throw new UserErrorException("No recipients given");
}
if (!noRecipients && groupIdString != null) {
throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
}
final long targetTimestamp = ns.getLong("target_timestamp");
final var writer = new PlainTextWriterImpl(System.out);
byte[] groupId = null;
if (groupIdString != null) {
try {
groupId = Util.decodeGroupId(groupIdString).serialize();
} catch (GroupIdFormatException e) {
throw new UserErrorException("Invalid group id: " + e.getMessage());
}
}
try {
long timestamp;
if (groupId != null) {
timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId);
} else {
timestamp = signal.sendRemoteDeleteMessage(targetTimestamp, recipients);
}
writer.println("{}", timestamp);
} catch (AssertionError e) {
handleAssertionError(e);
throw e;
} catch (UnknownObject e) {
throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
} catch (Signal.Error.InvalidNumber e) {
throw new UserErrorException("Invalid number: " + e.getMessage());
} catch (Signal.Error.GroupNotFound e) {
throw new UserErrorException("Failed to send to group: " + e.getMessage());
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage());
}
}
}

View file

@ -3,6 +3,8 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.Manager;
import java.io.IOException;
@ -18,18 +20,12 @@ public class RemoveDeviceCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
try {
int deviceId = ns.getInt("deviceId");
m.removeLinkedDevices(deviceId);
return 0;
} catch (IOException e) {
e.printStackTrace();
return 3;
throw new IOErrorException("Error while removing device: " + e.getMessage());
}
}
}

View file

@ -3,8 +3,12 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
@ -15,17 +19,13 @@ public class RemovePinCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
try {
m.setRegistrationLockPin(Optional.absent());
return 0;
} catch (UnauthenticatedResponseException e) {
throw new UnexpectedErrorException("Remove pin failed with unauthenticated response: " + e.getMessage());
} catch (IOException e) {
System.err.println("Remove pin error: " + e.getMessage());
return 3;
throw new IOErrorException("Remove pin error: " + e.getMessage());
}
}
}

View file

@ -5,25 +5,38 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.util.IOUtils;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.errors.UnknownObject;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
public class SendCommand implements DbusCommand {
private final static Logger logger = LoggerFactory.getLogger(SendCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-g", "--group").help("Specify the recipient group ID.");
subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*");
final var mutuallyExclusiveGroup = subparser.addMutuallyExclusiveGroup();
mutuallyExclusiveGroup.addArgument("-g", "--group").help("Specify the recipient group ID.");
mutuallyExclusiveGroup.addArgument("--note-to-self")
.help("Send the message to self without notification.")
.action(Arguments.storeTrue());
subparser.addArgument("-m", "--message").help("Specify the message, if missing standard input is used.");
subparser.addArgument("-a", "--attachment").nargs("*").help("Add file as attachment");
subparser.addArgument("-e", "--endsession")
@ -32,82 +45,108 @@ public class SendCommand implements DbusCommand {
}
@Override
public int handleCommand(final Namespace ns, final Signal signal) {
if (!signal.isRegistered()) {
System.err.println("User is not registered.");
return 1;
public void handleCommand(final Namespace ns, final Signal signal) throws CommandException {
final List<String> recipients = ns.getList("recipient");
final var isEndSession = ns.getBoolean("endsession");
final var groupIdString = ns.getString("group");
final var isNoteToSelf = ns.getBoolean("note_to_self");
final var noRecipients = recipients == null || recipients.isEmpty();
if ((noRecipients && isEndSession) || (noRecipients && groupIdString == null && !isNoteToSelf)) {
throw new UserErrorException("No recipients given");
}
if (!noRecipients && groupIdString != null) {
throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
}
if (!noRecipients && isNoteToSelf) {
throw new UserErrorException(
"You cannot specify recipients by phone number and not to self at the same time");
}
if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && (
ns.getBoolean("endsession") || ns.getString("group") == null
)) {
System.err.println("No recipients given");
System.err.println("Aborting sending.");
return 1;
}
if (ns.getBoolean("endsession")) {
if (isEndSession) {
try {
signal.sendEndSessionMessage(ns.getList("recipient"));
return 0;
signal.sendEndSessionMessage(recipients);
return;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
throw e;
} catch (Signal.Error.UntrustedIdentity e) {
throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage());
} catch (DBusExecutionException e) {
System.err.println("Failed to send message: " + e.getMessage());
return 1;
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage());
}
}
String messageText = ns.getString("message");
var messageText = ns.getString("message");
if (messageText == null) {
try {
messageText = IOUtils.readAll(System.in, Charset.defaultCharset());
} catch (IOException e) {
System.err.println("Failed to read message from stdin: " + e.getMessage());
System.err.println("Aborting sending.");
return 1;
throw new UserErrorException("Failed to read message from stdin: " + e.getMessage());
}
}
List<String> attachments = ns.getList("attachment");
if (attachments == null) {
attachments = new ArrayList<>();
attachments = List.of();
}
try {
if (ns.getString("group") != null) {
byte[] groupId;
try {
groupId = Util.decodeGroupId(ns.getString("group")).serialize();
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
}
final var writer = new PlainTextWriterImpl(System.out);
long timestamp = signal.sendGroupMessage(messageText, attachments, groupId);
System.out.println(timestamp);
return 0;
if (groupIdString != null) {
byte[] groupId;
try {
groupId = Util.decodeGroupId(groupIdString).serialize();
} catch (GroupIdFormatException e) {
throw new UserErrorException("Invalid group id: " + e.getMessage());
}
try {
var timestamp = signal.sendGroupMessage(messageText, attachments, groupId);
writer.println("{}", timestamp);
return;
} catch (AssertionError e) {
handleAssertionError(e);
throw e;
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage());
}
}
if (isNoteToSelf) {
try {
var timestamp = signal.sendNoteToSelfMessage(messageText, attachments);
writer.println("{}", timestamp);
return;
} catch (AssertionError e) {
handleAssertionError(e);
throw e;
} catch (Signal.Error.UntrustedIdentity e) {
throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage());
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage());
}
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (DBusExecutionException e) {
System.err.println("Failed to send message: " + e.getMessage());
return 1;
}
try {
<<<<<<< HEAD
System.out.println(Arrays.toString(ns.getList("recipient").toArray()));
long timestamp = signal.sendMessage(messageText, attachments, ns.getList("recipient"));
System.out.println(timestamp);
return 0;
=======
var timestamp = signal.sendMessage(messageText, attachments, recipients);
writer.println("{}", timestamp);
>>>>>>> upstream/master
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
throw e;
} catch (UnknownObject e) {
throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
} catch (Signal.Error.UntrustedIdentity e) {
throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage());
} catch (DBusExecutionException e) {
System.err.println("Failed to send message: " + e.getMessage());
return 1;
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage());
}
}
}

View file

@ -3,6 +3,9 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
@ -16,17 +19,13 @@ public class SendContactsCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
try {
m.sendContacts();
return 0;
} catch (IOException | UntrustedIdentityException e) {
System.err.println("SendContacts error: " + e.getMessage());
return 3;
} catch (UntrustedIdentityException e) {
throw new UntrustedKeyErrorException("SendContacts error: " + e.getMessage());
} catch (IOException e) {
throw new IOErrorException("SendContacts error: " + e.getMessage());
}
}
}

View file

@ -4,28 +4,21 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.Signal;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.freedesktop.dbus.errors.UnknownObject;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import java.io.IOException;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException;
import static org.asamk.signal.util.ErrorUtils.handleIOException;
import static org.asamk.signal.util.ErrorUtils.handleInvalidNumberException;
import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException;
import static org.asamk.signal.util.ErrorUtils.handleTimestampAndSendMessageResults;
public class SendReactionCommand implements LocalCommand {
public class SendReactionCommand implements DbusCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
@ -46,54 +39,53 @@ public class SendReactionCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
public void handleCommand(final Namespace ns, final Signal signal) throws CommandException {
final List<String> recipients = ns.getList("recipient");
final var groupIdString = ns.getString("group");
final var noRecipients = recipients == null || recipients.isEmpty();
if (noRecipients && groupIdString == null) {
throw new UserErrorException("No recipients given");
}
if (!noRecipients && groupIdString != null) {
throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
}
if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && ns.getString("group") == null) {
System.err.println("No recipients given");
System.err.println("Aborting sending.");
return 1;
}
final var emoji = ns.getString("emoji");
final boolean isRemove = ns.getBoolean("remove");
final var targetAuthor = ns.getString("target_author");
final long targetTimestamp = ns.getLong("target_timestamp");
String emoji = ns.getString("emoji");
boolean isRemove = ns.getBoolean("remove");
String targetAuthor = ns.getString("target_author");
long targetTimestamp = ns.getLong("target_timestamp");
final var writer = new PlainTextWriterImpl(System.out);
byte[] groupId = null;
if (groupIdString != null) {
try {
groupId = Util.decodeGroupId(groupIdString).serialize();
} catch (GroupIdFormatException e) {
throw new UserErrorException("Invalid group id: " + e.getMessage());
}
}
try {
final Pair<Long, List<SendMessageResult>> results;
if (ns.getString("group") != null) {
GroupId groupId = Util.decodeGroupId(ns.getString("group"));
results = m.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
long timestamp;
if (groupId != null) {
timestamp = signal.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId);
} else {
results = m.sendMessageReaction(emoji,
isRemove,
targetAuthor,
targetTimestamp,
ns.getList("recipient"));
timestamp = signal.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, recipients);
}
return handleTimestampAndSendMessageResults(results.first(), results.second());
} catch (IOException e) {
handleIOException(e);
return 3;
writer.println("{}", timestamp);
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
} catch (GroupNotFoundException e) {
handleGroupNotFoundException(e);
return 1;
} catch (NotAGroupMemberException e) {
handleNotAGroupMemberException(e);
return 1;
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
} catch (InvalidNumberException e) {
handleInvalidNumberException(e);
return 1;
throw e;
} catch (UnknownObject e) {
throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
} catch (Signal.Error.InvalidNumber e) {
throw new UserErrorException("Invalid number: " + e.getMessage());
} catch (Signal.Error.GroupNotFound e) {
throw new UserErrorException("Failed to send to group: " + e.getMessage());
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage());
}
}
}

View file

@ -3,8 +3,12 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
@ -17,18 +21,14 @@ public class SetPinCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
try {
String registrationLockPin = ns.getString("registrationLockPin");
var registrationLockPin = ns.getString("registrationLockPin");
m.setRegistrationLockPin(Optional.of(registrationLockPin));
return 0;
} catch (UnauthenticatedResponseException e) {
throw new UnexpectedErrorException("Set pin error failed with unauthenticated response: " + e.getMessage());
} catch (IOException e) {
System.err.println("Set pin error: " + e.getMessage());
return 3;
throw new IOErrorException("Set pin error: " + e.getMessage());
}
}
}

View file

@ -1,12 +1,12 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.ErrorUtils;
import org.asamk.signal.util.Hex;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -17,7 +17,7 @@ public class TrustCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("number").help("Specify the phone number, for which to set the trust.").required(true);
MutuallyExclusiveGroup mutTrust = subparser.addMutuallyExclusiveGroup();
var mutTrust = subparser.addMutuallyExclusiveGroup();
mutTrust.addArgument("-a", "--trust-all-known-keys")
.help("Trust all known keys of this user, only use this for testing.")
.action(Arguments.storeTrue());
@ -26,20 +26,15 @@ public class TrustCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
String number = ns.getString("number");
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
var number = ns.getString("number");
if (ns.getBoolean("trust_all_known_keys")) {
boolean res = m.trustIdentityAllKeys(number);
var res = m.trustIdentityAllKeys(number);
if (!res) {
System.err.println("Failed to set the trust for this number, make sure the number is correct.");
return 1;
throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct.");
}
} else {
String safetyNumber = ns.getString("verified_safety_number");
var safetyNumber = ns.getString("verified_safety_number");
if (safetyNumber != null) {
safetyNumber = safetyNumber.replaceAll(" ", "");
if (safetyNumber.length() == 66) {
@ -47,46 +42,38 @@ public class TrustCommand implements LocalCommand {
try {
fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT));
} catch (Exception e) {
System.err.println(
throw new UserErrorException(
"Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters.");
return 1;
}
boolean res;
try {
res = m.trustIdentityVerified(number, fingerprintBytes);
} catch (InvalidNumberException e) {
ErrorUtils.handleInvalidNumberException(e);
return 1;
throw new UserErrorException("Failed to parse recipient: " + e.getMessage());
}
if (!res) {
System.err.println(
throw new UserErrorException(
"Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
return 1;
}
} else if (safetyNumber.length() == 60) {
boolean res;
try {
res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber);
} catch (InvalidNumberException e) {
ErrorUtils.handleInvalidNumberException(e);
return 1;
throw new UserErrorException("Failed to parse recipient: " + e.getMessage());
}
if (!res) {
System.err.println(
throw new UserErrorException(
"Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct.");
return 1;
}
} else {
System.err.println(
throw new UserErrorException(
"Safety number has invalid format, either specify the old hex fingerprint or the new safety number");
return 1;
}
} else {
System.err.println(
throw new UserErrorException(
"You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER");
return 1;
}
}
return 0;
}
}

View file

@ -3,15 +3,19 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
public class UnblockCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(UnblockCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("contact").help("Contact number").nargs("*");
@ -20,31 +24,26 @@ public class UnblockCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
for (String contact_number : ns.<String>getList("contact")) {
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
for (var contactNumber : ns.<String>getList("contact")) {
try {
m.setContactBlocked(contact_number, false);
m.setContactBlocked(contactNumber, false);
} catch (InvalidNumberException e) {
System.err.println(e.getMessage());
logger.warn("Invalid number: {}", contactNumber);
}
}
if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) {
for (var groupIdString : ns.<String>getList("group")) {
try {
GroupId groupId = Util.decodeGroupId(groupIdString);
var groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, false);
} catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage());
} catch (GroupIdFormatException e) {
logger.warn("Invalid group id: {}", groupIdString);
} catch (GroupNotFoundException e) {
logger.warn("Unknown group id: {}", groupIdString);
}
}
}
return 0;
}
}

View file

@ -3,6 +3,8 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.Manager;
import java.io.IOException;
@ -15,17 +17,11 @@ public class UnregisterCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
try {
m.unregister();
return 0;
} catch (IOException e) {
System.err.println("Unregister error: " + e.getMessage());
return 3;
throw new IOErrorException("Unregister error: " + e.getMessage());
}
}
}

View file

@ -3,6 +3,8 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.Manager;
import java.io.IOException;
@ -15,17 +17,11 @@ public class UpdateAccountCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
try {
m.updateAccountAttributes();
return 0;
} catch (IOException e) {
System.err.println("UpdateAccount error: " + e.getMessage());
return 3;
throw new IOErrorException("UpdateAccount error: " + e.getMessage());
}
}
}

View file

@ -3,6 +3,9 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
@ -22,30 +25,21 @@ public class UpdateContactCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
String number = ns.getString("number");
String name = ns.getString("name");
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
var number = ns.getString("number");
var name = ns.getString("name");
try {
m.setContactName(number, name);
Integer expiration = ns.getInt("expiration");
var expiration = ns.getInt("expiration");
if (expiration != null) {
m.setExpirationTimer(number, expiration);
}
} catch (InvalidNumberException e) {
System.err.println("Invalid contact number: " + e.getMessage());
return 1;
throw new UserErrorException("Invalid contact number: " + e.getMessage());
} catch (IOException e) {
System.err.println("Update contact error: " + e.getMessage());
return 3;
throw new IOErrorException("Update contact error: " + e.getMessage());
}
return 0;
}
}

View file

@ -4,19 +4,26 @@ import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.asamk.signal.util.Util;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import static org.asamk.signal.util.ErrorUtils.handleAssertionError;
import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException;
public class UpdateGroupCommand implements DbusCommand {
private final static Logger logger = LoggerFactory.getLogger(UpdateGroupCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("-g", "--group").help("Specify the recipient group ID.");
@ -26,26 +33,21 @@ public class UpdateGroupCommand implements DbusCommand {
}
@Override
public int handleCommand(final Namespace ns, final Signal signal) {
if (!signal.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
public void handleCommand(final Namespace ns, final Signal signal) throws CommandException {
final var writer = new PlainTextWriterImpl(System.out);
byte[] groupId = null;
if (ns.getString("group") != null) {
try {
groupId = Util.decodeGroupId(ns.getString("group")).serialize();
} catch (GroupIdFormatException e) {
handleGroupIdFormatException(e);
return 1;
throw new UserErrorException("Invalid group id:" + e.getMessage());
}
}
if (groupId == null) {
groupId = new byte[0];
}
String groupName = ns.getString("name");
var groupName = ns.getString("name");
if (groupName == null) {
groupName = "";
}
@ -55,26 +57,23 @@ public class UpdateGroupCommand implements DbusCommand {
groupMembers = new ArrayList<>();
}
String groupAvatar = ns.getString("avatar");
var groupAvatar = ns.getString("avatar");
if (groupAvatar == null) {
groupAvatar = "";
}
try {
byte[] newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar);
var newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar);
if (groupId.length != newGroupId.length) {
System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\"");
writer.println("Created new group: \"{}\"", Base64.getEncoder().encodeToString(newGroupId));
}
return 0;
} catch (AssertionError e) {
handleAssertionError(e);
return 1;
throw e;
} catch (Signal.Error.AttachmentInvalid e) {
System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
return 1;
throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage());
} catch (DBusExecutionException e) {
System.err.println("Failed to send message: " + e.getMessage());
return 1;
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage());
}
}
}

View file

@ -1,11 +1,13 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.manager.Manager;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.IOException;
@ -14,34 +16,33 @@ public class UpdateProfileCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup().required(true);
subparser.addArgument("--name").help("New profile name");
subparser.addArgument("--about").help("New profile about text");
subparser.addArgument("--about-emoji").help("New profile about emoji");
final var avatarOptions = subparser.addMutuallyExclusiveGroup();
avatarOptions.addArgument("--avatar").help("Path to new profile avatar");
avatarOptions.addArgument("--remove-avatar").action(Arguments.storeTrue());
subparser.addArgument("--name").required(true).help("New profile name");
subparser.help("Set a name and avatar image for the user profile");
subparser.help("Set a name, about and avatar image for the user profile");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
String name = ns.getString("name");
String avatarPath = ns.getString("avatar");
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
var name = ns.getString("name");
var about = ns.getString("about");
var aboutEmoji = ns.getString("about_emoji");
var avatarPath = ns.getString("avatar");
boolean removeAvatar = ns.getBoolean("remove_avatar");
try {
File avatarFile = removeAvatar ? null : new File(avatarPath);
m.setProfile(name, avatarFile);
} catch (IOException e) {
System.err.println("UpdateAccount error: " + e.getMessage());
return 3;
}
Optional<File> avatarFile = removeAvatar
? Optional.absent()
: avatarPath == null ? null : Optional.of(new File(avatarPath));
return 0;
try {
m.setProfile(name, about, aboutEmoji, avatarFile);
} catch (IOException e) {
throw new IOErrorException("Update profile error: " + e.getMessage());
}
}
}

View file

@ -3,14 +3,22 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.PlainTextWriterImpl;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.StickerPackInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
public class UploadStickerPackCommand implements LocalCommand {
private final static Logger logger = LoggerFactory.getLogger(UploadStickerPackCommand.class);
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("path")
@ -18,18 +26,17 @@ public class UploadStickerPackCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
public void handleCommand(final Namespace ns, final Manager m) throws CommandException {
final var writer = new PlainTextWriterImpl(System.out);
var path = new File(ns.getString("path"));
try {
File path = new File(ns.getString("path"));
String url = m.uploadStickerPack(path);
System.out.println(url);
return 0;
var url = m.uploadStickerPack(path);
writer.println("{}", url);
} catch (IOException e) {
System.err.println("Upload error: " + e.getMessage());
return 3;
throw new IOErrorException("Upload error: " + e.getMessage());
} catch (StickerPackInvalidException e) {
System.err.println("Invalid sticker pack: " + e.getMessage());
return 3;
throw new UserErrorException("Invalid sticker pack: " + e.getMessage());
}
}
}

View file

@ -3,12 +3,18 @@ package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.RegistrationManager;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException;
public class VerifyCommand implements LocalCommand {
public class VerifyCommand implements RegistrationCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
@ -17,24 +23,24 @@ public class VerifyCommand implements LocalCommand {
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (m.isRegistered()) {
System.err.println("User registration is already verified");
return 1;
}
public void handleCommand(final Namespace ns, final RegistrationManager m) throws CommandException {
var verificationCode = ns.getString("verificationCode");
var pin = ns.getString("pin");
try {
String verificationCode = ns.getString("verificationCode");
String pin = ns.getString("pin");
m.verifyAccount(verificationCode, pin);
return 0;
final var manager = m.verifyAccount(verificationCode, pin);
manager.close();
} catch (LockedException e) {
System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: "
+ (e.getTimeRemaining() / 1000 / 60 / 60));
System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN");
return 3;
throw new UserErrorException(
"Verification failed! This number is locked with a pin. Hours remaining until reset: "
+ (e.getTimeRemaining() / 1000 / 60 / 60)
+ "\nUse '--pin PIN_CODE' to specify the registration lock PIN");
} catch (KeyBackupServicePinException e) {
throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining());
} catch (KeyBackupSystemNoDataException e) {
throw new UnexpectedErrorException("Verification failed! No KBS data.");
} catch (IOException e) {
System.err.println("Verify error: " + e.getMessage());
return 3;
throw new IOErrorException("Verify error: " + e.getMessage());
}
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.commands.exceptions;
public class CommandException extends Exception {
public CommandException(final String message) {
super(message);
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.commands.exceptions;
public final class IOErrorException extends CommandException {
public IOErrorException(final String message) {
super(message);
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.commands.exceptions;
public final class UnexpectedErrorException extends CommandException {
public UnexpectedErrorException(final String message) {
super(message);
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.commands.exceptions;
public final class UntrustedKeyErrorException extends CommandException {
public UntrustedKeyErrorException(final String message) {
super(message);
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.commands.exceptions;
public final class UserErrorException extends CommandException {
public UserErrorException(final String message) {
super(message);
}
}

View file

@ -1,24 +1,28 @@
package org.asamk.signal.dbus;
import org.asamk.Signal;
import org.asamk.signal.BaseConfig;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DbusSignalImpl implements Signal {
@ -40,23 +44,44 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendMessage(final String message, final List<String> attachments, final String recipient) {
List<String> recipients = new ArrayList<>(1);
var recipients = new ArrayList<String>(1);
recipients.add(recipient);
return sendMessage(message, attachments, recipients);
}
private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
if (error == null) {
return;
}
final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
if (result.getIdentityFailure() != null) {
throw new Error.UntrustedIdentity(message);
} else {
throw new Error.Failure(message);
}
}
private static void checkSendMessageResults(
long timestamp, List<SendMessageResult> results
) throws DBusExecutionException {
List<String> errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
if (results.size() == 1) {
checkSendMessageResult(timestamp, results.get(0));
return;
}
var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
if (errors.size() == 0) {
return;
}
StringBuilder message = new StringBuilder();
var message = new StringBuilder();
message.append(timestamp).append('\n');
message.append("Failed to send (some) messages:\n");
for (String error : errors) {
for (var error : errors) {
message.append(error).append('\n');
}
@ -66,7 +91,7 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
try {
final Pair<Long, List<SendMessageResult>> results = m.sendMessage(message, attachments, recipients);
final var results = m.sendMessage(message, attachments, recipients);
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (InvalidNumberException e) {
@ -78,10 +103,88 @@ public class DbusSignalImpl implements Signal {
}
}
@Override
public long sendRemoteDeleteMessage(
final long targetSentTimestamp, final String recipient
) {
var recipients = new ArrayList<String>(1);
recipients.add(recipient);
return sendRemoteDeleteMessage(targetSentTimestamp, recipients);
}
@Override
public long sendRemoteDeleteMessage(
final long targetSentTimestamp, final List<String> recipients
) {
try {
final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, recipients);
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
}
}
@Override
public long sendGroupRemoteDeleteMessage(
final long targetSentTimestamp, final byte[] groupId
) {
try {
final var results = m.sendGroupRemoteDeleteMessage(targetSentTimestamp, GroupId.unknownVersion(groupId));
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (GroupNotFoundException | NotAGroupMemberException e) {
throw new Error.GroupNotFound(e.getMessage());
}
}
@Override
public long sendMessageReaction(
final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final String recipient
) {
var recipients = new ArrayList<String>(1);
recipients.add(recipient);
return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
}
@Override
public long sendMessageReaction(
final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final List<String> recipients
) {
try {
final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
@Override
public long sendNoteToSelfMessage(
final String message, final List<String> attachments
) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
try {
final var results = m.sendSelfMessage(message, attachments);
checkSendMessageResult(results.first(), results.second());
return results.first();
} catch (AttachmentInvalidException e) {
throw new Error.AttachmentInvalid(e.getMessage());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
@Override
public void sendEndSessionMessage(final List<String> recipients) {
try {
final Pair<Long, List<SendMessageResult>> results = m.sendEndSessionMessage(recipients);
final var results = m.sendEndSessionMessage(recipients);
checkSendMessageResults(results.first(), results.second());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
@ -93,9 +196,7 @@ public class DbusSignalImpl implements Signal {
@Override
public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
try {
Pair<Long, List<SendMessageResult>> results = m.sendGroupMessage(message,
attachments,
GroupId.unknownVersion(groupId));
var results = m.sendGroupMessage(message, attachments, GroupId.unknownVersion(groupId));
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (IOException e) {
@ -107,11 +208,30 @@ public class DbusSignalImpl implements Signal {
}
}
@Override
public long sendGroupMessageReaction(
final String emoji, final boolean remove, final String targetAuthor, final long targetSentTimestamp, final byte[] groupId
) {
try {
final var results = m.sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, GroupId.unknownVersion(groupId));
checkSendMessageResults(results.first(), results.second());
return results.first();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (InvalidNumberException e) {
throw new Error.InvalidNumber(e.getMessage());
} catch (GroupNotFoundException | NotAGroupMemberException e) {
throw new Error.GroupNotFound(e.getMessage());
}
}
// Since contact names might be empty if not defined, also potentially return
// the profile name
@Override
public String getContactName(final String number) {
try {
return m.getContactName(number);
} catch (InvalidNumberException e) {
return m.getContactOrProfileName(number);
} catch (Exception e) {
throw new Error.InvalidNumber(e.getMessage());
}
}
@ -145,9 +265,9 @@ public class DbusSignalImpl implements Signal {
@Override
public List<byte[]> getGroupIds() {
List<GroupInfo> groups = m.getGroups();
List<byte[]> ids = new ArrayList<>(groups.size());
for (GroupInfo group : groups) {
var groups = m.getGroups();
var ids = new ArrayList<byte[]>(groups.size());
for (var group : groups) {
ids.add(group.getGroupId().serialize());
}
return ids;
@ -155,7 +275,7 @@ public class DbusSignalImpl implements Signal {
@Override
public String getGroupName(final byte[] groupId) {
GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
var group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) {
return "";
} else {
@ -165,9 +285,9 @@ public class DbusSignalImpl implements Signal {
@Override
public List<String> getGroupMembers(final byte[] groupId) {
GroupInfo group = m.getGroup(GroupId.unknownVersion(groupId));
var group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) {
return Collections.emptyList();
return List.of();
} else {
return group.getMembers()
.stream()
@ -192,9 +312,10 @@ public class DbusSignalImpl implements Signal {
if (avatar.isEmpty()) {
avatar = null;
}
final Pair<GroupId, List<SendMessageResult>> results = m.updateGroup(groupId == null
? null
: GroupId.unknownVersion(groupId), name, members, avatar);
final var results = m.updateGroup(groupId == null ? null : GroupId.unknownVersion(groupId),
name,
members,
avatar == null ? null : new File(avatar));
checkSendMessageResults(0, results.second());
return results.first().serialize();
} catch (IOException e) {
@ -212,4 +333,127 @@ public class DbusSignalImpl implements Signal {
public boolean isRegistered() {
return true;
}
@Override
public void updateProfile(
final String name,
final String about,
final String aboutEmoji,
String avatarPath,
final boolean removeAvatar
) {
try {
if (avatarPath.isEmpty()) {
avatarPath = null;
}
Optional<File> avatarFile = removeAvatar
? Optional.absent()
: avatarPath == null ? null : Optional.of(new File(avatarPath));
m.setProfile(name, about, aboutEmoji, avatarFile);
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
// Provide option to query a version string in order to react on potential
// future interface changes
@Override
public String version() {
return BaseConfig.PROJECT_VERSION;
}
// Create a unique list of Numbers from Identities and Contacts to really get
// all numbers the system knows
@Override
public List<String> listNumbers() {
return Stream.concat(m.getIdentities().stream().map(i -> i.getAddress().getNumber().orNull()),
m.getContacts().stream().map(c -> c.number))
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
@Override
public List<String> getContactNumber(final String name) {
// Contact names have precedence.
var numbers = new ArrayList<String>();
var contacts = m.getContacts();
for (var c : contacts) {
if (c.name != null && c.name.equals(name)) {
numbers.add(c.number);
}
}
// Try profiles if no contact name was found
for (var identity : m.getIdentities()) {
final var address = identity.getAddress();
var number = address.getNumber().orNull();
if (number != null) {
var profile = m.getRecipientProfile(address);
if (profile != null && profile.getDisplayName().equals(name)) {
numbers.add(number);
}
}
}
return numbers;
}
@Override
public void quitGroup(final byte[] groupId) {
var group = GroupId.unknownVersion(groupId);
try {
m.sendQuitGroupMessage(group);
} catch (GroupNotFoundException | NotAGroupMemberException e) {
throw new Error.GroupNotFound(e.getMessage());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
@Override
public void joinGroup(final String groupLink) {
try {
final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
if (linkUrl == null) {
throw new Error.Failure("Group link is invalid:");
}
m.joinGroup(linkUrl);
} catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
throw new Error.Failure("Group link is invalid: " + e.getMessage());
} catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
@Override
public boolean isContactBlocked(final String number) {
var contacts = m.getContacts();
for (var c : contacts) {
if (c.number.equals(number)) {
return c.blocked;
}
}
return false;
}
@Override
public boolean isGroupBlocked(final byte[] groupId) {
var group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) {
return false;
} else {
return group.isBlocked();
}
}
@Override
public boolean isMember(final byte[] groupId) {
var group = m.getGroup(GroupId.unknownVersion(groupId));
if (group == null) {
return false;
} else {
return group.isMember(m.getSelfAddress());
}
}
}

View file

@ -1,31 +1,43 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
class JsonAttachment {
String contentType;
String filename;
String id;
int size;
@JsonProperty
final String contentType;
@JsonProperty
final String filename;
@JsonProperty
final String id;
@JsonProperty
final Long size;
JsonAttachment(SignalServiceAttachment attachment) {
this.contentType = attachment.getContentType();
final SignalServiceAttachmentPointer pointer = attachment.asPointer();
if (attachment.isPointer()) {
this.id = String.valueOf(pointer.getRemoteId());
if (pointer.getFileName().isPresent()) {
this.filename = pointer.getFileName().get();
}
if (pointer.getSize().isPresent()) {
this.size = pointer.getSize().get();
}
final var pointer = attachment.asPointer();
this.id = pointer.getRemoteId().toString();
this.filename = pointer.getFileName().orNull();
this.size = pointer.getSize().transform(Integer::longValue).orNull();
} else {
final var stream = attachment.asStream();
this.id = null;
this.filename = stream.getFileName().orNull();
this.size = stream.getLength();
}
}
JsonAttachment(String filename) {
this.filename = filename;
this.contentType = null;
this.id = null;
this.size = null;
}
}

View file

@ -1,5 +1,8 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
@ -11,27 +14,31 @@ import java.util.List;
class JsonCallMessage {
OfferMessage offerMessage;
AnswerMessage answerMessage;
BusyMessage busyMessage;
HangupMessage hangupMessage;
List<IceUpdateMessage> iceUpdateMessages;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final OfferMessage offerMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final AnswerMessage answerMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final BusyMessage busyMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final HangupMessage hangupMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<IceUpdateMessage> iceUpdateMessages;
JsonCallMessage(SignalServiceCallMessage callMessage) {
if (callMessage.getOfferMessage().isPresent()) {
this.offerMessage = callMessage.getOfferMessage().get();
}
if (callMessage.getAnswerMessage().isPresent()) {
this.answerMessage = callMessage.getAnswerMessage().get();
}
if (callMessage.getBusyMessage().isPresent()) {
this.busyMessage = callMessage.getBusyMessage().get();
}
if (callMessage.getHangupMessage().isPresent()) {
this.hangupMessage = callMessage.getHangupMessage().get();
}
if (callMessage.getIceUpdateMessages().isPresent()) {
this.iceUpdateMessages = callMessage.getIceUpdateMessages().get();
}
this.offerMessage = callMessage.getOfferMessage().orNull();
this.answerMessage = callMessage.getAnswerMessage().orNull();
this.busyMessage = callMessage.getBusyMessage().orNull();
this.hangupMessage = callMessage.getHangupMessage().orNull();
this.iceUpdateMessages = callMessage.getIceUpdateMessages().orNull();
}
}

View file

@ -0,0 +1,48 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
public class JsonContactAddress {
@JsonProperty
private final SharedContact.PostalAddress.Type type;
@JsonProperty
private final String label;
@JsonProperty
private final String street;
@JsonProperty
private final String pobox;
@JsonProperty
private final String neighborhood;
@JsonProperty
private final String city;
@JsonProperty
private final String region;
@JsonProperty
private final String postcode;
@JsonProperty
private final String country;
public JsonContactAddress(SharedContact.PostalAddress address) {
type = address.getType();
label = Util.getStringIfNotBlank(address.getLabel());
street = Util.getStringIfNotBlank(address.getStreet());
pobox = Util.getStringIfNotBlank(address.getPobox());
neighborhood = Util.getStringIfNotBlank(address.getNeighborhood());
city = Util.getStringIfNotBlank(address.getCity());
region = Util.getStringIfNotBlank(address.getRegion());
postcode = Util.getStringIfNotBlank(address.getPostcode());
country = Util.getStringIfNotBlank(address.getCountry());
}
}

View file

@ -0,0 +1,19 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
public class JsonContactAvatar {
@JsonProperty
private final JsonAttachment attachment;
@JsonProperty
private final boolean isProfile;
public JsonContactAvatar(SharedContact.Avatar avatar) {
attachment = new JsonAttachment(avatar.getAttachment());
isProfile = avatar.isProfile();
}
}

View file

@ -0,0 +1,24 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
public class JsonContactEmail {
@JsonProperty
private final String value;
@JsonProperty
private final SharedContact.Email.Type type;
@JsonProperty
private final String label;
public JsonContactEmail(SharedContact.Email email) {
value = email.getValue();
type = email.getType();
label = Util.getStringIfNotBlank(email.getLabel());
}
}

View file

@ -0,0 +1,36 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
public class JsonContactName {
@JsonProperty
private final String display;
@JsonProperty
private final String given;
@JsonProperty
private final String family;
@JsonProperty
private final String prefix;
@JsonProperty
private final String suffix;
@JsonProperty
private final String middle;
public JsonContactName(SharedContact.Name name) {
display = Util.getStringIfNotBlank(name.getDisplay());
given = Util.getStringIfNotBlank(name.getGiven());
family = Util.getStringIfNotBlank(name.getFamily());
prefix = Util.getStringIfNotBlank(name.getPrefix());
suffix = Util.getStringIfNotBlank(name.getSuffix());
middle = Util.getStringIfNotBlank(name.getMiddle());
}
}

View file

@ -0,0 +1,24 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
public class JsonContactPhone {
@JsonProperty
private final String value;
@JsonProperty
private final SharedContact.Phone.Type type;
@JsonProperty
private final String label;
public JsonContactPhone(SharedContact.Phone phone) {
value = phone.getValue();
type = phone.getType();
label = Util.getStringIfNotBlank(phone.getLabel());
}
}

View file

@ -1,5 +1,8 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.Signal;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@ -27,9 +30,46 @@ class JsonReaction {
class JsonDataMessage {
long timestamp;
String message;
int expiresInSeconds;
@JsonProperty
final long timestamp;
@JsonProperty
final String message;
@JsonProperty
final Integer expiresInSeconds;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final Boolean viewOnce;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonReaction reaction;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonQuote quote;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonMention> mentions;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonAttachment> attachments;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonSticker sticker;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonRemoteDelete remoteDelete;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonSharedContact> contacts;
JsonReaction reaction;
JsonQuote quote;
@ -38,28 +78,33 @@ class JsonDataMessage {
JsonGroupInfo groupInfo;
JsonReaction reaction;
SignalServiceDataMessage.Quote quote;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonGroupInfo groupInfo;
JsonDataMessage(SignalServiceDataMessage dataMessage, Manager m) {
this.timestamp = dataMessage.getTimestamp();
if (dataMessage.getGroupContext().isPresent()) {
if (dataMessage.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = dataMessage.getGroupContext().get().getGroupV1().get();
final var groupContext = dataMessage.getGroupContext().get();
if (groupContext.getGroupV1().isPresent()) {
var groupInfo = groupContext.getGroupV1().get();
this.groupInfo = new JsonGroupInfo(groupInfo);
} else if (dataMessage.getGroupContext().get().getGroupV2().isPresent()) {
SignalServiceGroupV2 groupInfo = dataMessage.getGroupContext().get().getGroupV2().get();
} else if (groupContext.getGroupV2().isPresent()) {
var groupInfo = groupContext.getGroupV2().get();
this.groupInfo = new JsonGroupInfo(groupInfo);
} else {
this.groupInfo = null;
}
} else {
this.groupInfo = null;
}
if (dataMessage.getBody().isPresent()) {
this.message = dataMessage.getBody().get();
}
this.message = dataMessage.getBody().orNull();
this.expiresInSeconds = dataMessage.getExpiresInSeconds();
if (dataMessage.getReaction().isPresent()) {
this.reaction = new JsonReaction(dataMessage.getReaction().get(), m);
}
if (dataMessage.getQuote().isPresent()) {
this.quote = new JsonQuote(dataMessage.getQuote().get(), m);
}
this.viewOnce = dataMessage.isViewOnce();
this.reaction = dataMessage.getReaction().isPresent()
? new JsonReaction(dataMessage.getReaction().get(), m)
: null;
this.quote = dataMessage.getQuote().isPresent() ? new JsonQuote(dataMessage.getQuote().get(), m) : null;
if (dataMessage.getMentions().isPresent()) {
this.mentions = dataMessage.getMentions()
.get()
@ -69,6 +114,8 @@ class JsonDataMessage {
} else {
this.mentions = List.of();
}
remoteDelete = dataMessage.getRemoteDelete().isPresent() ? new JsonRemoteDelete(dataMessage.getRemoteDelete()
.get()) : null;
if (dataMessage.getAttachments().isPresent()) {
this.attachments = dataMessage.getAttachments()
.get()
@ -78,6 +125,7 @@ class JsonDataMessage {
} else {
this.attachments = List.of();
}
<<<<<<< HEAD
if (dataMessage.getReaction().isPresent()) {
final SignalServiceDataMessage.Reaction reaction = dataMessage.getReaction().get();
this.reaction = new JsonReaction(reaction);
@ -89,6 +137,20 @@ class JsonDataMessage {
this.emoji = "";
this.targetAuthor = "";
this.targetTimestamp = 0;
=======
this.sticker = dataMessage.getSticker().isPresent() ? new JsonSticker(dataMessage.getSticker().get()) : null;
if (dataMessage.getSharedContacts().isPresent()) {
this.contacts = dataMessage.getSharedContacts()
.get()
.stream()
.map(JsonSharedContact::new)
.collect(Collectors.toList());
} else {
this.contacts = List.of();
}
}
>>>>>>> upstream/master
}
@ -107,10 +169,15 @@ class JsonDataMessage {
public JsonDataMessage(Signal.MessageReceived messageReceived) {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
reaction = null; // TODO Replace these 3 with the proper commands
groupInfo = messageReceived.getGroupId().length > 0 ? new JsonGroupInfo(messageReceived.getGroupId()) : null;
expiresInSeconds = null;
viewOnce = null;
remoteDelete = null;
reaction = null; // TODO Replace these 5 with the proper commands
quote = null;
mentions = null;
sticker = null;
contacts = null;
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
// i don't understand what SyncMessages are so i'm going to ignore them
@ -118,10 +185,15 @@ class JsonDataMessage {
public JsonDataMessage(Signal.SyncMessageReceived messageReceived) {
timestamp = messageReceived.getTimestamp();
message = messageReceived.getMessage();
groupInfo = new JsonGroupInfo(messageReceived.getGroupId());
reaction = null; // TODO Replace these 3 with the proper commands
groupInfo = messageReceived.getGroupId().length > 0 ? new JsonGroupInfo(messageReceived.getGroupId()) : null;
expiresInSeconds = null;
viewOnce = null;
remoteDelete = null;
reaction = null; // TODO Replace these 5 with the proper commands
quote = null;
mentions = null;
sticker = null;
contacts = null;
attachments = messageReceived.getAttachments().stream().map(JsonAttachment::new).collect(Collectors.toList());
}
}

View file

@ -1,8 +1,11 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
public class JsonError {
String message;
@JsonProperty
final String message;
public JsonError(Throwable exception) {
this.message = exception.getMessage();

View file

@ -1,41 +1,59 @@
package org.asamk.signal.json;
import org.asamk.signal.manager.GroupUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.manager.groups.GroupUtils;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.util.Base64;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
class JsonGroupInfo {
String groupId;
List<String> members;
String name;
String type;
@JsonProperty
final String groupId;
@JsonProperty
final String type;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final String name;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<String> members;
JsonGroupInfo(SignalServiceGroup groupInfo) {
this.groupId = Base64.encodeBytes(groupInfo.getGroupId());
if (groupInfo.getMembers().isPresent()) {
this.members = new ArrayList<>(groupInfo.getMembers().get().size());
for (SignalServiceAddress address : groupInfo.getMembers().get()) {
this.members.add(address.getLegacyIdentifier());
}
}
if (groupInfo.getName().isPresent()) {
this.name = groupInfo.getName().get();
}
this.groupId = Base64.getEncoder().encodeToString(groupInfo.getGroupId());
this.type = groupInfo.getType().toString();
this.name = groupInfo.getName().orNull();
if (groupInfo.getMembers().isPresent()) {
this.members = groupInfo.getMembers()
.get()
.stream()
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toList());
} else {
this.members = null;
}
}
JsonGroupInfo(SignalServiceGroupV2 groupInfo) {
this.groupId = GroupUtils.getGroupIdV2(groupInfo.getMasterKey()).toBase64();
this.type = groupInfo.hasSignedGroupChange() ? "UPDATE" : "DELIVER";
this.members = null;
this.name = null;
}
JsonGroupInfo(byte[] groupId) {
this.groupId = Base64.encodeBytes(groupId);
this.groupId = Base64.getEncoder().encodeToString(groupId);
this.type = "DELIVER";
this.members = null;
this.name = null;
}
}

View file

@ -1,14 +1,21 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class JsonMention {
String name;
int start;
int length;
@JsonProperty
final String name;
@JsonProperty
final int start;
@JsonProperty
final int length;
JsonMention(SignalServiceDataMessage.Mention mention, Manager m) {
this.name = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null))

View file

@ -1,15 +1,18 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.Signal;
//import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.List;
public class JsonMessageEnvelope {
<<<<<<< HEAD
String source;
int sourceDevice;
String relay;
@ -19,18 +22,66 @@ public class JsonMessageEnvelope {
JsonCallMessage callMessage;
JsonReceiptMessage receiptMessage;
// String typingAction;
=======
@JsonProperty
final String source;
@JsonProperty
final Integer sourceDevice;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final String relay;
@JsonProperty
final long timestamp;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonDataMessage dataMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonSyncMessage syncMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonCallMessage callMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonReceiptMessage receiptMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonTypingMessage typingMessage;
>>>>>>> upstream/master
public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content, Manager m) {
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
SignalServiceAddress source = envelope.getSourceAddress();
var source = envelope.getSourceAddress();
this.source = source.getLegacyIdentifier();
this.relay = source.getRelay().isPresent() ? source.getRelay().get() : null;
this.sourceDevice = envelope.getSourceDevice();
this.relay = source.getRelay().orNull();
} else if (envelope.isUnidentifiedSender() && content != null) {
this.source = content.getSender().getLegacyIdentifier();
this.sourceDevice = content.getSenderDevice();
this.relay = null;
} else {
this.source = null;
this.sourceDevice = null;
this.relay = null;
}
this.sourceDevice = envelope.getSourceDevice();
this.timestamp = envelope.getTimestamp();
if (envelope.isReceipt()) {
this.receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
} else if (content != null && content.getReceiptMessage().isPresent()) {
this.receiptMessage = new JsonReceiptMessage(content.getReceiptMessage().get());
} else {
this.receiptMessage = null;
}
<<<<<<< HEAD
if (content != null) {
if (envelope.isUnidentifiedSender()) {
this.source = content.getSender().getLegacyIdentifier();
@ -53,23 +104,56 @@ public class JsonMessageEnvelope {
this.typingAction = content.getTypingMessage().get();
}
*/ }
=======
this.typingMessage = content != null && content.getTypingMessage().isPresent()
? new JsonTypingMessage(content.getTypingMessage().get())
: null;
this.dataMessage = content != null && content.getDataMessage().isPresent()
? new JsonDataMessage(content.getDataMessage().get(), m)
: null;
this.syncMessage = content != null && content.getSyncMessage().isPresent()
? new JsonSyncMessage(content.getSyncMessage().get(), m)
: null;
this.callMessage = content != null && content.getCallMessage().isPresent()
? new JsonCallMessage(content.getCallMessage().get())
: null;
>>>>>>> upstream/master
}
public JsonMessageEnvelope(Signal.MessageReceived messageReceived) {
source = messageReceived.getSender();
sourceDevice = null;
relay = null;
timestamp = messageReceived.getTimestamp();
receiptMessage = null;
dataMessage = new JsonDataMessage(messageReceived);
syncMessage = null;
callMessage = null;
typingMessage = null;
}
public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) {
source = receiptReceived.getSender();
sourceDevice = null;
relay = null;
timestamp = receiptReceived.getTimestamp();
receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp));
dataMessage = null;
syncMessage = null;
callMessage = null;
typingMessage = null;
}
public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) {
source = messageReceived.getSource();
sourceDevice = null;
relay = null;
timestamp = messageReceived.getTimestamp();
receiptMessage = null;
dataMessage = null;
syncMessage = new JsonSyncMessage(messageReceived);
callMessage = null;
typingMessage = null;
}
}

View file

@ -1,5 +1,8 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@ -9,12 +12,21 @@ import java.util.stream.Collectors;
public class JsonQuote {
long id;
String author;
String text;
@JsonProperty
final long id;
List<JsonMention> mentions;
List<JsonQuotedAttachment> attachments;
@JsonProperty
final String author;
@JsonProperty
final String text;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonMention> mentions;
@JsonProperty
final List<JsonQuotedAttachment> attachments;
JsonQuote(SignalServiceDataMessage.Quote quote, Manager m) {
this.id = quote.getId();
@ -26,6 +38,8 @@ public class JsonQuote {
.stream()
.map(quotedMention -> new JsonMention(quotedMention, m))
.collect(Collectors.toList());
} else {
this.mentions = null;
}
if (quote.getAttachments().size() > 0) {

View file

@ -1,12 +1,21 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
public class JsonQuotedAttachment {
String contentType;
String filename;
JsonAttachment thumbnail;
@JsonProperty
final String contentType;
@JsonProperty
final String filename;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonAttachment thumbnail;
JsonQuotedAttachment(SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment) {
contentType = quotedAttachment.getContentType();

View file

@ -1,14 +1,23 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction;
public class JsonReaction {
String emoji;
String targetAuthor;
long targetSentTimestamp;
boolean isRemove;
@JsonProperty
final String emoji;
@JsonProperty
final String targetAuthor;
@JsonProperty
final long targetSentTimestamp;
@JsonProperty
final boolean isRemove;
JsonReaction(Reaction reaction, Manager m) {
this.emoji = reaction.getEmoji();

View file

@ -1,25 +1,29 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
import java.util.List;
class JsonReceiptMessage {
long when;
boolean isDelivery;
boolean isRead;
List<Long> timestamps;
@JsonProperty
final long when;
@JsonProperty
final boolean isDelivery;
@JsonProperty
final boolean isRead;
@JsonProperty
final List<Long> timestamps;
JsonReceiptMessage(SignalServiceReceiptMessage receiptMessage) {
this.when = receiptMessage.getWhen();
if (receiptMessage.isDeliveryReceipt()) {
this.isDelivery = true;
}
if (receiptMessage.isReadReceipt()) {
this.isRead = true;
}
this.isDelivery = receiptMessage.isDeliveryReceipt();
this.isRead = receiptMessage.isReadReceipt();
this.timestamps = receiptMessage.getTimestamps();
}

View file

@ -0,0 +1,15 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
class JsonRemoteDelete {
@JsonProperty
final long timestamp;
JsonRemoteDelete(SignalServiceDataMessage.RemoteDelete remoteDelete) {
this.timestamp = remoteDelete.getTargetSentTimestamp();
}
}

View file

@ -0,0 +1,62 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import java.util.List;
import java.util.stream.Collectors;
public class JsonSharedContact {
@JsonProperty
final JsonContactName name;
@JsonProperty
final JsonContactAvatar avatar;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonContactPhone> phone;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonContactEmail> email;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonContactAddress> address;
@JsonProperty
final String organization;
public JsonSharedContact(SharedContact contact) {
name = new JsonContactName(contact.getName());
if (contact.getAvatar().isPresent()) {
avatar = new JsonContactAvatar(contact.getAvatar().get());
} else {
avatar = null;
}
if (contact.getPhone().isPresent()) {
phone = contact.getPhone().get().stream().map(JsonContactPhone::new).collect(Collectors.toList());
} else {
phone = null;
}
if (contact.getEmail().isPresent()) {
email = contact.getEmail().get().stream().map(JsonContactEmail::new).collect(Collectors.toList());
} else {
email = null;
}
if (contact.getAddress().isPresent()) {
address = contact.getAddress().get().stream().map(JsonContactAddress::new).collect(Collectors.toList());
} else {
address = null;
}
organization = contact.getOrganization().orNull();
}
}

View file

@ -0,0 +1,25 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import java.util.Base64;
public class JsonSticker {
@JsonProperty
final String packId;
@JsonProperty
final String packKey;
@JsonProperty
final int stickerId;
public JsonSticker(SignalServiceDataMessage.Sticker sticker) {
this.packId = Base64.getEncoder().encodeToString(sticker.getPackId());
this.packKey = Base64.getEncoder().encodeToString(sticker.getPackKey());
this.stickerId = sticker.getStickerId();
}
}

View file

@ -1,18 +1,23 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.Signal;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
class JsonSyncDataMessage extends JsonDataMessage {
String destination;
@JsonProperty
final String destination;
JsonSyncDataMessage(SentTranscriptMessage transcriptMessage, Manager m) {
super(transcriptMessage.getMessage(), m);
if (transcriptMessage.getDestination().isPresent()) {
this.destination = transcriptMessage.getDestination().get().getLegacyIdentifier();
}
this.destination = transcriptMessage.getDestination()
.transform(SignalServiceAddress::getLegacyIdentifier)
.orNull();
}
JsonSyncDataMessage(Signal.SyncMessageReceived messageReceived) {

View file

@ -1,13 +1,16 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.Signal;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
enum JsonSyncMessageType {
CONTACTS_SYNC,
@ -17,23 +20,57 @@ enum JsonSyncMessageType {
class JsonSyncMessage {
JsonSyncDataMessage sentMessage;
List<String> blockedNumbers;
List<ReadMessage> readMessages;
JsonSyncMessageType type;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonSyncDataMessage sentMessage;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<String> blockedNumbers;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<String> blockedGroupIds;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final List<JsonSyncReadMessage> readMessages;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final JsonSyncMessageType type;
JsonSyncMessage(SignalServiceSyncMessage syncMessage, Manager m) {
if (syncMessage.getSent().isPresent()) {
this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get(), m);
}
this.sentMessage = syncMessage.getSent().isPresent()
? new JsonSyncDataMessage(syncMessage.getSent().get(), m)
: null;
if (syncMessage.getBlockedList().isPresent()) {
this.blockedNumbers = new ArrayList<>(syncMessage.getBlockedList().get().getAddresses().size());
for (SignalServiceAddress address : syncMessage.getBlockedList().get().getAddresses()) {
this.blockedNumbers.add(address.getLegacyIdentifier());
}
final var base64 = Base64.getEncoder();
this.blockedNumbers = syncMessage.getBlockedList()
.get()
.getAddresses()
.stream()
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toList());
this.blockedGroupIds = syncMessage.getBlockedList()
.get()
.getGroupIds()
.stream()
.map(base64::encodeToString)
.collect(Collectors.toList());
} else {
this.blockedNumbers = null;
this.blockedGroupIds = null;
}
if (syncMessage.getRead().isPresent()) {
this.readMessages = syncMessage.getRead().get();
this.readMessages = syncMessage.getRead()
.get()
.stream()
.map(message -> new JsonSyncReadMessage(message.getSender().getLegacyIdentifier(),
message.getTimestamp()))
.collect(Collectors.toList());
} else {
this.readMessages = null;
}
if (syncMessage.getContacts().isPresent()) {
@ -42,10 +79,16 @@ class JsonSyncMessage {
this.type = JsonSyncMessageType.GROUPS_SYNC;
} else if (syncMessage.getRequest().isPresent()) {
this.type = JsonSyncMessageType.REQUEST_SYNC;
} else {
this.type = null;
}
}
JsonSyncMessage(Signal.SyncMessageReceived messageReceived) {
sentMessage = new JsonSyncDataMessage(messageReceived);
this.sentMessage = new JsonSyncDataMessage(messageReceived);
this.blockedNumbers = null;
this.blockedGroupIds = null;
this.readMessages = null;
this.type = null;
}
}

View file

@ -0,0 +1,17 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonProperty;
class JsonSyncReadMessage {
@JsonProperty
final String sender;
@JsonProperty
final long timestamp;
public JsonSyncReadMessage(final String sender, final long timestamp) {
this.sender = sender;
this.timestamp = timestamp;
}
}

View file

@ -0,0 +1,28 @@
package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import java.util.Base64;
class JsonTypingMessage {
@JsonProperty
final String action;
@JsonProperty
final long timestamp;
@JsonProperty
@JsonInclude(JsonInclude.Include.NON_NULL)
final String groupId;
JsonTypingMessage(SignalServiceTypingMessage typingMessage) {
this.action = typingMessage.getAction().name();
this.timestamp = typingMessage.getTimestamp();
final var encoder = Base64.getEncoder();
this.groupId = typingMessage.getGroupId().transform(encoder::encodeToString).orNull();
}
}

View file

@ -13,8 +13,8 @@ public class DateUtils {
}
public static String formatTimestamp(long timestamp) {
Date date = new Date(timestamp);
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); // Quoted "Z" to indicate UTC, no timezone offset
var date = new Date(timestamp);
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"); // Quoted "Z" to indicate UTC, no timezone offset
df.setTimeZone(tzUTC);
return timestamp + " (" + df.format(date) + ")";
}

View file

@ -1,83 +1,68 @@
package org.asamk.signal.util;
import org.asamk.signal.manager.GroupIdFormatException;
import org.asamk.signal.manager.GroupNotFoundException;
import org.asamk.signal.manager.NotAGroupMemberException;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ErrorUtils {
private final static Logger logger = LoggerFactory.getLogger(ErrorUtils.class);
private ErrorUtils() {
}
public static void handleAssertionError(AssertionError e) {
System.err.println("Failed to send/receive message (Assertion): " + e.getMessage());
e.printStackTrace();
System.err.println(
"If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
logger.warn("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
}
public static int handleTimestampAndSendMessageResults(long timestamp, List<SendMessageResult> results) {
public static void handleTimestampAndSendMessageResults(
PlainTextWriter writer, long timestamp, List<SendMessageResult> results
) throws CommandException {
if (timestamp != 0) {
System.out.println(timestamp);
writer.println("{}", timestamp);
}
List<String> errors = getErrorMessagesFromSendMessageResults(results);
return handleSendMessageResultErrors(errors);
var errors = getErrorMessagesFromSendMessageResults(results);
handleSendMessageResultErrors(errors);
}
public static List<String> getErrorMessagesFromSendMessageResults(List<SendMessageResult> results) {
List<String> errors = new ArrayList<>();
for (SendMessageResult result : results) {
if (result.isNetworkFailure()) {
errors.add(String.format("Network failure for \"%s\"", result.getAddress().getLegacyIdentifier()));
} else if (result.isUnregisteredFailure()) {
errors.add(String.format("Unregistered user \"%s\"", result.getAddress().getLegacyIdentifier()));
} else if (result.getIdentityFailure() != null) {
errors.add(String.format("Untrusted Identity for \"%s\"", result.getAddress().getLegacyIdentifier()));
var errors = new ArrayList<String>();
for (var result : results) {
var error = getErrorMessageFromSendMessageResult(result);
if (error != null) {
errors.add(error);
}
}
return errors;
}
private static int handleSendMessageResultErrors(List<String> errors) {
public static String getErrorMessageFromSendMessageResult(SendMessageResult result) {
if (result.isNetworkFailure()) {
return String.format("Network failure for \"%s\"", result.getAddress().getLegacyIdentifier());
} else if (result.isUnregisteredFailure()) {
return String.format("Unregistered user \"%s\"", result.getAddress().getLegacyIdentifier());
} else if (result.getIdentityFailure() != null) {
return String.format("Untrusted Identity for \"%s\"", result.getAddress().getLegacyIdentifier());
}
return null;
}
private static void handleSendMessageResultErrors(List<String> errors) throws CommandException {
if (errors.size() == 0) {
return 0;
return;
}
System.err.println("Failed to send (some) messages:");
for (String error : errors) {
System.err.println(error);
var message = new StringBuilder();
message.append("Failed to send (some) messages:\n");
for (var error : errors) {
message.append(error).append("\n");
}
return 3;
}
public static void handleIOException(IOException e) {
System.err.println("Failed to send message: " + e.getMessage());
}
public static void handleGroupNotFoundException(GroupNotFoundException e) {
System.err.println("Failed to send to group: " + e.getMessage());
System.err.println("Aborting sending.");
}
public static void handleNotAGroupMemberException(NotAGroupMemberException e) {
System.err.println("Failed to send to group: " + e.getMessage());
System.err.println("Update the group on another device to readd the user to this group.");
System.err.println("Aborting sending.");
}
public static void handleGroupIdFormatException(GroupIdFormatException e) {
System.err.println(e.getMessage());
System.err.println("Aborting sending.");
}
public static void handleInvalidNumberException(InvalidNumberException e) {
System.err.println("Failed to parse recipient: " + e.getMessage());
System.err.println("Aborting sending.");
throw new IOErrorException(message.toString());
}
}

View file

@ -8,8 +8,8 @@ public class Hex {
}
public static String toString(byte[] bytes) {
StringBuffer buf = new StringBuffer();
for (final byte aByte : bytes) {
var buf = new StringBuffer();
for (final var aByte : bytes) {
appendHexChar(buf, aByte);
buf.append(" ");
}
@ -17,8 +17,8 @@ public class Hex {
}
public static String toStringCondensed(byte[] bytes) {
StringBuffer buf = new StringBuffer();
for (final byte aByte : bytes) {
var buf = new StringBuffer();
for (final var aByte : bytes) {
appendHexChar(buf, aByte);
}
return buf.toString();
@ -30,9 +30,9 @@ public class Hex {
}
public static byte[] toByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
var len = s.length();
var data = new byte[len / 2];
for (var i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;

View file

@ -1,38 +1,19 @@
package org.asamk.signal.util;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.EnumSet;
import java.util.Set;
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
public class IOUtils {
private IOUtils() {
}
public static File createTempFile() throws IOException {
return File.createTempFile("signal_tmp_", ".tmp");
}
public static String readAll(InputStream in, Charset charset) throws IOException {
StringWriter output = new StringWriter();
byte[] buffer = new byte[4096];
var output = new StringWriter();
var buffer = new byte[4096];
int n;
while (-1 != (n = in.read(buffer))) {
output.write(new String(buffer, 0, n, charset));
@ -40,57 +21,12 @@ public class IOUtils {
return output.toString();
}
public static byte[] readFully(InputStream in) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Util.copy(in, baos);
return baos.toByteArray();
}
public static void createPrivateDirectories(File file) throws IOException {
if (file.exists()) {
return;
}
final Path path = file.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms));
} catch (UnsupportedOperationException e) {
Files.createDirectories(path);
}
}
public static void createPrivateFile(File path) throws IOException {
final Path file = path.toPath();
try {
Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
Files.createFile(file, PosixFilePermissions.asFileAttribute(perms));
} catch (UnsupportedOperationException e) {
Files.createFile(file);
}
}
public static File getDataHomeDir() {
String dataHome = System.getenv("XDG_DATA_HOME");
var dataHome = System.getenv("XDG_DATA_HOME");
if (dataHome != null) {
return new File(dataHome);
}
return new File(new File(System.getProperty("user.home"), ".local"), "share");
}
public static void copyStreamToFile(InputStream input, File outputFile) throws IOException {
copyStreamToFile(input, outputFile, 8192);
}
public static void copyStreamToFile(InputStream input, File outputFile, int bufferSize) throws IOException {
try (OutputStream output = new FileOutputStream(outputFile)) {
byte[] buffer = new byte[bufferSize];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
}
}
}

View file

@ -6,7 +6,7 @@ import java.security.SecureRandom;
public class RandomUtils {
private static final ThreadLocal<SecureRandom> LOCAL_RANDOM = ThreadLocal.withInitial(() -> {
SecureRandom rand = getSecureRandomUnseeded();
var rand = getSecureRandomUnseeded();
// Let the SecureRandom seed it self initially
rand.nextBoolean();

View file

@ -1,66 +1,33 @@
package org.asamk.signal.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.asamk.signal.manager.GroupId;
import org.asamk.signal.manager.GroupIdFormatException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.InvalidObjectException;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupIdFormatException;
import org.whispersystems.libsignal.util.guava.Optional;
public class Util {
private Util() {
}
public static String getStringIfNotBlank(Optional<String> value) {
var string = value.orNull();
if (string == null || string.isBlank()) {
return null;
}
return string;
}
public static String formatSafetyNumber(String digits) {
final int partCount = 12;
int partSize = digits.length() / partCount;
StringBuilder f = new StringBuilder(digits.length() + partCount);
for (int i = 0; i < partCount; i++) {
final var partCount = 12;
var partSize = digits.length() / partCount;
var f = new StringBuilder(digits.length() + partCount);
for (var i = 0; i < partCount; i++) {
f.append(digits, i * partSize, (i * partSize) + partSize).append(" ");
}
return f.toString();
}
public static String join(CharSequence separator, Iterable<? extends CharSequence> list) {
StringBuilder buf = new StringBuilder();
for (CharSequence str : list) {
if (buf.length() > 0) {
buf.append(separator);
}
buf.append(str);
}
return buf.toString();
}
public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
JsonNode node = parent.get(name);
if (node == null) {
throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ",
name));
}
return node;
}
public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException {
return GroupId.fromBase64(groupId);
}
public static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException {
return PhoneNumberFormatter.formatNumber(number, localNumber);
}
public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) {
if (UuidUtil.isUuid(identifier)) {
return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null);
} else {
return new SignalServiceAddress(null, identifier);
}
}
}