mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Implement MessageSendLog for resending after encryption error
This commit is contained in:
parent
3491782912
commit
95cc0ae7fd
16 changed files with 960 additions and 80 deletions
|
@ -62,6 +62,36 @@
|
||||||
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
|
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
|
||||||
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]
|
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.Collation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.Function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.Function$Aggregate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.Function$Window"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.ProgressHandler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.core.DB",
|
||||||
|
"methods":[{"name":"throwex","parameterTypes":["int"] }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.core.DB$ProgressObserver"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.core.NativeDB",
|
||||||
|
"fields":[
|
||||||
|
{"name":"colldatalist"},
|
||||||
|
{"name":"pointer"},
|
||||||
|
{"name":"udfdatalist"}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.libsignal.DuplicateMessageException",
|
"name":"org.whispersystems.libsignal.DuplicateMessageException",
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
@ -77,6 +107,10 @@
|
||||||
"name":"org.whispersystems.libsignal.IdentityKeyPair",
|
"name":"org.whispersystems.libsignal.IdentityKeyPair",
|
||||||
"methods":[{"name":"serialize","parameterTypes":[] }]
|
"methods":[{"name":"serialize","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.whispersystems.libsignal.InvalidKeyException",
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.libsignal.InvalidMessageException",
|
"name":"org.whispersystems.libsignal.InvalidMessageException",
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"interfaces":["java.sql.Connection"]}
|
||||||
|
,
|
||||||
{
|
{
|
||||||
"interfaces":["org.asamk.Signal"]}
|
"interfaces":["org.asamk.Signal"]}
|
||||||
,
|
,
|
||||||
|
|
|
@ -15,9 +15,15 @@
|
||||||
{
|
{
|
||||||
"name":"[J"
|
"name":"[J"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"[Lcom.zaxxer.hikari.util.ConcurrentBag$IConcurrentBagEntry;"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"[Ljava.lang.String;"
|
"name":"[Ljava.lang.String;"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"[Ljava.sql.Statement;"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"[Lorg.whispersystems.signalservice.api.groupsv2.TemporalCredential;"
|
"name":"[Lorg.whispersystems.signalservice.api.groupsv2.TemporalCredential;"
|
||||||
},
|
},
|
||||||
|
@ -118,6 +124,48 @@
|
||||||
"name":"com.sun.crypto.provider.TlsPrfGenerator$V12",
|
"name":"com.sun.crypto.provider.TlsPrfGenerator$V12",
|
||||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"com.zaxxer.hikari.HikariConfig",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllPublicMethods":true,
|
||||||
|
"methods":[
|
||||||
|
{"name":"getCatalog","parameterTypes":[] },
|
||||||
|
{"name":"getConnectionInitSql","parameterTypes":[] },
|
||||||
|
{"name":"getConnectionTestQuery","parameterTypes":[] },
|
||||||
|
{"name":"getConnectionTimeout","parameterTypes":[] },
|
||||||
|
{"name":"getDataSource","parameterTypes":[] },
|
||||||
|
{"name":"getDataSourceClassName","parameterTypes":[] },
|
||||||
|
{"name":"getDataSourceJNDI","parameterTypes":[] },
|
||||||
|
{"name":"getDataSourceProperties","parameterTypes":[] },
|
||||||
|
{"name":"getDriverClassName","parameterTypes":[] },
|
||||||
|
{"name":"getExceptionOverrideClassName","parameterTypes":[] },
|
||||||
|
{"name":"getHealthCheckProperties","parameterTypes":[] },
|
||||||
|
{"name":"getHealthCheckRegistry","parameterTypes":[] },
|
||||||
|
{"name":"getIdleTimeout","parameterTypes":[] },
|
||||||
|
{"name":"getInitializationFailTimeout","parameterTypes":[] },
|
||||||
|
{"name":"getJdbcUrl","parameterTypes":[] },
|
||||||
|
{"name":"getKeepaliveTime","parameterTypes":[] },
|
||||||
|
{"name":"getLeakDetectionThreshold","parameterTypes":[] },
|
||||||
|
{"name":"getMaxLifetime","parameterTypes":[] },
|
||||||
|
{"name":"getMaximumPoolSize","parameterTypes":[] },
|
||||||
|
{"name":"getMetricRegistry","parameterTypes":[] },
|
||||||
|
{"name":"getMetricsTrackerFactory","parameterTypes":[] },
|
||||||
|
{"name":"getMinimumIdle","parameterTypes":[] },
|
||||||
|
{"name":"getPassword","parameterTypes":[] },
|
||||||
|
{"name":"getPoolName","parameterTypes":[] },
|
||||||
|
{"name":"getScheduledExecutor","parameterTypes":[] },
|
||||||
|
{"name":"getSchema","parameterTypes":[] },
|
||||||
|
{"name":"getThreadFactory","parameterTypes":[] },
|
||||||
|
{"name":"getTransactionIsolation","parameterTypes":[] },
|
||||||
|
{"name":"getUsername","parameterTypes":[] },
|
||||||
|
{"name":"getValidationTimeout","parameterTypes":[] },
|
||||||
|
{"name":"isAllowPoolSuspension","parameterTypes":[] },
|
||||||
|
{"name":"isAutoCommit","parameterTypes":[] },
|
||||||
|
{"name":"isIsolateInternalQueries","parameterTypes":[] },
|
||||||
|
{"name":"isReadOnly","parameterTypes":[] },
|
||||||
|
{"name":"isRegisterMbeans","parameterTypes":[] }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"int",
|
"name":"int",
|
||||||
"allDeclaredMethods":true,
|
"allDeclaredMethods":true,
|
||||||
|
@ -1607,13 +1655,13 @@
|
||||||
{
|
{
|
||||||
"name":"org.signal.storageservice.protos.groups.GroupJoinInfo",
|
"name":"org.signal.storageservice.protos.groups.GroupJoinInfo",
|
||||||
"fields":[
|
"fields":[
|
||||||
{"name":"addFromInviteLink_"},
|
{"name":"addFromInviteLink_"},
|
||||||
{"name":"avatar_"},
|
{"name":"avatar_"},
|
||||||
{"name":"description_"},
|
{"name":"description_"},
|
||||||
{"name":"memberCount_"},
|
{"name":"memberCount_"},
|
||||||
{"name":"pendingAdminApproval_"},
|
{"name":"pendingAdminApproval_"},
|
||||||
{"name":"publicKey_"},
|
{"name":"publicKey_"},
|
||||||
{"name":"revision_"},
|
{"name":"revision_"},
|
||||||
{"name":"title_"}
|
{"name":"title_"}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1696,14 +1744,14 @@
|
||||||
{
|
{
|
||||||
"name":"org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo",
|
"name":"org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo",
|
||||||
"fields":[
|
"fields":[
|
||||||
{"name":"addFromInviteLink_"},
|
{"name":"addFromInviteLink_"},
|
||||||
{"name":"avatar_"},
|
{"name":"avatar_"},
|
||||||
{"name":"description_"},
|
{"name":"description_"},
|
||||||
{"name":"isAnnouncementGroup_"},
|
{"name":"isAnnouncementGroup_"},
|
||||||
{"name":"memberCount_"},
|
{"name":"memberCount_"},
|
||||||
{"name":"pendingAdminApproval_"},
|
{"name":"pendingAdminApproval_"},
|
||||||
{"name":"publicKey_"},
|
{"name":"publicKey_"},
|
||||||
{"name":"revision_"},
|
{"name":"revision_"},
|
||||||
{"name":"title_"}
|
{"name":"title_"}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -1773,6 +1821,9 @@
|
||||||
"queryAllDeclaredMethods":true,
|
"queryAllDeclaredMethods":true,
|
||||||
"queryAllDeclaredConstructors":true
|
"queryAllDeclaredConstructors":true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.sqlite.JDBC"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.whispersystems.libsignal.state.IdentityKeyStore",
|
"name":"org.whispersystems.libsignal.state.IdentityKeyStore",
|
||||||
"allDeclaredMethods":true
|
"allDeclaredMethods":true
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
{
|
{
|
||||||
"resources":{
|
"resources":{
|
||||||
"includes":[
|
"includes":[
|
||||||
|
{
|
||||||
|
"pattern":"\\QMETA-INF/maven/org.xerial/sqlite-jdbc/pom.properties\\E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern":"\\QMETA-INF/services/java.sql.Driver\\E"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.transport.ITransportProvider\\E"
|
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.transport.ITransportProvider\\E"
|
||||||
},
|
},
|
||||||
|
@ -187,6 +193,12 @@
|
||||||
{
|
{
|
||||||
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
|
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pattern":"\\Qorg/sqlite/native/Linux/x86_64/libsqlitejdbc.so\\E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern":"\\Qsqlite-jdbc.properties\\E"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pattern":"com/google/i18n/phonenumbers/data/.*"
|
"pattern":"com/google/i18n/phonenumbers/data/.*"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ dependencies {
|
||||||
implementation("com.google.protobuf", "protobuf-javalite", "3.11.4")
|
implementation("com.google.protobuf", "protobuf-javalite", "3.11.4")
|
||||||
implementation("org.bouncycastle", "bcprov-jdk15on", "1.70")
|
implementation("org.bouncycastle", "bcprov-jdk15on", "1.70")
|
||||||
implementation("org.slf4j", "slf4j-api", "1.7.32")
|
implementation("org.slf4j", "slf4j-api", "1.7.32")
|
||||||
|
implementation("org.xerial", "sqlite-jdbc", "3.36.0.3")
|
||||||
|
implementation("com.zaxxer", "HikariCP", "5.0.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
|
|
|
@ -67,6 +67,7 @@ public interface Manager extends Closeable {
|
||||||
throw new NotRegisteredException();
|
throw new NotRegisteredException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
account.initDatabase();
|
||||||
final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
|
final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
|
||||||
|
|
||||||
return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
|
return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
|
||||||
|
|
|
@ -571,6 +571,17 @@ public class ManagerImpl implements Manager {
|
||||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||||
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
|
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
|
||||||
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
|
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
|
||||||
|
for (final var recipient : recipients) {
|
||||||
|
if (recipient instanceof RecipientIdentifier.Single r) {
|
||||||
|
try {
|
||||||
|
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
|
||||||
|
account.getMessageSendLogStore().deleteEntryForRecipientNonGroup(targetSentTimestamp, recipientId);
|
||||||
|
} catch (UnregisteredRecipientException ignored) {
|
||||||
|
}
|
||||||
|
} else if (recipient instanceof RecipientIdentifier.Group r) {
|
||||||
|
account.getMessageSendLogStore().deleteEntryForGroup(targetSentTimestamp, r.groupId());
|
||||||
|
}
|
||||||
|
}
|
||||||
return sendMessage(messageBuilder, recipients);
|
return sendMessage(messageBuilder, recipients);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.asamk.signal.manager.actions;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.helper.Context;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ResendMessageAction implements HandleAction {
|
||||||
|
|
||||||
|
private final RecipientId recipientId;
|
||||||
|
private final long timestamp;
|
||||||
|
private final MessageSendLogEntry messageSendLogEntry;
|
||||||
|
|
||||||
|
public ResendMessageAction(
|
||||||
|
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
|
||||||
|
) {
|
||||||
|
this.recipientId = recipientId;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.messageSendLogEntry = messageSendLogEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Context context) throws Throwable {
|
||||||
|
context.getSendHelper().resendMessage(recipientId, timestamp, messageSendLogEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(final Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
final ResendMessageAction that = (ResendMessageAction) o;
|
||||||
|
return timestamp == that.timestamp
|
||||||
|
&& recipientId.equals(that.recipientId)
|
||||||
|
&& messageSendLogEntry.equals(that.messageSendLogEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(recipientId, timestamp, messageSendLogEntry);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import org.asamk.signal.manager.UntrustedIdentityException;
|
||||||
import org.asamk.signal.manager.actions.HandleAction;
|
import org.asamk.signal.manager.actions.HandleAction;
|
||||||
import org.asamk.signal.manager.actions.RefreshPreKeysAction;
|
import org.asamk.signal.manager.actions.RefreshPreKeysAction;
|
||||||
import org.asamk.signal.manager.actions.RenewSessionAction;
|
import org.asamk.signal.manager.actions.RenewSessionAction;
|
||||||
|
import org.asamk.signal.manager.actions.ResendMessageAction;
|
||||||
import org.asamk.signal.manager.actions.RetrieveProfileAction;
|
import org.asamk.signal.manager.actions.RetrieveProfileAction;
|
||||||
import org.asamk.signal.manager.actions.RetrieveStorageDataAction;
|
import org.asamk.signal.manager.actions.RetrieveStorageDataAction;
|
||||||
import org.asamk.signal.manager.actions.SendGroupInfoAction;
|
import org.asamk.signal.manager.actions.SendGroupInfoAction;
|
||||||
|
@ -41,6 +42,7 @@ import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||||
|
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
|
@ -165,6 +167,13 @@ public final class IncomingMessageHandler {
|
||||||
// address/uuid is validated by unidentified sender certificate
|
// address/uuid is validated by unidentified sender certificate
|
||||||
account.getRecipientStore().resolveRecipientTrusted(content.getSender());
|
account.getRecipientStore().resolveRecipientTrusted(content.getSender());
|
||||||
}
|
}
|
||||||
|
if (envelope.isReceipt()) {
|
||||||
|
final var senderPair = getSender(envelope, content);
|
||||||
|
final var sender = senderPair.first();
|
||||||
|
final var senderDeviceId = senderPair.second();
|
||||||
|
account.getMessageSendLogStore().deleteEntryForRecipient(envelope.getTimestamp(), sender, senderDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
if (isMessageBlocked(envelope, content)) {
|
if (isMessageBlocked(envelope, content)) {
|
||||||
logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
|
logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
|
||||||
return List.of();
|
return List.of();
|
||||||
|
@ -198,6 +207,14 @@ public final class IncomingMessageHandler {
|
||||||
final var sender = senderPair.first();
|
final var sender = senderPair.first();
|
||||||
final var senderDeviceId = senderPair.second();
|
final var senderDeviceId = senderPair.second();
|
||||||
|
|
||||||
|
if (content.getReceiptMessage().isPresent()) {
|
||||||
|
final var message = content.getReceiptMessage().get();
|
||||||
|
if (message.isDeliveryReceipt()) {
|
||||||
|
account.getMessageSendLogStore()
|
||||||
|
.deleteEntriesForRecipient(message.getTimestamps(), sender, senderDeviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (content.getSenderKeyDistributionMessage().isPresent()) {
|
if (content.getSenderKeyDistributionMessage().isPresent()) {
|
||||||
final var message = content.getSenderKeyDistributionMessage().get();
|
final var message = content.getSenderKeyDistributionMessage().get();
|
||||||
final var protocolAddress = new SignalProtocolAddress(context.getRecipientHelper()
|
final var protocolAddress = new SignalProtocolAddress(context.getRecipientHelper()
|
||||||
|
@ -212,15 +229,10 @@ public final class IncomingMessageHandler {
|
||||||
if (content.getDecryptionErrorMessage().isPresent()) {
|
if (content.getDecryptionErrorMessage().isPresent()) {
|
||||||
var message = content.getDecryptionErrorMessage().get();
|
var message = content.getDecryptionErrorMessage().get();
|
||||||
logger.debug("Received a decryption error message (resend request for {})", message.getTimestamp());
|
logger.debug("Received a decryption error message (resend request for {})", message.getTimestamp());
|
||||||
if (message.getRatchetKey().isPresent()) {
|
if (message.getDeviceId() == account.getDeviceId()) {
|
||||||
if (message.getDeviceId() == account.getDeviceId() && account.getSessionStore()
|
handleDecryptionErrorMessage(actions, sender, senderDeviceId, message);
|
||||||
.isCurrentRatchetKey(sender, senderDeviceId, message.getRatchetKey().get())) {
|
|
||||||
logger.debug("Renewing the session with sender");
|
|
||||||
actions.add(new RenewSessionAction(sender));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.debug("Reset shared sender keys with this recipient");
|
logger.debug("Request is for another one of our devices");
|
||||||
account.getSenderKeyStore().deleteSharedWith(sender);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,6 +258,54 @@ public final class IncomingMessageHandler {
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleDecryptionErrorMessage(
|
||||||
|
final List<HandleAction> actions,
|
||||||
|
final RecipientId sender,
|
||||||
|
final int senderDeviceId,
|
||||||
|
final DecryptionErrorMessage message
|
||||||
|
) {
|
||||||
|
final var logEntries = account.getMessageSendLogStore()
|
||||||
|
.findMessages(sender, senderDeviceId, message.getTimestamp(), !message.getRatchetKey().isPresent());
|
||||||
|
|
||||||
|
for (final var logEntry : logEntries) {
|
||||||
|
actions.add(new ResendMessageAction(sender, message.getTimestamp(), logEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.getRatchetKey().isPresent()) {
|
||||||
|
if (account.getSessionStore().isCurrentRatchetKey(sender, senderDeviceId, message.getRatchetKey().get())) {
|
||||||
|
if (logEntries.isEmpty()) {
|
||||||
|
logger.debug("Renewing the session with sender");
|
||||||
|
actions.add(new RenewSessionAction(sender));
|
||||||
|
} else {
|
||||||
|
logger.trace("Archiving the session with sender, a resend message has already been queued");
|
||||||
|
context.getAccount().getSessionStore().archiveSessions(sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var found = false;
|
||||||
|
for (final var logEntry : logEntries) {
|
||||||
|
if (logEntry.groupId().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final var group = account.getGroupStore().getGroup(logEntry.groupId().get());
|
||||||
|
if (group == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
logger.trace("Deleting shared sender key with {} ({}): {}",
|
||||||
|
sender,
|
||||||
|
senderDeviceId,
|
||||||
|
group.getDistributionId());
|
||||||
|
account.getSenderKeyStore().deleteSharedWith(sender, senderDeviceId, group.getDistributionId());
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
logger.debug("Reset all shared sender keys with this recipient, no related message found in send log");
|
||||||
|
account.getSenderKeyStore().deleteSharedWith(sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<HandleAction> handleSyncMessage(
|
private List<HandleAction> handleSyncMessage(
|
||||||
final SignalServiceSyncMessage syncMessage, final RecipientId sender, final boolean ignoreAttachments
|
final SignalServiceSyncMessage syncMessage, final RecipientId sender, final boolean ignoreAttachments
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
import org.asamk.signal.manager.SignalDependencies;
|
import org.asamk.signal.manager.SignalDependencies;
|
||||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||||
import org.asamk.signal.manager.groups.GroupId;
|
import org.asamk.signal.manager.groups.GroupId;
|
||||||
|
@ -11,11 +13,13 @@ import org.asamk.signal.manager.storage.SignalAccount;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||||
import org.asamk.signal.manager.storage.recipients.Profile;
|
import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
import org.whispersystems.libsignal.InvalidRegistrationIdException;
|
import org.whispersystems.libsignal.InvalidRegistrationIdException;
|
||||||
import org.whispersystems.libsignal.NoSessionException;
|
import org.whispersystems.libsignal.NoSessionException;
|
||||||
|
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||||
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
|
@ -45,6 +49,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class SendHelper {
|
public class SendHelper {
|
||||||
|
@ -74,9 +79,7 @@ public class SendHelper {
|
||||||
messageBuilder.withProfileKey(account.getProfileKey().serialize());
|
messageBuilder.withProfileKey(account.getProfileKey().serialize());
|
||||||
|
|
||||||
final var message = messageBuilder.build();
|
final var message = messageBuilder.build();
|
||||||
final var result = sendMessage(message, recipientId);
|
return sendMessage(message, recipientId);
|
||||||
handleSendMessageResult(result);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,29 +93,6 @@ public class SendHelper {
|
||||||
return sendAsGroupMessage(messageBuilder, g);
|
return sendAsGroupMessage(messageBuilder, g);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SendMessageResult> sendAsGroupMessage(
|
|
||||||
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
|
|
||||||
) throws IOException, GroupSendingNotAllowedException {
|
|
||||||
GroupUtils.setGroupContext(messageBuilder, g);
|
|
||||||
messageBuilder.withExpiration(g.getMessageExpirationTimer());
|
|
||||||
|
|
||||||
final var message = messageBuilder.build();
|
|
||||||
final var recipients = g.getMembersWithout(account.getSelfRecipientId());
|
|
||||||
|
|
||||||
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
|
|
||||||
if (message.getBody().isPresent()
|
|
||||||
|| message.getAttachments().isPresent()
|
|
||||||
|| message.getQuote().isPresent()
|
|
||||||
|| message.getPreviews().isPresent()
|
|
||||||
|| message.getMentions().isPresent()
|
|
||||||
|| message.getSticker().isPresent()) {
|
|
||||||
throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sendGroupMessage(message, recipients, g.getDistributionId());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a complete group message to the given recipients (should be current/old/new members)
|
* Send a complete group message to the given recipients (should be current/old/new members)
|
||||||
* This method should only be used for create/update/quit group messages.
|
* This method should only be used for create/update/quit group messages.
|
||||||
|
@ -122,31 +102,7 @@ public class SendHelper {
|
||||||
final Set<RecipientId> recipientIds,
|
final Set<RecipientId> recipientIds,
|
||||||
final DistributionId distributionId
|
final DistributionId distributionId
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
final var messageSender = dependencies.getMessageSender();
|
return sendGroupMessage(message, recipientIds, distributionId, ContentHint.IMPLICIT);
|
||||||
final var results = sendGroupMessageInternal((recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendDataMessage(
|
|
||||||
recipients,
|
|
||||||
unidentifiedAccess,
|
|
||||||
isRecipientUpdate,
|
|
||||||
ContentHint.DEFAULT,
|
|
||||||
message,
|
|
||||||
SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
|
|
||||||
sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()),
|
|
||||||
() -> false),
|
|
||||||
(distId, recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendGroupDataMessage(distId,
|
|
||||||
recipients,
|
|
||||||
unidentifiedAccess,
|
|
||||||
isRecipientUpdate,
|
|
||||||
ContentHint.DEFAULT,
|
|
||||||
message,
|
|
||||||
SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY),
|
|
||||||
recipientIds,
|
|
||||||
distributionId);
|
|
||||||
|
|
||||||
for (var r : results) {
|
|
||||||
handleSendMessageResult(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendDeliveryReceipt(
|
public SendMessageResult sendDeliveryReceipt(
|
||||||
|
@ -162,10 +118,14 @@ public class SendHelper {
|
||||||
public SendMessageResult sendReceiptMessage(
|
public SendMessageResult sendReceiptMessage(
|
||||||
final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
|
final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
|
||||||
) {
|
) {
|
||||||
return handleSendMessage(recipientId,
|
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||||
|
final var result = handleSendMessage(recipientId,
|
||||||
(messageSender, address, unidentifiedAccess) -> messageSender.sendReceipt(address,
|
(messageSender, address, unidentifiedAccess) -> messageSender.sendReceipt(address,
|
||||||
unidentifiedAccess,
|
unidentifiedAccess,
|
||||||
receiptMessage));
|
receiptMessage));
|
||||||
|
messageSendLogStore.insertIfPossible(receiptMessage.getWhen(), result, ContentHint.IMPLICIT);
|
||||||
|
handleSendMessageResult(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendRetryReceipt(
|
public SendMessageResult sendRetryReceipt(
|
||||||
|
@ -175,15 +135,19 @@ public class SendHelper {
|
||||||
errorMessage.getTimestamp(),
|
errorMessage.getTimestamp(),
|
||||||
recipientId,
|
recipientId,
|
||||||
errorMessage.getDeviceId());
|
errorMessage.getDeviceId());
|
||||||
return handleSendMessage(recipientId,
|
final var result = handleSendMessage(recipientId,
|
||||||
(messageSender, address, unidentifiedAccess) -> messageSender.sendRetryReceipt(address,
|
(messageSender, address, unidentifiedAccess) -> messageSender.sendRetryReceipt(address,
|
||||||
unidentifiedAccess,
|
unidentifiedAccess,
|
||||||
groupId.transform(GroupId::serialize),
|
groupId.transform(GroupId::serialize),
|
||||||
errorMessage));
|
errorMessage));
|
||||||
|
handleSendMessageResult(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendNullMessage(RecipientId recipientId) {
|
public SendMessageResult sendNullMessage(RecipientId recipientId) {
|
||||||
return handleSendMessage(recipientId, SignalServiceMessageSender::sendNullMessage);
|
final var result = handleSendMessage(recipientId, SignalServiceMessageSender::sendNullMessage);
|
||||||
|
handleSendMessageResult(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendSelfMessage(
|
public SendMessageResult sendSelfMessage(
|
||||||
|
@ -225,10 +189,12 @@ public class SendHelper {
|
||||||
public SendMessageResult sendTypingMessage(
|
public SendMessageResult sendTypingMessage(
|
||||||
SignalServiceTypingMessage message, RecipientId recipientId
|
SignalServiceTypingMessage message, RecipientId recipientId
|
||||||
) {
|
) {
|
||||||
return handleSendMessage(recipientId,
|
final var result = handleSendMessage(recipientId,
|
||||||
(messageSender, address, unidentifiedAccess) -> messageSender.sendTyping(address,
|
(messageSender, address, unidentifiedAccess) -> messageSender.sendTyping(address,
|
||||||
unidentifiedAccess,
|
unidentifiedAccess,
|
||||||
message));
|
message));
|
||||||
|
handleSendMessageResult(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SendMessageResult> sendGroupTypingMessage(
|
public List<SendMessageResult> sendGroupTypingMessage(
|
||||||
|
@ -244,6 +210,142 @@ public class SendHelper {
|
||||||
return sendGroupTypingMessage(message, recipientIds, distributionId);
|
return sendGroupTypingMessage(message, recipientIds, distributionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SendMessageResult resendMessage(
|
||||||
|
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
|
||||||
|
) {
|
||||||
|
if (messageSendLogEntry.groupId().isEmpty()) {
|
||||||
|
return handleSendMessage(recipientId,
|
||||||
|
(messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address,
|
||||||
|
unidentifiedAccess,
|
||||||
|
timestamp,
|
||||||
|
messageSendLogEntry.content(),
|
||||||
|
messageSendLogEntry.contentHint(),
|
||||||
|
Optional.absent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
final var groupId = messageSendLogEntry.groupId().get();
|
||||||
|
final var group = account.getGroupStore().getGroup(groupId);
|
||||||
|
|
||||||
|
if (group == null) {
|
||||||
|
logger.debug("Could not find a matching group for the groupId {}! Skipping message send.",
|
||||||
|
groupId.toBase64());
|
||||||
|
return null;
|
||||||
|
} else if (!group.getMembers().contains(recipientId)) {
|
||||||
|
logger.warn("The target user is no longer in the group {}! Skipping message send.", groupId.toBase64());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var senderKeyDistributionMessage = dependencies.getMessageSender()
|
||||||
|
.getOrCreateNewGroupSession(group.getDistributionId());
|
||||||
|
final var distributionBytes = ByteString.copyFrom(senderKeyDistributionMessage.serialize());
|
||||||
|
final var contentToSend = messageSendLogEntry.content()
|
||||||
|
.toBuilder()
|
||||||
|
.setSenderKeyDistributionMessage(distributionBytes)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final var result = handleSendMessage(recipientId,
|
||||||
|
(messageSender, address, unidentifiedAccess) -> messageSender.resendContent(address,
|
||||||
|
unidentifiedAccess,
|
||||||
|
timestamp,
|
||||||
|
contentToSend,
|
||||||
|
messageSendLogEntry.contentHint(),
|
||||||
|
Optional.of(group.getGroupId().serialize())));
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||||
|
final var addresses = result.getSuccess()
|
||||||
|
.getDevices()
|
||||||
|
.stream()
|
||||||
|
.map(device -> new SignalProtocolAddress(address.getIdentifier(), device))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
account.getSenderKeyStore().markSenderKeySharedWith(group.getDistributionId(), addresses);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SendMessageResult> sendAsGroupMessage(
|
||||||
|
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g
|
||||||
|
) throws IOException, GroupSendingNotAllowedException {
|
||||||
|
GroupUtils.setGroupContext(messageBuilder, g);
|
||||||
|
messageBuilder.withExpiration(g.getMessageExpirationTimer());
|
||||||
|
|
||||||
|
final var message = messageBuilder.build();
|
||||||
|
final var recipients = g.getMembersWithout(account.getSelfRecipientId());
|
||||||
|
|
||||||
|
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
|
||||||
|
if (message.getBody().isPresent()
|
||||||
|
|| message.getAttachments().isPresent()
|
||||||
|
|| message.getQuote().isPresent()
|
||||||
|
|| message.getPreviews().isPresent()
|
||||||
|
|| message.getMentions().isPresent()
|
||||||
|
|| message.getSticker().isPresent()) {
|
||||||
|
throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendGroupMessage(message, recipients, g.getDistributionId(), ContentHint.RESENDABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SendMessageResult> sendGroupMessage(
|
||||||
|
final SignalServiceDataMessage message,
|
||||||
|
final Set<RecipientId> recipientIds,
|
||||||
|
final DistributionId distributionId,
|
||||||
|
final ContentHint contentHint
|
||||||
|
) throws IOException {
|
||||||
|
final var messageSender = dependencies.getMessageSender();
|
||||||
|
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||||
|
final AtomicLong entryId = new AtomicLong(-1);
|
||||||
|
|
||||||
|
final LegacySenderHandler legacySender = (recipients, unidentifiedAccess, isRecipientUpdate) -> messageSender.sendDataMessage(
|
||||||
|
recipients,
|
||||||
|
unidentifiedAccess,
|
||||||
|
isRecipientUpdate,
|
||||||
|
contentHint,
|
||||||
|
message,
|
||||||
|
SignalServiceMessageSender.LegacyGroupEvents.EMPTY,
|
||||||
|
sendResult -> {
|
||||||
|
logger.trace("Partial message send result: {}", sendResult.isSuccess());
|
||||||
|
synchronized (entryId) {
|
||||||
|
if (entryId.get() == -1) {
|
||||||
|
final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(),
|
||||||
|
sendResult,
|
||||||
|
contentHint);
|
||||||
|
entryId.set(newId);
|
||||||
|
} else {
|
||||||
|
messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), sendResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() -> false);
|
||||||
|
final SenderKeySenderHandler senderKeySender = (distId, recipients, unidentifiedAccess, isRecipientUpdate) -> {
|
||||||
|
final var res = messageSender.sendGroupDataMessage(distId,
|
||||||
|
recipients,
|
||||||
|
unidentifiedAccess,
|
||||||
|
isRecipientUpdate,
|
||||||
|
contentHint,
|
||||||
|
message,
|
||||||
|
SignalServiceMessageSender.SenderKeyGroupEvents.EMPTY);
|
||||||
|
synchronized (entryId) {
|
||||||
|
if (entryId.get() == -1) {
|
||||||
|
final var newId = messageSendLogStore.insertIfPossible(message.getTimestamp(), res, contentHint);
|
||||||
|
entryId.set(newId);
|
||||||
|
} else {
|
||||||
|
messageSendLogStore.addRecipientToExistingEntryIfPossible(entryId.get(), res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
final var results = sendGroupMessageInternal(legacySender, senderKeySender, recipientIds, distributionId);
|
||||||
|
|
||||||
|
for (var r : results) {
|
||||||
|
handleSendMessageResult(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
private List<SendMessageResult> sendGroupTypingMessage(
|
private List<SendMessageResult> sendGroupTypingMessage(
|
||||||
final SignalServiceTypingMessage message,
|
final SignalServiceTypingMessage message,
|
||||||
final Set<RecipientId> recipientIds,
|
final Set<RecipientId> recipientIds,
|
||||||
|
@ -462,12 +564,16 @@ public class SendHelper {
|
||||||
private SendMessageResult sendMessage(
|
private SendMessageResult sendMessage(
|
||||||
SignalServiceDataMessage message, RecipientId recipientId
|
SignalServiceDataMessage message, RecipientId recipientId
|
||||||
) {
|
) {
|
||||||
return handleSendMessage(recipientId,
|
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||||
|
final var result = handleSendMessage(recipientId,
|
||||||
(messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
|
(messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address,
|
||||||
unidentifiedAccess,
|
unidentifiedAccess,
|
||||||
ContentHint.DEFAULT,
|
ContentHint.RESENDABLE,
|
||||||
message,
|
message,
|
||||||
SignalServiceMessageSender.IndividualSendEvents.EMPTY));
|
SignalServiceMessageSender.IndividualSendEvents.EMPTY));
|
||||||
|
messageSendLogStore.insertIfPossible(message.getTimestamp(), result, ContentHint.RESENDABLE);
|
||||||
|
handleSendMessageResult(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandler s) {
|
private SendMessageResult handleSendMessage(RecipientId recipientId, SenderHandler s) {
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
package org.asamk.signal.manager.storage;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.sendLog.MessageSendLogStore;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.sqlite.SQLiteConfig;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public class Database implements AutoCloseable {
|
||||||
|
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||||
|
private static final long DATABASE_VERSION = 1;
|
||||||
|
|
||||||
|
private final HikariDataSource dataSource;
|
||||||
|
|
||||||
|
private Database(final HikariDataSource dataSource) {
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Database init(File databaseFile) throws SQLException {
|
||||||
|
HikariDataSource dataSource = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataSource = getHikariDataSource(databaseFile.getAbsolutePath());
|
||||||
|
|
||||||
|
try (final var connection = dataSource.getConnection()) {
|
||||||
|
final var userVersion = getUserVersion(connection);
|
||||||
|
logger.trace("Current database version: {} Program database version: {}",
|
||||||
|
userVersion,
|
||||||
|
DATABASE_VERSION);
|
||||||
|
|
||||||
|
if (userVersion > DATABASE_VERSION) {
|
||||||
|
logger.error("Database has been updated by a newer signal-cli version");
|
||||||
|
throw new SQLException("Database has been updated by a newer signal-cli version");
|
||||||
|
} else if (userVersion < DATABASE_VERSION) {
|
||||||
|
if (userVersion < 1) {
|
||||||
|
logger.debug("Updating database: Creating message send log tables");
|
||||||
|
MessageSendLogStore.createSql(connection);
|
||||||
|
}
|
||||||
|
setUserVersion(connection, DATABASE_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
final var result = new Database(dataSource);
|
||||||
|
dataSource = null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (dataSource != null) {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
return dataSource.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws SQLException {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getUserVersion(final Connection connection) throws SQLException {
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
final var resultSet = statement.executeQuery("PRAGMA user_version");
|
||||||
|
return resultSet.getLong(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setUserVersion(final Connection connection, long userVersion) throws SQLException {
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("PRAGMA user_version = " + userVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HikariDataSource getHikariDataSource(final String databaseFile) {
|
||||||
|
final var sqliteConfig = new SQLiteConfig();
|
||||||
|
sqliteConfig.setBusyTimeout(10_000);
|
||||||
|
sqliteConfig.setTransactionMode(SQLiteConfig.TransactionMode.IMMEDIATE);
|
||||||
|
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
config.setJdbcUrl("jdbc:sqlite:" + databaseFile);
|
||||||
|
config.setDataSourceProperties(sqliteConfig.toProperties());
|
||||||
|
config.setMinimumIdle(1);
|
||||||
|
config.setConnectionInitSql("PRAGMA foreign_keys=ON");
|
||||||
|
return new HikariDataSource(config);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import org.asamk.signal.manager.storage.recipients.Profile;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
||||||
|
import org.asamk.signal.manager.storage.sendLog.MessageSendLogStore;
|
||||||
import org.asamk.signal.manager.storage.senderKeys.SenderKeyStore;
|
import org.asamk.signal.manager.storage.senderKeys.SenderKeyStore;
|
||||||
import org.asamk.signal.manager.storage.sessions.SessionStore;
|
import org.asamk.signal.manager.storage.sessions.SessionStore;
|
||||||
import org.asamk.signal.manager.storage.stickers.StickerStore;
|
import org.asamk.signal.manager.storage.stickers.StickerStore;
|
||||||
|
@ -62,6 +63,7 @@ import java.nio.channels.ClosedChannelException;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.FileLock;
|
import java.nio.channels.FileLock;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -120,6 +122,9 @@ public class SignalAccount implements Closeable {
|
||||||
private ConfigurationStore.Storage configurationStoreStorage;
|
private ConfigurationStore.Storage configurationStoreStorage;
|
||||||
|
|
||||||
private MessageCache messageCache;
|
private MessageCache messageCache;
|
||||||
|
private MessageSendLogStore messageSendLogStore;
|
||||||
|
|
||||||
|
private Database database;
|
||||||
|
|
||||||
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
|
private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
|
||||||
this.fileChannel = fileChannel;
|
this.fileChannel = fileChannel;
|
||||||
|
@ -227,6 +232,10 @@ public class SignalAccount implements Closeable {
|
||||||
return signalAccount;
|
return signalAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void initDatabase() {
|
||||||
|
getDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
private void clearAllPreKeys() {
|
private void clearAllPreKeys() {
|
||||||
this.preKeyIdOffset = new SecureRandom().nextInt(Medium.MAX_VALUE);
|
this.preKeyIdOffset = new SecureRandom().nextInt(Medium.MAX_VALUE);
|
||||||
this.nextSignedPreKeyId = new SecureRandom().nextInt(Medium.MAX_VALUE);
|
this.nextSignedPreKeyId = new SecureRandom().nextInt(Medium.MAX_VALUE);
|
||||||
|
@ -383,6 +392,10 @@ public class SignalAccount implements Closeable {
|
||||||
return new File(getUserPath(dataPath, account), "recipients-store");
|
return new File(getUserPath(dataPath, account), "recipients-store");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static File getDatabaseFile(File dataPath, String account) {
|
||||||
|
return new File(getUserPath(dataPath, account), "account.db");
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean userExists(File dataPath, String account) {
|
public static boolean userExists(File dataPath, String account) {
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -869,6 +882,21 @@ public class SignalAccount implements Closeable {
|
||||||
() -> messageCache = new MessageCache(getMessageCachePath(dataPath, account)));
|
() -> messageCache = new MessageCache(getMessageCachePath(dataPath, account)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Database getDatabase() {
|
||||||
|
return getOrCreate(() -> database, () -> {
|
||||||
|
try {
|
||||||
|
database = Database.init(getDatabaseFile(dataPath, account));
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageSendLogStore getMessageSendLogStore() {
|
||||||
|
return getOrCreate(() -> messageSendLogStore,
|
||||||
|
() -> messageSendLogStore = new MessageSendLogStore(getRecipientStore(), getDatabase()));
|
||||||
|
}
|
||||||
|
|
||||||
public String getAccount() {
|
public String getAccount() {
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
@ -1050,6 +1078,16 @@ public class SignalAccount implements Closeable {
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
synchronized (fileChannel) {
|
synchronized (fileChannel) {
|
||||||
|
if (database != null) {
|
||||||
|
try {
|
||||||
|
database.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed to close account database: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (messageSendLogStore != null) {
|
||||||
|
messageSendLogStore.close();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
lock.close();
|
lock.close();
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.asamk.signal.manager.storage.sendLog;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.groups.GroupId;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public record MessageSendLogEntry(
|
||||||
|
Optional<GroupId> groupId, SignalServiceProtos.Content content, ContentHint contentHint
|
||||||
|
) {}
|
|
@ -0,0 +1,396 @@
|
||||||
|
package org.asamk.signal.manager.storage.sendLog;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.groups.GroupId;
|
||||||
|
import org.asamk.signal.manager.groups.GroupUtils;
|
||||||
|
import org.asamk.signal.manager.storage.Database;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||||
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||||
|
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||||
|
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Spliterator;
|
||||||
|
import java.util.Spliterators;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
public class MessageSendLogStore implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MessageSendLogStore.class);
|
||||||
|
|
||||||
|
private static final String TABLE_MESSAGE_SEND_LOG = "message_send_log";
|
||||||
|
private static final String TABLE_MESSAGE_SEND_LOG_CONTENT = "message_send_log_content";
|
||||||
|
|
||||||
|
private static final Duration LOG_DURATION = Duration.ofDays(1);
|
||||||
|
|
||||||
|
private final RecipientResolver recipientResolver;
|
||||||
|
private final Database database;
|
||||||
|
private final Thread cleanupThread;
|
||||||
|
|
||||||
|
public MessageSendLogStore(
|
||||||
|
final RecipientResolver recipientResolver, final Database database
|
||||||
|
) {
|
||||||
|
this.recipientResolver = recipientResolver;
|
||||||
|
this.database = database;
|
||||||
|
this.cleanupThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
final var interval = Duration.ofHours(1).toMillis();
|
||||||
|
while (true) {
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
deleteOutdatedEntries(connection);
|
||||||
|
Thread.sleep(interval);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Deleting outdated entries failed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.debug("Stopping msl cleanup thread");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cleanupThread.setDaemon(true);
|
||||||
|
cleanupThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void createSql(Connection connection) throws SQLException {
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("""
|
||||||
|
CREATE TABLE message_send_log (
|
||||||
|
_id INTEGER PRIMARY KEY,
|
||||||
|
content_id INTEGER NOT NULL REFERENCES message_send_log_content (_id) ON DELETE CASCADE,
|
||||||
|
recipient_id INTEGER NOT NULL,
|
||||||
|
device_id INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE message_send_log_content (
|
||||||
|
_id INTEGER PRIMARY KEY,
|
||||||
|
group_id BLOB,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
content BLOB NOT NULL,
|
||||||
|
content_hint INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX mslc_timestamp_index ON message_send_log_content (timestamp);
|
||||||
|
CREATE INDEX msl_recipient_index ON message_send_log (recipient_id, device_id, content_id);
|
||||||
|
CREATE INDEX msl_content_index ON message_send_log (content_id);
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MessageSendLogEntry> findMessages(
|
||||||
|
final RecipientId recipientId, final int deviceId, final long timestamp, final boolean isSenderKey
|
||||||
|
) {
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
deleteOutdatedEntries(connection);
|
||||||
|
|
||||||
|
try (final var statement = connection.prepareStatement(
|
||||||
|
"SELECT group_id, content, content_hint FROM %s l INNER JOIN %s lc ON l.content_id = lc._id WHERE l.recipient_id = ? AND l.device_id = ? AND lc.timestamp = ?".formatted(
|
||||||
|
TABLE_MESSAGE_SEND_LOG,
|
||||||
|
TABLE_MESSAGE_SEND_LOG_CONTENT))) {
|
||||||
|
statement.setLong(1, recipientId.id());
|
||||||
|
statement.setInt(2, deviceId);
|
||||||
|
statement.setLong(3, timestamp);
|
||||||
|
try (var result = executeQueryForStream(statement, resultSet -> {
|
||||||
|
final var groupId = Optional.ofNullable(resultSet.getBytes("group_id"))
|
||||||
|
.map(GroupId::unknownVersion);
|
||||||
|
final SignalServiceProtos.Content content;
|
||||||
|
try {
|
||||||
|
content = SignalServiceProtos.Content.parseFrom(resultSet.getBinaryStream("content"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("Failed to parse content from message send log", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final var contentHint = ContentHint.fromType(resultSet.getInt("content_hint"));
|
||||||
|
return new MessageSendLogEntry(groupId, content, contentHint);
|
||||||
|
})) {
|
||||||
|
return result.filter(Objects::nonNull)
|
||||||
|
.filter(e -> !isSenderKey || e.groupId().isPresent())
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed read from message send log", e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long insertIfPossible(
|
||||||
|
long sentTimestamp, SendMessageResult sendMessageResult, ContentHint contentHint
|
||||||
|
) {
|
||||||
|
final RecipientDevices recipientDevice = getRecipientDevices(sendMessageResult);
|
||||||
|
if (recipientDevice == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return insert(List.of(recipientDevice),
|
||||||
|
sentTimestamp,
|
||||||
|
sendMessageResult.getSuccess().getContent().get(),
|
||||||
|
contentHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long insertIfPossible(
|
||||||
|
long sentTimestamp, List<SendMessageResult> sendMessageResults, ContentHint contentHint
|
||||||
|
) {
|
||||||
|
final var recipientDevices = sendMessageResults.stream()
|
||||||
|
.map(this::getRecipientDevices)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
if (recipientDevices.isEmpty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var content = sendMessageResults.stream()
|
||||||
|
.filter(r -> r.isSuccess() && r.getSuccess().getContent().isPresent())
|
||||||
|
.map(r -> r.getSuccess().getContent().get())
|
||||||
|
.findFirst()
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return insert(recipientDevices, sentTimestamp, content, contentHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRecipientToExistingEntryIfPossible(final long contentId, final SendMessageResult sendMessageResult) {
|
||||||
|
final RecipientDevices recipientDevice = getRecipientDevices(sendMessageResult);
|
||||||
|
if (recipientDevice == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertRecipientsForExistingContent(contentId, List.of(recipientDevice));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRecipientToExistingEntryIfPossible(
|
||||||
|
final long contentId, final List<SendMessageResult> sendMessageResults
|
||||||
|
) {
|
||||||
|
final var recipientDevices = sendMessageResults.stream()
|
||||||
|
.map(this::getRecipientDevices)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
if (recipientDevices.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertRecipientsForExistingContent(contentId, recipientDevices);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteEntryForGroup(long sentTimestamp, GroupId groupId) {
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
try (final var statement = connection.prepareStatement(
|
||||||
|
"DELETE FROM %s AS lc WHERE lc.timestamp = ? AND lc.group_id = ?".formatted(
|
||||||
|
TABLE_MESSAGE_SEND_LOG_CONTENT))) {
|
||||||
|
statement.setLong(1, sentTimestamp);
|
||||||
|
statement.setBytes(2, groupId.serialize());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed delete from message send log", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteEntryForRecipientNonGroup(long sentTimestamp, RecipientId recipientId) {
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
try (final var statement = connection.prepareStatement(
|
||||||
|
"DELETE FROM %s AS lc WHERE lc.timestamp = ? AND lc.group_id IS NULL AND lc._id IN (SELECT content_id FROM %s l WHERE l.recipient_id = ?)".formatted(
|
||||||
|
TABLE_MESSAGE_SEND_LOG_CONTENT,
|
||||||
|
TABLE_MESSAGE_SEND_LOG))) {
|
||||||
|
statement.setLong(1, sentTimestamp);
|
||||||
|
statement.setLong(2, recipientId.id());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteOrphanedLogContents(connection);
|
||||||
|
connection.commit();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed delete from message send log", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteEntryForRecipient(long sentTimestamp, RecipientId recipientId, int deviceId) {
|
||||||
|
deleteEntriesForRecipient(List.of(sentTimestamp), recipientId, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteEntriesForRecipient(List<Long> sentTimestamps, RecipientId recipientId, int deviceId) {
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
try (final var statement = connection.prepareStatement(
|
||||||
|
"DELETE FROM %s AS l WHERE l.content_id IN (SELECT _id FROM %s lc WHERE lc.timestamp = ?) AND l.recipient_id = ? AND l.device_id = ?".formatted(
|
||||||
|
TABLE_MESSAGE_SEND_LOG,
|
||||||
|
TABLE_MESSAGE_SEND_LOG_CONTENT))) {
|
||||||
|
for (final var sentTimestamp : sentTimestamps) {
|
||||||
|
statement.setLong(1, sentTimestamp);
|
||||||
|
statement.setLong(2, recipientId.id());
|
||||||
|
statement.setInt(3, deviceId);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteOrphanedLogContents(connection);
|
||||||
|
connection.commit();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed delete from message send log", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
cleanupThread.interrupt();
|
||||||
|
try {
|
||||||
|
cleanupThread.join();
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RecipientDevices getRecipientDevices(final SendMessageResult sendMessageResult) {
|
||||||
|
if (sendMessageResult.isSuccess() && sendMessageResult.getSuccess().getContent().isPresent()) {
|
||||||
|
final var recipientId = recipientResolver.resolveRecipient(sendMessageResult.getAddress());
|
||||||
|
return new RecipientDevices(recipientId, sendMessageResult.getSuccess().getDevices());
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long insert(
|
||||||
|
final List<RecipientDevices> recipientDevices,
|
||||||
|
final long sentTimestamp,
|
||||||
|
final SignalServiceProtos.Content content,
|
||||||
|
final ContentHint contentHint
|
||||||
|
) {
|
||||||
|
byte[] groupId = getGroupId(content);
|
||||||
|
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
final long contentId;
|
||||||
|
try (final var statement = connection.prepareStatement(
|
||||||
|
"INSERT INTO %s (timestamp, group_id, content, content_hint) VALUES (?,?,?,?)".formatted(
|
||||||
|
TABLE_MESSAGE_SEND_LOG_CONTENT))) {
|
||||||
|
statement.setLong(1, sentTimestamp);
|
||||||
|
statement.setBytes(2, groupId);
|
||||||
|
statement.setBytes(3, content.toByteArray());
|
||||||
|
statement.setInt(4, contentHint.getType());
|
||||||
|
statement.executeUpdate();
|
||||||
|
final var generatedKeys = statement.getGeneratedKeys();
|
||||||
|
if (generatedKeys.next()) {
|
||||||
|
contentId = generatedKeys.getLong(1);
|
||||||
|
} else {
|
||||||
|
contentId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (contentId == -1) {
|
||||||
|
logger.warn("Failed to insert message send log content");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
insertRecipientsForExistingContent(contentId, recipientDevices, connection);
|
||||||
|
|
||||||
|
connection.commit();
|
||||||
|
return contentId;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed to insert into message send log", e);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getGroupId(final SignalServiceProtos.Content content) {
|
||||||
|
try {
|
||||||
|
return !content.hasDataMessage()
|
||||||
|
? null
|
||||||
|
: content.getDataMessage().hasGroup()
|
||||||
|
? content.getDataMessage().getGroup().getId().toByteArray()
|
||||||
|
: content.getDataMessage().hasGroupV2()
|
||||||
|
? GroupUtils.getGroupIdV2(new GroupMasterKey(content.getDataMessage()
|
||||||
|
.getGroupV2()
|
||||||
|
.getMasterKey()
|
||||||
|
.toByteArray())).serialize()
|
||||||
|
: null;
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
logger.warn("Failed to parse groupId id from content");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertRecipientsForExistingContent(
|
||||||
|
final long contentId, final List<RecipientDevices> recipientDevices
|
||||||
|
) {
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
insertRecipientsForExistingContent(contentId, recipientDevices, connection);
|
||||||
|
connection.commit();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed to append recipients to message send log", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertRecipientsForExistingContent(
|
||||||
|
final long contentId, final List<RecipientDevices> recipientDevices, final Connection connection
|
||||||
|
) throws SQLException {
|
||||||
|
try (final var statement = connection.prepareStatement(
|
||||||
|
"INSERT INTO %s (recipient_id, device_id, content_id) VALUES (?,?,?)".formatted(TABLE_MESSAGE_SEND_LOG))) {
|
||||||
|
for (final var recipientDevice : recipientDevices) {
|
||||||
|
for (final var deviceId : recipientDevice.deviceIds()) {
|
||||||
|
statement.setLong(1, recipientDevice.recipientId().id());
|
||||||
|
statement.setInt(2, deviceId);
|
||||||
|
statement.setLong(3, contentId);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteOutdatedEntries(final Connection connection) throws SQLException {
|
||||||
|
try (final var statement = connection.prepareStatement("DELETE FROM %s WHERE timestamp < ?".formatted(
|
||||||
|
TABLE_MESSAGE_SEND_LOG_CONTENT))) {
|
||||||
|
statement.setLong(1, System.currentTimeMillis() - LOG_DURATION.toMillis());
|
||||||
|
final var rowCount = statement.executeUpdate();
|
||||||
|
if (rowCount > 0) {
|
||||||
|
logger.debug("Removed {} outdated entries from the message send log", rowCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteOrphanedLogContents(final Connection connection) throws SQLException {
|
||||||
|
try (final var statement = connection.prepareStatement(
|
||||||
|
"DELETE FROM %s WHERE _id NOT IN (SELECT content_id FROM %s)".formatted(TABLE_MESSAGE_SEND_LOG_CONTENT,
|
||||||
|
TABLE_MESSAGE_SEND_LOG))) {
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Stream<T> executeQueryForStream(
|
||||||
|
PreparedStatement statement, ResultSetMapper<T> mapper
|
||||||
|
) throws SQLException {
|
||||||
|
final var resultSet = statement.executeQuery();
|
||||||
|
|
||||||
|
return StreamSupport.stream(new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, Spliterator.ORDERED) {
|
||||||
|
@Override
|
||||||
|
public boolean tryAdvance(final Consumer<? super T> consumer) {
|
||||||
|
try {
|
||||||
|
if (!resultSet.next()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
consumer.accept(mapper.apply(resultSet));
|
||||||
|
return true;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.warn("Failed to read from database result", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ResultSetMapper<T> {
|
||||||
|
|
||||||
|
T apply(ResultSet resultSet) throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RecipientDevices(RecipientId recipientId, List<Integer> deviceIds) {}
|
||||||
|
}
|
|
@ -164,6 +164,21 @@ public class SenderKeySharedStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteSharedWith(
|
||||||
|
final RecipientId recipientId, final int deviceId, final DistributionId distributionId
|
||||||
|
) {
|
||||||
|
synchronized (sharedSenderKeys) {
|
||||||
|
final var entries = sharedSenderKeys.getOrDefault(distributionId.asUuid(), Set.of());
|
||||||
|
|
||||||
|
sharedSenderKeys.put(distributionId.asUuid(), new HashSet<>(entries) {
|
||||||
|
{
|
||||||
|
remove(new SenderKeySharedEntry(recipientId, deviceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
saveLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteAllFor(final DistributionId distributionId) {
|
public void deleteAllFor(final DistributionId distributionId) {
|
||||||
synchronized (sharedSenderKeys) {
|
synchronized (sharedSenderKeys) {
|
||||||
if (sharedSenderKeys.remove(distributionId.asUuid()) != null) {
|
if (sharedSenderKeys.remove(distributionId.asUuid()) != null) {
|
||||||
|
|
|
@ -71,6 +71,10 @@ public class SenderKeyStore implements SignalServiceSenderKeyStore {
|
||||||
senderKeySharedStore.deleteAllFor(recipientId);
|
senderKeySharedStore.deleteAllFor(recipientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteSharedWith(RecipientId recipientId, int deviceId, DistributionId distributionId) {
|
||||||
|
senderKeySharedStore.deleteSharedWith(recipientId, deviceId, distributionId);
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteOurKey(RecipientId selfRecipientId, DistributionId distributionId) {
|
public void deleteOurKey(RecipientId selfRecipientId, DistributionId distributionId) {
|
||||||
senderKeySharedStore.deleteAllFor(distributionId);
|
senderKeySharedStore.deleteAllFor(distributionId);
|
||||||
senderKeyRecordStore.deleteSenderKey(selfRecipientId, distributionId.asUuid());
|
senderKeyRecordStore.deleteSenderKey(selfRecipientId, distributionId.asUuid());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue