Implement configuration handling

Closes #747
This commit is contained in:
AsamK 2021-09-29 19:38:31 +02:00
parent c9f5550d18
commit 6f5e72119e
11 changed files with 274 additions and 3 deletions

View file

@ -98,6 +98,13 @@ public interface Manager extends Closeable {
void updateAccountAttributes(String deviceName) throws IOException;
void updateConfiguration(
final Boolean readReceipts,
final Boolean unidentifiedDeliveryIndicators,
final Boolean typingIndicators,
final Boolean linkPreviews
) throws IOException, NotMasterDeviceException;
void setProfile(
String givenName, String familyName, String about, String aboutEmoji, Optional<File> avatar
) throws IOException;

View file

@ -320,6 +320,31 @@ public class ManagerImpl implements Manager {
account.isDiscoverableByPhoneNumber());
}
@Override
public void updateConfiguration(
final Boolean readReceipts,
final Boolean unidentifiedDeliveryIndicators,
final Boolean typingIndicators,
final Boolean linkPreviews
) throws IOException, NotMasterDeviceException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
if (readReceipts != null) {
account.getConfigurationStore().setReadReceipts(readReceipts);
}
if (unidentifiedDeliveryIndicators != null) {
account.getConfigurationStore().setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators);
}
if (typingIndicators != null) {
account.getConfigurationStore().setTypingIndicators(typingIndicators);
}
if (linkPreviews != null) {
account.getConfigurationStore().setLinkPreviews(linkPreviews);
}
syncHelper.sendConfigurationMessage();
}
/**
* @param givenName if null, the previous givenName will be kept
* @param familyName if null, the previous familyName will be kept

View file

@ -0,0 +1,20 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.jobs.Context;
public class SendSyncConfigurationAction implements HandleAction {
private static final SendSyncConfigurationAction INSTANCE = new SendSyncConfigurationAction();
private SendSyncConfigurationAction() {
}
public static SendSyncConfigurationAction create() {
return INSTANCE;
}
@Override
public void execute(Context context) throws Throwable {
context.getSyncHelper().sendConfigurationMessage();
}
}

View file

@ -0,0 +1,93 @@
package org.asamk.signal.manager.configuration;
public class ConfigurationStore {
private final Saver saver;
private Boolean readReceipts;
private Boolean unidentifiedDeliveryIndicators;
private Boolean typingIndicators;
private Boolean linkPreviews;
public ConfigurationStore(final Saver saver) {
this.saver = saver;
}
public static ConfigurationStore fromStorage(Storage storage, Saver saver) {
final var store = new ConfigurationStore(saver);
store.readReceipts = storage.readReceipts;
store.unidentifiedDeliveryIndicators = storage.unidentifiedDeliveryIndicators;
store.typingIndicators = storage.typingIndicators;
store.linkPreviews = storage.linkPreviews;
return store;
}
public Boolean getReadReceipts() {
return readReceipts;
}
public void setReadReceipts(final boolean readReceipts) {
this.readReceipts = readReceipts;
saver.save(toStorage());
}
public Boolean getUnidentifiedDeliveryIndicators() {
return unidentifiedDeliveryIndicators;
}
public void setUnidentifiedDeliveryIndicators(final boolean unidentifiedDeliveryIndicators) {
this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
saver.save(toStorage());
}
public Boolean getTypingIndicators() {
return typingIndicators;
}
public void setTypingIndicators(final boolean typingIndicators) {
this.typingIndicators = typingIndicators;
saver.save(toStorage());
}
public Boolean getLinkPreviews() {
return linkPreviews;
}
public void setLinkPreviews(final boolean linkPreviews) {
this.linkPreviews = linkPreviews;
saver.save(toStorage());
}
private Storage toStorage() {
return new Storage(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews);
}
public static final class Storage {
public Boolean readReceipts;
public Boolean unidentifiedDeliveryIndicators;
public Boolean typingIndicators;
public Boolean linkPreviews;
// For deserialization
private Storage() {
}
public Storage(
final Boolean readReceipts,
final Boolean unidentifiedDeliveryIndicators,
final Boolean typingIndicators,
final Boolean linkPreviews
) {
this.readReceipts = readReceipts;
this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
this.typingIndicators = typingIndicators;
this.linkPreviews = linkPreviews;
}
}
public interface Saver {
void save(Storage storage);
}
}

View file

@ -15,6 +15,7 @@ import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
import org.asamk.signal.manager.actions.SendReceiptAction;
import org.asamk.signal.manager.actions.SendRetryMessageRequestAction;
import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
import org.asamk.signal.manager.actions.SendSyncConfigurationAction;
import org.asamk.signal.manager.actions.SendSyncContactsAction;
import org.asamk.signal.manager.actions.SendSyncGroupsAction;
import org.asamk.signal.manager.actions.SendSyncKeysAction;
@ -271,7 +272,9 @@ public final class IncomingMessageHandler {
if (rm.isKeysRequest()) {
actions.add(SendSyncKeysAction.create());
}
// TODO Handle rm.isConfigurationRequest();
if (rm.isConfigurationRequest()) {
actions.add(SendSyncConfigurationAction.create());
}
}
if (syncMessage.getGroups().isPresent()) {
logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring.");
@ -353,7 +356,13 @@ public final class IncomingMessageHandler {
}
}
if (syncMessage.getConfiguration().isPresent()) {
// TODO
final var configurationMessage = syncMessage.getConfiguration().get();
account.getConfigurationStore().setReadReceipts(configurationMessage.getReadReceipts().orNull());
account.getConfigurationStore().setLinkPreviews(configurationMessage.getLinkPreviews().orNull());
account.getConfigurationStore().setTypingIndicators(configurationMessage.getTypingIndicators().orNull());
account.getConfigurationStore()
.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators()
.orNull());
}
return actions;
}

View file

@ -14,6 +14,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
@ -221,6 +222,15 @@ public class SyncHelper {
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
public void sendConfigurationMessage() throws IOException {
final var config = account.getConfigurationStore();
var configurationMessage = new ConfigurationMessage(Optional.fromNullable(config.getReadReceipts()),
Optional.fromNullable(config.getUnidentifiedDeliveryIndicators()),
Optional.fromNullable(config.getTypingIndicators()),
Optional.fromNullable(config.getLinkPreviews()));
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
}
public void handleSyncDeviceContacts(final InputStream input) throws IOException {
final var s = new DeviceContactsInputStream(input);
DeviceContact c;

View file

@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.configuration.ConfigurationStore;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
@ -103,6 +104,8 @@ public class SignalAccount implements Closeable {
private RecipientStore recipientStore;
private StickerStore stickerStore;
private StickerStore.Storage stickerStoreStorage;
private ConfigurationStore configurationStore;
private ConfigurationStore.Storage configurationStoreStorage;
private MessageCache messageCache;
@ -159,6 +162,7 @@ public class SignalAccount implements Closeable {
account.recipientStore,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore);
account.configurationStore = new ConfigurationStore(account::saveConfigurationStore);
account.registered = false;
@ -267,6 +271,7 @@ public class SignalAccount implements Closeable {
account.recipientStore,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore);
account.configurationStore = new ConfigurationStore(account::saveConfigurationStore);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.migrateLegacyConfigs();
@ -491,6 +496,15 @@ public class SignalAccount implements Closeable {
stickerStore = new StickerStore(this::saveStickerStore);
}
if (rootNode.hasNonNull("configurationStore")) {
configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
ConfigurationStore.Storage.class);
configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage,
this::saveConfigurationStore);
} else {
configurationStore = new ConfigurationStore(this::saveConfigurationStore);
}
migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
if (migratedLegacyConfig) {
@ -677,6 +691,11 @@ public class SignalAccount implements Closeable {
save();
}
private void saveConfigurationStore(ConfigurationStore.Storage storage) {
this.configurationStoreStorage = storage;
save();
}
private void save() {
synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode();
@ -707,7 +726,8 @@ public class SignalAccount implements Closeable {
profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
.putPOJO("groupStore", groupStoreStorage)
.putPOJO("stickerStore", stickerStoreStorage);
.putPOJO("stickerStore", stickerStoreStorage)
.putPOJO("configurationStore", configurationStoreStorage);
try {
try (var output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
@ -797,6 +817,10 @@ public class SignalAccount implements Closeable {
return senderKeyStore;
}
public ConfigurationStore getConfigurationStore() {
return configurationStore;
}
public MessageCache getMessageCache() {
return messageCache;
}

View file

@ -113,6 +113,23 @@ Can fix problems with receiving messages.
*-n* NAME, *--device-name* NAME::
Set a new device name for the main or linked device
=== updateConfiguration
Update signal configs and sync them to linked devices.
This command only works on the main devices.
*--read-receipts* {true,false}::
Indicates if Signal should send read receipts.
*--unidentified-delivery-indicators* {true,false}::
Indicates if Signal should show unidentified delivery indicators.
*--typing-indicators* {true,false}::
Indicates if Signal should send/show typing indicators.
*--link-previews* {true,false}::
Indicates if Signal should generate link previews.
=== setPin
Set a registration lock pin, to prevent others from registering this number.

View file

@ -39,6 +39,7 @@ public class Commands {
addCommand(new UnblockCommand());
addCommand(new UnregisterCommand());
addCommand(new UpdateAccountCommand());
addCommand(new UpdateConfigurationCommand());
addCommand(new UpdateContactCommand());
addCommand(new UpdateGroupCommand());
addCommand(new UpdateProfileCommand());

View file

@ -0,0 +1,55 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.OutputWriter;
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.NotMasterDeviceException;
import java.io.IOException;
public class UpdateConfigurationCommand implements JsonRpcLocalCommand {
@Override
public String getName() {
return "updateConfiguration";
}
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.help("Update signal configs and sync them to linked devices.");
subparser.addArgument("--read-receipts")
.type(Boolean.class)
.help("Indicates if Signal should send read receipts.");
subparser.addArgument("--unidentified-delivery-indicators")
.type(Boolean.class)
.help("Indicates if Signal should show unidentified delivery indicators.");
subparser.addArgument("--typing-indicators")
.type(Boolean.class)
.help("Indicates if Signal should send/show typing indicators.");
subparser.addArgument("--link-previews")
.type(Boolean.class)
.help("Indicates if Signal should generate link previews.");
}
@Override
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var readReceipts = ns.getBoolean("read-receipts");
final var unidentifiedDeliveryIndicators = ns.getBoolean("unidentified-delivery-indicators");
final var typingIndicators = ns.getBoolean("typing-indicators");
final var linkPreviews = ns.getBoolean("link-previews");
try {
m.updateConfiguration(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews);
} catch (IOException e) {
throw new IOErrorException("UpdateAccount error: " + e.getMessage(), e);
} catch (NotMasterDeviceException e) {
throw new UserErrorException("This command doesn't work on linked devices.");
}
}
}

View file

@ -93,6 +93,16 @@ public class DbusManagerImpl implements Manager {
}
}
@Override
public void updateConfiguration(
final Boolean readReceipts,
final Boolean unidentifiedDeliveryIndicators,
final Boolean typingIndicators,
final Boolean linkPreviews
) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void setProfile(
final String givenName,