mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-10-29 09:19:29 +00:00
Expose history-aware device linking over daemon JSON-RPC
This commit is contained in:
parent
6971d29f60
commit
f6a5640b5d
@ -18,6 +18,13 @@ public interface MultiAccountManager extends AutoCloseable {
|
||||
|
||||
URI getNewProvisioningDeviceLinkUri() throws TimeoutException, IOException;
|
||||
|
||||
default URI getNewProvisioningDeviceLinkUri(final boolean importHistory) throws TimeoutException, IOException {
|
||||
if (importHistory) {
|
||||
throw new UnsupportedOperationException("History transfer is not supported by this manager backend");
|
||||
}
|
||||
return getNewProvisioningDeviceLinkUri();
|
||||
}
|
||||
|
||||
ProvisioningManager getProvisioningManagerFor(URI deviceLinkUri);
|
||||
|
||||
RegistrationManager getNewRegistrationManager(String account) throws IOException;
|
||||
|
||||
@ -135,8 +135,13 @@ public class MultiAccountManagerImpl implements MultiAccountManager {
|
||||
|
||||
@Override
|
||||
public URI getNewProvisioningDeviceLinkUri() throws TimeoutException, IOException {
|
||||
return getNewProvisioningDeviceLinkUri(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getNewProvisioningDeviceLinkUri(final boolean importHistory) throws TimeoutException, IOException {
|
||||
final var provisioningManager = getNewProvisioningManager();
|
||||
final var deviceLinkUri = provisioningManager.getDeviceLinkUri();
|
||||
final var deviceLinkUri = provisioningManager.getDeviceLinkUri(importHistory);
|
||||
provisioningManagers.put(deviceLinkUri, provisioningManager);
|
||||
return deviceLinkUri;
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package org.asamk.signal.commands;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import net.sourceforge.argparse4j.impl.Arguments;
|
||||
import net.sourceforge.argparse4j.inf.Namespace;
|
||||
import net.sourceforge.argparse4j.inf.Subparser;
|
||||
@ -7,6 +9,7 @@ import net.sourceforge.argparse4j.inf.Subparser;
|
||||
import org.asamk.signal.OutputType;
|
||||
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.json.JsonReceiveMessageHandler;
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.output.JsonWriter;
|
||||
@ -15,10 +18,11 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.SequencedCollection;
|
||||
|
||||
public final class ExportHistoryCommand implements LocalCommand {
|
||||
public final class ExportHistoryCommand implements LocalCommand, JsonRpcSingleCommand<ExportHistoryCommand.ExportHistoryParams> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExportHistoryCommand.class);
|
||||
|
||||
@ -35,6 +39,42 @@ public final class ExportHistoryCommand implements LocalCommand {
|
||||
.help("Delete the encrypted transfer archive and key after a successful export.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeReference<ExportHistoryParams> getRequestType() {
|
||||
return new TypeReference<>() {};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(
|
||||
final ExportHistoryParams request,
|
||||
final Manager manager,
|
||||
final JsonWriter jsonWriter
|
||||
) throws CommandException {
|
||||
final var offset = request == null || request.offset() == null ? 0 : request.offset();
|
||||
final var limit = request == null || request.limit() == null ? 100 : request.limit();
|
||||
if (offset < 0 || limit < 1 || limit > 100) {
|
||||
throw new UserErrorException(
|
||||
"History offset must be non-negative and limit must be between 1 and 100");
|
||||
}
|
||||
final var messages = new ArrayList<Object>();
|
||||
final var handler = new JsonReceiveMessageHandler(manager, messages::add);
|
||||
final var seen = new long[] {0};
|
||||
final var pageHandler = (Manager.ReceiveMessageHandler) (envelope, exception) -> {
|
||||
if (seen[0]++ >= offset && messages.size() < limit) {
|
||||
handler.handleMessage(envelope, exception);
|
||||
}
|
||||
};
|
||||
try {
|
||||
final var result = manager.exportHistory(pageHandler, false);
|
||||
jsonWriter.write(new JsonHistoryPage(messages,
|
||||
result.exportedMessages(),
|
||||
result.skippedRecords(),
|
||||
(long) offset + messages.size() < result.exportedMessages()));
|
||||
} catch (IOException e) {
|
||||
throw new IOErrorException("Failed to export linked-device history: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SequencedCollection<OutputType> getSupportedOutputTypes() {
|
||||
return List.of(OutputType.JSON);
|
||||
@ -57,4 +97,8 @@ public final class ExportHistoryCommand implements LocalCommand {
|
||||
throw new IOErrorException("Failed to export linked-device history: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public record ExportHistoryParams(Integer offset, Integer limit) {}
|
||||
|
||||
private record JsonHistoryPage(List<Object> messages, int exportedMessages, int skippedRecords, boolean hasMore) {}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ public class FinishLinkCommand implements JsonRpcMultiCommand<FinishLinkCommand.
|
||||
}
|
||||
final String identifier;
|
||||
try {
|
||||
identifier = provisioningManager.finishDeviceLink(deviceName);
|
||||
identifier = provisioningManager.finishDeviceLink(deviceName, Boolean.TRUE.equals(request.importHistory()));
|
||||
} catch (TimeoutException e) {
|
||||
throw new UserErrorException("Link request timed out, please try again.");
|
||||
} catch (IOException e) {
|
||||
@ -77,7 +77,7 @@ public class FinishLinkCommand implements JsonRpcMultiCommand<FinishLinkCommand.
|
||||
jsonWriter.write(new JsonFinishLink(manager.getSelfNumber(), manager.getSelfACI()));
|
||||
}
|
||||
|
||||
public record FinishLinkParams(String deviceLinkUri, String deviceName) {}
|
||||
public record FinishLinkParams(String deviceLinkUri, String deviceName, Boolean importHistory) {}
|
||||
|
||||
private record JsonFinishLink(String number, String aci) {}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package org.asamk.signal.commands;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import org.asamk.signal.commands.exceptions.CommandException;
|
||||
import org.asamk.signal.commands.exceptions.IOErrorException;
|
||||
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||
@ -12,7 +14,7 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class StartLinkCommand implements JsonRpcMultiCommand<Void> {
|
||||
public class StartLinkCommand implements JsonRpcMultiCommand<StartLinkCommand.StartLinkParams> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StartLinkCommand.class);
|
||||
|
||||
@ -21,15 +23,20 @@ public class StartLinkCommand implements JsonRpcMultiCommand<Void> {
|
||||
return "startLink";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeReference<StartLinkParams> getRequestType() {
|
||||
return new TypeReference<>() {};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(
|
||||
final Void request,
|
||||
final StartLinkParams request,
|
||||
final MultiAccountManager m,
|
||||
final JsonWriter jsonWriter
|
||||
) throws CommandException {
|
||||
final URI deviceLinkUri;
|
||||
try {
|
||||
deviceLinkUri = m.getNewProvisioningDeviceLinkUri();
|
||||
deviceLinkUri = m.getNewProvisioningDeviceLinkUri(request != null && Boolean.TRUE.equals(request.importHistory()));
|
||||
} catch (TimeoutException e) {
|
||||
throw new UserErrorException("Device link creation timed out, please try again.");
|
||||
} catch (IOException e) {
|
||||
@ -39,5 +46,7 @@ public class StartLinkCommand implements JsonRpcMultiCommand<Void> {
|
||||
jsonWriter.write(new JsonLink(deviceLinkUri.toString()));
|
||||
}
|
||||
|
||||
public record StartLinkParams(Boolean importHistory) {}
|
||||
|
||||
private record JsonLink(String deviceLinkUri) {}
|
||||
}
|
||||
|
||||
@ -0,0 +1,101 @@
|
||||
package org.asamk.signal.commands;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.asamk.signal.manager.Manager;
|
||||
import org.asamk.signal.manager.MultiAccountManager;
|
||||
import org.asamk.signal.manager.ProvisioningManager;
|
||||
import org.asamk.signal.manager.api.HistoryExportResult;
|
||||
import org.asamk.signal.output.JsonWriter;
|
||||
import org.asamk.signal.testutil.ManagerMock;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DaemonHistoryAndLinkTest {
|
||||
|
||||
@Test
|
||||
void daemonLinkPassesHistoryFlagToBothProvisioningSteps() throws Exception {
|
||||
final var started = new AtomicBoolean();
|
||||
final var finished = new AtomicBoolean();
|
||||
final var uri = URI.create("sgnl://linkdevice?uuid=test&pub_key=key");
|
||||
final var provisioning = (ProvisioningManager) Proxy.newProxyInstance(
|
||||
ProvisioningManager.class.getClassLoader(), new Class<?>[]{ProvisioningManager.class},
|
||||
(proxy, method, args) -> switch (method.getName()) {
|
||||
case "finishDeviceLink" -> {
|
||||
finished.set(Boolean.TRUE.equals(args[1]));
|
||||
yield "+15551234567";
|
||||
}
|
||||
default -> null;
|
||||
});
|
||||
final var account = ManagerMock.create("+15551234567");
|
||||
final var multi = (MultiAccountManager) Proxy.newProxyInstance(
|
||||
MultiAccountManager.class.getClassLoader(), new Class<?>[]{MultiAccountManager.class},
|
||||
(proxy, method, args) -> switch (method.getName()) {
|
||||
case "getNewProvisioningDeviceLinkUri" -> {
|
||||
started.set(Boolean.TRUE.equals(args[0]));
|
||||
yield uri;
|
||||
}
|
||||
case "getProvisioningManagerFor" -> provisioning;
|
||||
case "getManager" -> account;
|
||||
default -> null;
|
||||
});
|
||||
final var startResult = new AtomicReference<Object>();
|
||||
new StartLinkCommand().handleCommand(new StartLinkCommand.StartLinkParams(true), multi, startResult::set);
|
||||
assertEquals(uri.toString(), new ObjectMapper().valueToTree(startResult.get()).get("deviceLinkUri").asText());
|
||||
final var finishResult = new AtomicReference<Object>();
|
||||
new FinishLinkCommand().handleCommand(
|
||||
new FinishLinkCommand.FinishLinkParams(uri.toString(), "dev", true), multi, finishResult::set);
|
||||
assertTrue(started.get());
|
||||
assertTrue(finished.get());
|
||||
assertEquals("+15551234567", new ObjectMapper().valueToTree(finishResult.get()).get("number").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void daemonHistoryIsPagedAndLeavesArchiveForReplay() throws Exception {
|
||||
final var retained = new AtomicBoolean();
|
||||
final var base = ManagerMock.create("+15551234567");
|
||||
final var manager = (Manager) Proxy.newProxyInstance(Manager.class.getClassLoader(),
|
||||
new Class<?>[]{Manager.class}, (proxy, method, args) -> {
|
||||
if (method.getName().equals("exportHistory")) {
|
||||
retained.set(!Boolean.TRUE.equals(args[1]));
|
||||
final var handler = (Manager.ReceiveMessageHandler) args[0];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
handler.handleMessage(null, null);
|
||||
}
|
||||
return new HistoryExportResult(3, 1);
|
||||
}
|
||||
return method.invoke(base, args);
|
||||
});
|
||||
final var first = exportPage(manager, 0, 2);
|
||||
final var last = exportPage(manager, 2, 2);
|
||||
final var replayed = exportPage(manager, 0, 2);
|
||||
final var exhausted = exportPage(manager, 3, 2);
|
||||
assertTrue(retained.get());
|
||||
assertEquals(2, first.get("messages").size());
|
||||
assertTrue(first.get("hasMore").asBoolean());
|
||||
assertEquals(1, last.get("messages").size());
|
||||
assertFalse(last.get("hasMore").asBoolean());
|
||||
assertEquals(first, replayed);
|
||||
assertEquals(0, exhausted.get("messages").size());
|
||||
assertFalse(exhausted.get("hasMore").asBoolean());
|
||||
assertEquals(3, first.get("exportedMessages").asInt());
|
||||
assertEquals(1, first.get("skippedRecords").asInt());
|
||||
assertFalse(first.get("messages").get(0).has("envelope"));
|
||||
}
|
||||
|
||||
private static JsonNode exportPage(final Manager manager, final int offset, final int limit) throws Exception {
|
||||
final var output = new AtomicReference<Object>();
|
||||
new ExportHistoryCommand().handleCommand(new ExportHistoryCommand.ExportHistoryParams(offset, limit),
|
||||
manager, output::set);
|
||||
return new ObjectMapper().valueToTree(output.get());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user