mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-08-30 06:16:02 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c17e01c37 | ||
|
|
9605c94f09 | ||
|
|
d2f5696df1 | ||
|
|
81e513d6b7 | ||
|
|
dbf11fd006 | ||
|
|
7e802166a3 | ||
|
|
0f88410465 | ||
|
|
13819c8f09 | ||
|
|
85580d1ff6 | ||
|
|
f8ea8ed314 | ||
|
|
472784e614 | ||
|
|
db97731f63 | ||
|
|
ff9f2028ae | ||
|
|
9ee5262876 |
72
.github/workflows/release.yml
vendored
72
.github/workflows/release.yml
vendored
@ -35,55 +35,19 @@ jobs:
|
|||||||
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
|
||||||
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create release
|
- name: Create draft release and upload assets
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
run: |
|
||||||
tag_name: v${{ steps.version.outputs.version }} # note: added `v`
|
gh release create "v${{ steps.version.outputs.version }}" \
|
||||||
release_name: v${{ steps.version.outputs.version }} # note: added `v`
|
--repo "${{ github.repository }}" \
|
||||||
draft: true
|
--title "v${{ steps.version.outputs.version }}" \
|
||||||
|
--draft \
|
||||||
- name: Upload archive
|
--verify-tag \
|
||||||
uses: actions/upload-release-asset@v1
|
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
|
||||||
env:
|
"signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
"signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \
|
||||||
with:
|
"signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz"
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.version.outputs.version }}.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.version.outputs.version }}.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload Linux native archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload Linux client archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
- name: Upload JSON schemas archive
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz
|
|
||||||
asset_name: signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz
|
|
||||||
asset_content_type: application/x-compressed-tar # .tar.gz
|
|
||||||
|
|
||||||
build-container:
|
build-container:
|
||||||
needs: release
|
needs: release
|
||||||
@ -104,7 +68,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v3
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
|
||||||
@ -112,7 +76,7 @@ jobs:
|
|||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v2
|
uses: redhat-actions/push-to-registry@v3
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
@ -145,7 +109,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v3
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
|
||||||
@ -153,7 +117,7 @@ jobs:
|
|||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v2
|
uses: redhat-actions/push-to-registry@v3
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
@ -186,7 +150,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Image
|
- name: Build Image
|
||||||
id: build_image
|
id: build_image
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v3
|
||||||
with:
|
with:
|
||||||
image: ${{ env.IMAGE_NAME }}
|
image: ${{ env.IMAGE_NAME }}
|
||||||
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
|
||||||
@ -194,7 +158,7 @@ jobs:
|
|||||||
oci: true
|
oci: true
|
||||||
|
|
||||||
- name: Push To GHCR
|
- name: Push To GHCR
|
||||||
uses: redhat-actions/push-to-registry@v2
|
uses: redhat-actions/push-to-registry@v3
|
||||||
id: push
|
id: push
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.build_image.outputs.image }}
|
image: ${{ steps.build_image.outputs.image }}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
## [0.14.7] - 2026-08-01
|
## [0.14.7] - 2026-08-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -5,12 +5,12 @@ plugins {
|
|||||||
application
|
application
|
||||||
eclipse
|
eclipse
|
||||||
`check-lib-versions`
|
`check-lib-versions`
|
||||||
id("org.graalvm.buildtools.native") version "1.1.6"
|
id("org.graalvm.buildtools.native") version "1.1.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "org.asamk"
|
group = "org.asamk"
|
||||||
version = "0.14.7"
|
version = "0.14.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ coroutines = "1.10.2"
|
|||||||
junit = "6.1.2"
|
junit = "6.1.2"
|
||||||
micronaut-json-schema = "2.1.0"
|
micronaut-json-schema = "2.1.0"
|
||||||
micronaut-core = "5.1.10"
|
micronaut-core = "5.1.10"
|
||||||
signal-service = "2.15.3_unofficial_151"
|
signal-service = "2.15.3_unofficial_152"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85"
|
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85"
|
||||||
|
|||||||
@ -184,6 +184,10 @@ public interface Manager extends Closeable {
|
|||||||
|
|
||||||
void deleteGroup(GroupId groupId) throws IOException;
|
void deleteGroup(GroupId groupId) throws IOException;
|
||||||
|
|
||||||
|
SendGroupMessageResults terminateGroup(
|
||||||
|
GroupId groupId
|
||||||
|
) throws IOException, GroupNotFoundException, NotAGroupMemberException;
|
||||||
|
|
||||||
Pair<GroupId, SendGroupMessageResults> createGroup(
|
Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
String name,
|
String name,
|
||||||
Set<RecipientIdentifier.Single> members,
|
Set<RecipientIdentifier.Single> members,
|
||||||
|
|||||||
@ -15,6 +15,7 @@ public record Contact(
|
|||||||
long muteUntil,
|
long muteUntil,
|
||||||
boolean hideStory,
|
boolean hideStory,
|
||||||
boolean isBlocked,
|
boolean isBlocked,
|
||||||
|
long blockedAt,
|
||||||
boolean isArchived,
|
boolean isArchived,
|
||||||
boolean isProfileSharingEnabled,
|
boolean isProfileSharingEnabled,
|
||||||
boolean isHidden,
|
boolean isHidden,
|
||||||
@ -34,6 +35,7 @@ public record Contact(
|
|||||||
builder.muteUntil,
|
builder.muteUntil,
|
||||||
builder.hideStory,
|
builder.hideStory,
|
||||||
builder.isBlocked,
|
builder.isBlocked,
|
||||||
|
builder.blockedAt,
|
||||||
builder.isArchived,
|
builder.isArchived,
|
||||||
builder.isProfileSharingEnabled,
|
builder.isProfileSharingEnabled,
|
||||||
builder.isHidden,
|
builder.isHidden,
|
||||||
@ -58,6 +60,7 @@ public record Contact(
|
|||||||
builder.muteUntil = copy.muteUntil();
|
builder.muteUntil = copy.muteUntil();
|
||||||
builder.hideStory = copy.hideStory();
|
builder.hideStory = copy.hideStory();
|
||||||
builder.isBlocked = copy.isBlocked();
|
builder.isBlocked = copy.isBlocked();
|
||||||
|
builder.blockedAt = copy.blockedAt();
|
||||||
builder.isArchived = copy.isArchived();
|
builder.isArchived = copy.isArchived();
|
||||||
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
|
||||||
builder.isHidden = copy.isHidden();
|
builder.isHidden = copy.isHidden();
|
||||||
@ -109,6 +112,7 @@ public record Contact(
|
|||||||
private long muteUntil;
|
private long muteUntil;
|
||||||
private boolean hideStory;
|
private boolean hideStory;
|
||||||
private boolean isBlocked;
|
private boolean isBlocked;
|
||||||
|
private long blockedAt;
|
||||||
private boolean isArchived;
|
private boolean isArchived;
|
||||||
private boolean isProfileSharingEnabled;
|
private boolean isProfileSharingEnabled;
|
||||||
private boolean isHidden;
|
private boolean isHidden;
|
||||||
@ -177,10 +181,20 @@ public record Contact(
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Builder withIsBlocked(final boolean val) {
|
public Builder withIsBlocked(final boolean val) {
|
||||||
|
if (val && !isBlocked) {
|
||||||
|
blockedAt = System.currentTimeMillis();
|
||||||
|
} else if (!val) {
|
||||||
|
blockedAt = 0;
|
||||||
|
}
|
||||||
isBlocked = val;
|
isBlocked = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withBlockedAt(final long val) {
|
||||||
|
blockedAt = val;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder withIsArchived(final boolean val) {
|
public Builder withIsArchived(final boolean val) {
|
||||||
isArchived = val;
|
isArchived = val;
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@ -22,7 +22,8 @@ public record Group(
|
|||||||
GroupPermission permissionEditDetails,
|
GroupPermission permissionEditDetails,
|
||||||
GroupPermission permissionSendMessage,
|
GroupPermission permissionSendMessage,
|
||||||
boolean isMember,
|
boolean isMember,
|
||||||
boolean isAdmin
|
boolean isAdmin,
|
||||||
|
boolean isTerminated
|
||||||
) {
|
) {
|
||||||
|
|
||||||
public static Group from(
|
public static Group from(
|
||||||
@ -59,6 +60,7 @@ public record Group(
|
|||||||
groupInfo.getPermissionEditDetails(),
|
groupInfo.getPermissionEditDetails(),
|
||||||
groupInfo.getPermissionSendMessage(),
|
groupInfo.getPermissionSendMessage(),
|
||||||
groupInfo.isMember(selfRecipientId),
|
groupInfo.isMember(selfRecipientId),
|
||||||
groupInfo.isAdmin(selfRecipientId));
|
groupInfo.isAdmin(selfRecipientId),
|
||||||
|
groupInfo.isTerminated());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -739,7 +739,10 @@ public record MessageEnvelope(
|
|||||||
null,
|
null,
|
||||||
d.getE164(),
|
d.getE164(),
|
||||||
null))
|
null))
|
||||||
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
|
.toList(),
|
||||||
|
blockedListMessage.groups.stream()
|
||||||
|
.map(group -> GroupId.unknownVersion(group.getGroupId()))
|
||||||
|
.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,13 @@ public class ServiceConfig {
|
|||||||
final var attachmentBackfill = !isPrimaryDevice;
|
final var attachmentBackfill = !isPrimaryDevice;
|
||||||
final var spqr = true;
|
final var spqr = true;
|
||||||
final var usernameSyncChangeMessage = !isPrimaryDevice;
|
final var usernameSyncChangeMessage = !isPrimaryDevice;
|
||||||
return new AccountAttributes.Capabilities(true, true, attachmentBackfill, spqr, usernameSyncChangeMessage);
|
final var optionalPhoneNumber = !isPrimaryDevice;
|
||||||
|
return new AccountAttributes.Capabilities(true,
|
||||||
|
true,
|
||||||
|
attachmentBackfill,
|
||||||
|
spqr,
|
||||||
|
usernameSyncChangeMessage,
|
||||||
|
optionalPhoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevic
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@ -399,13 +400,17 @@ public class AccountHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void reserveUsername(final List<Username> candidates) throws IOException {
|
private void reserveUsername(final List<Username> candidates) throws IOException {
|
||||||
final var candidateHashes = new ArrayList<String>();
|
final var candidateHashes = new ArrayList<byte[]>();
|
||||||
for (final var candidate : candidates) {
|
for (final var candidate : candidates) {
|
||||||
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
|
candidateHashes.add(candidate.getHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
final var usernameHash = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||||
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
final var hashIndex = candidateHashes.stream()
|
||||||
|
.filter(candidateHash -> Arrays.equals(candidateHash, usernameHash))
|
||||||
|
.findFirst()
|
||||||
|
.map(candidateHashes::indexOf)
|
||||||
|
.orElse(-1);
|
||||||
if (hashIndex == -1) {
|
if (hashIndex == -1) {
|
||||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||||
throw new IOException("Unexpected username response");
|
throw new IOException("Unexpected username response");
|
||||||
@ -498,8 +503,7 @@ public class AccountHelper {
|
|||||||
final var usernameLink = account.getUsernameLink();
|
final var usernameLink = account.getUsernameLink();
|
||||||
|
|
||||||
if (usernameLink == null) {
|
if (usernameLink == null) {
|
||||||
handleResponseException(dependencies.getAccountApi()
|
handleResponseException(dependencies.getAccountApi().reserveUsername(List.of(username.getHash())));
|
||||||
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
|
|
||||||
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
||||||
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||||
account.setUsernameLink(linkComponents);
|
account.setUsernameLink(linkComponents);
|
||||||
@ -534,19 +538,19 @@ public class AccountHelper {
|
|||||||
|
|
||||||
public void setDeviceName(String deviceName) {
|
public void setDeviceName(String deviceName) {
|
||||||
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
||||||
account.setEncryptedDeviceName(encryptedDeviceName);
|
account.setEncryptedDeviceName(Base64.encodeWithoutPadding(encryptedDeviceName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
public void setDeviceName(int deviceId, String deviceName) throws IOException {
|
||||||
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
|
||||||
handleResponseException(dependencies.getLinkDeviceApi().setDeviceName(encryptedDeviceName, deviceId));
|
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
|
||||||
|
.setDeviceName(encryptedDeviceName, deviceId, cont));
|
||||||
context.getSyncHelper().sendDeviceNameChange(deviceId);
|
context.getSyncHelper().sendDeviceNameChange(deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getEncryptedDeviceName(final String deviceName) {
|
private byte[] getEncryptedDeviceName(final String deviceName) {
|
||||||
final var identityKey = account.getAciIdentityKeyPair();
|
final var identityKey = account.getAciIdentityKeyPair();
|
||||||
return Base64.encodeWithoutPadding(DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8),
|
return DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), identityKey);
|
||||||
identityKey));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshDeviceName() throws IOException {
|
public void refreshDeviceName() throws IOException {
|
||||||
@ -585,7 +589,8 @@ public class AccountHelper {
|
|||||||
account.getOrCreatePinMasterKey(),
|
account.getOrCreatePinMasterKey(),
|
||||||
account.getOrCreateMediaRootBackupKey(),
|
account.getOrCreateMediaRootBackupKey(),
|
||||||
verificationCode.getVerificationCode(),
|
verificationCode.getVerificationCode(),
|
||||||
null));
|
null,
|
||||||
|
account.getAuthCredentialSalt()));
|
||||||
account.setMultiDevice(true);
|
account.setMultiDevice(true);
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
}
|
}
|
||||||
@ -601,16 +606,14 @@ public class AccountHelper {
|
|||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
||||||
handleResponseException(dependencies.getAccountApi()
|
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
||||||
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRegistrationPin(String pin) throws IOException {
|
public void setRegistrationPin(String pin) throws IOException {
|
||||||
var masterKey = account.getOrCreatePinMasterKey();
|
var masterKey = account.getOrCreatePinMasterKey();
|
||||||
|
|
||||||
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
||||||
handleResponseException(dependencies.getAccountApi()
|
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
|
||||||
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
|
||||||
|
|
||||||
account.setRegistrationLockPin(pin);
|
account.setRegistrationLockPin(pin);
|
||||||
updateAccountAttributes();
|
updateAccountAttributes();
|
||||||
|
|||||||
@ -85,12 +85,18 @@ public class ContactHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setContactBlocked(RecipientId recipientId, boolean blocked) {
|
public void setContactBlocked(RecipientId recipientId, boolean blocked) {
|
||||||
|
setContactBlocked(recipientId, blocked, blocked ? System.currentTimeMillis() : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContactBlocked(RecipientId recipientId, boolean blocked, long blockedAt) {
|
||||||
var contact = account.getContactStore().getContact(recipientId);
|
var contact = account.getContactStore().getContact(recipientId);
|
||||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
if (blocked) {
|
if (blocked) {
|
||||||
builder.withIsProfileSharingEnabled(false);
|
builder.withIsProfileSharingEnabled(false);
|
||||||
}
|
}
|
||||||
account.getContactStore().storeContact(recipientId, builder.withIsBlocked(blocked).build());
|
account.getContactStore()
|
||||||
|
.storeContact(recipientId,
|
||||||
|
builder.withIsBlocked(blocked).withBlockedAt(blocked ? blockedAt : 0).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
public void setContactProfileSharing(RecipientId recipientId, boolean profileSharing) {
|
||||||
|
|||||||
@ -363,6 +363,28 @@ public class GroupHelper {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SendGroupMessageResults terminateGroup(final GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
||||||
|
final var group = getGroupForUpdating(groupId);
|
||||||
|
if (!(group instanceof GroupInfoV2)) {
|
||||||
|
throw new IOException("Terminating a group is only supported for Signal group v2 groups.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SendGroupMessageResults results;
|
||||||
|
try {
|
||||||
|
results = terminateGroupV2((GroupInfoV2) group);
|
||||||
|
} catch (ConflictException e) {
|
||||||
|
// Detected conflicting update, refreshing group and trying again
|
||||||
|
results = terminateGroupV2((GroupInfoV2) getGroup(groupId, true));
|
||||||
|
}
|
||||||
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendGroupMessageResults terminateGroupV2(final GroupInfoV2 group) throws IOException {
|
||||||
|
final var groupGroupChangePair = context.getGroupV2Helper().terminateGroup(group);
|
||||||
|
return sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||||
|
}
|
||||||
|
|
||||||
public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException {
|
public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException {
|
||||||
var group = getGroupForUpdating(groupId);
|
var group = getGroupForUpdating(groupId);
|
||||||
|
|
||||||
@ -437,12 +459,21 @@ public class GroupHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
|
public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException {
|
||||||
|
setGroupBlocked(groupId, blocked, blocked ? System.currentTimeMillis() : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroupBlocked(
|
||||||
|
final GroupId groupId,
|
||||||
|
final boolean blocked,
|
||||||
|
final long blockedAt
|
||||||
|
) throws GroupNotFoundException {
|
||||||
var group = getGroup(groupId);
|
var group = getGroup(groupId);
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
throw new GroupNotFoundException(groupId);
|
throw new GroupNotFoundException(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
group.setBlocked(blocked);
|
group.setBlocked(blocked);
|
||||||
|
group.setBlockedAt(blocked ? blockedAt : 0);
|
||||||
account.getGroupStore().updateGroup(group);
|
account.getGroupStore().updateGroup(group);
|
||||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -543,6 +543,14 @@ class GroupV2Helper {
|
|||||||
return commitChange(groupInfoV2, change);
|
return commitChange(groupInfoV2, change);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pair<DecryptedGroup, GroupChangeResponse> terminateGroup(
|
||||||
|
GroupInfoV2 groupInfoV2
|
||||||
|
) throws IOException {
|
||||||
|
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||||
|
final var change = groupOperations.createTerminateGroup();
|
||||||
|
return commitChange(groupInfoV2, change);
|
||||||
|
}
|
||||||
|
|
||||||
Pair<DecryptedGroup, GroupChangeResponse> setMemberLabels(
|
Pair<DecryptedGroup, GroupChangeResponse> setMemberLabels(
|
||||||
GroupInfoV2 groupInfoV2,
|
GroupInfoV2 groupInfoV2,
|
||||||
String labelEmoji,
|
String labelEmoji,
|
||||||
|
|||||||
@ -626,13 +626,12 @@ public final class IncomingMessageHandler {
|
|||||||
for (var individual : blockedListMessage.individuals) {
|
for (var individual : blockedListMessage.individuals) {
|
||||||
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
||||||
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
||||||
context.getContactHelper().setContactBlocked(recipientId, true);
|
context.getContactHelper().setContactBlocked(recipientId, true, individual.getBlockedAt());
|
||||||
}
|
}
|
||||||
for (var groupId : blockedListMessage.groupIds.stream()
|
for (var group : blockedListMessage.groups) {
|
||||||
.map(GroupId::unknownVersion)
|
final var groupId = GroupId.unknownVersion(group.getGroupId());
|
||||||
.collect(Collectors.toSet())) {
|
|
||||||
try {
|
try {
|
||||||
context.getGroupHelper().setGroupBlocked(groupId, true);
|
context.getGroupHelper().setGroupBlocked(groupId, true, group.getBlockedAt());
|
||||||
} catch (GroupNotFoundException e) {
|
} catch (GroupNotFoundException e) {
|
||||||
logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
|
logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}",
|
||||||
groupId.toBase64());
|
groupId.toBase64());
|
||||||
@ -837,6 +836,15 @@ public final class IncomingMessageHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (group.isTerminated()) {
|
||||||
|
return message == null
|
||||||
|
|| message.getBody().isPresent()
|
||||||
|
|| message.getAttachments().isPresent()
|
||||||
|
|| message.getQuote().isPresent()
|
||||||
|
|| message.getPreviews().isPresent()
|
||||||
|
|| message.getMentions().isPresent()
|
||||||
|
|| message.getSticker().isPresent();
|
||||||
|
}
|
||||||
if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
|
if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
|
||||||
return message == null
|
return message == null
|
||||||
|| message.getBody().isPresent()
|
|| message.getBody().isPresent()
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
|||||||
import org.signal.core.models.ServiceId.ACI;
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.messages.EnvelopeResponse;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||||
@ -147,15 +148,19 @@ public class ReceiveHelper {
|
|||||||
logger.debug("Retrieved {} envelopes!", batch.size());
|
logger.debug("Retrieved {} envelopes!", batch.size());
|
||||||
isWaitingForMessage = false;
|
isWaitingForMessage = false;
|
||||||
for (final var it : batch) {
|
for (final var it : batch) {
|
||||||
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(it.getEnvelope(),
|
if (it instanceof EnvelopeResponse.Unparseable) {
|
||||||
it.getServerDeliveredTimestamp());
|
logger.warn("Received unparseable envelope from server, ignoring.");
|
||||||
final var sourceServiceId = envelope1.getSourceServiceId();
|
} else if (it instanceof EnvelopeResponse.Parsed parsed) {
|
||||||
final var recipientId = sourceServiceId == null
|
SignalServiceEnvelope envelope1 = new SignalServiceEnvelope(parsed.getEnvelope(),
|
||||||
? null
|
parsed.getServerDeliveredTimestamp());
|
||||||
: account.getRecipientResolver().resolveRecipient(sourceServiceId);
|
final var sourceServiceId = envelope1.getSourceServiceId();
|
||||||
logger.trace("Storing new message from {}", recipientId);
|
final var recipientId = sourceServiceId == null
|
||||||
// store message on disk, before acknowledging receipt to the server
|
? null
|
||||||
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
: account.getRecipientResolver().resolveRecipient(sourceServiceId);
|
||||||
|
logger.trace("Storing new message from {}", recipientId);
|
||||||
|
// store message on disk, before acknowledging receipt to the server
|
||||||
|
cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
signalWebSocket.sendAck(it);
|
signalWebSocket.sendAck(it);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -167,6 +172,9 @@ public class ReceiveHelper {
|
|||||||
backOffCounter = 0;
|
backOffCounter = 0;
|
||||||
|
|
||||||
if (queueNotEmpty) {
|
if (queueNotEmpty) {
|
||||||
|
if (cachedMessage[0] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (remainingMessages > 0) {
|
if (remainingMessages > 0) {
|
||||||
remainingMessages -= 1;
|
remainingMessages -= 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -567,7 +567,7 @@ public class SendHelper {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
|
private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException {
|
||||||
var g = context.getGroupHelper().getGroup(groupId);
|
var g = context.getGroupHelper().getGroup(groupId);
|
||||||
if (g == null) {
|
if (g == null) {
|
||||||
throw new GroupNotFoundException(groupId);
|
throw new GroupNotFoundException(groupId);
|
||||||
@ -575,6 +575,10 @@ public class SendHelper {
|
|||||||
if (!g.isMember(account.getSelfRecipientId())) {
|
if (!g.isMember(account.getSelfRecipientId())) {
|
||||||
throw new NotAGroupMemberException(groupId, g.getTitle());
|
throw new NotAGroupMemberException(groupId, g.getTitle());
|
||||||
}
|
}
|
||||||
|
if (g.isTerminated()) {
|
||||||
|
// Other clients drop messages sent to a terminated group.
|
||||||
|
throw new GroupSendingNotAllowedException(groupId, g.getTitle());
|
||||||
|
}
|
||||||
if (!g.isProfileSharingEnabled()) {
|
if (!g.isProfileSharingEnabled()) {
|
||||||
g.setProfileSharingEnabled(true);
|
g.setProfileSharingEnabled(true);
|
||||||
account.getGroupStore().updateGroup(g);
|
account.getGroupStore().updateGroup(g);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import org.asamk.signal.manager.syncStorage.ContactRecordProcessor;
|
|||||||
import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor;
|
import org.asamk.signal.manager.syncStorage.GroupV1RecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.GroupV2RecordProcessor;
|
import org.asamk.signal.manager.syncStorage.GroupV2RecordProcessor;
|
||||||
import org.asamk.signal.manager.syncStorage.StickerPackRecordProcessor;
|
import org.asamk.signal.manager.syncStorage.StickerPackRecordProcessor;
|
||||||
|
import org.asamk.signal.manager.syncStorage.StorageSyncLoopDetector;
|
||||||
import org.asamk.signal.manager.syncStorage.StorageSyncModels;
|
import org.asamk.signal.manager.syncStorage.StorageSyncModels;
|
||||||
import org.asamk.signal.manager.syncStorage.StorageSyncValidations;
|
import org.asamk.signal.manager.syncStorage.StorageSyncValidations;
|
||||||
import org.asamk.signal.manager.syncStorage.WriteOperationResult;
|
import org.asamk.signal.manager.syncStorage.WriteOperationResult;
|
||||||
@ -61,11 +62,13 @@ public class StorageHelper {
|
|||||||
private final SignalAccount account;
|
private final SignalAccount account;
|
||||||
private final SignalDependencies dependencies;
|
private final SignalDependencies dependencies;
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
private final StorageSyncLoopDetector storageSyncLoopDetector;
|
||||||
|
|
||||||
public StorageHelper(final Context context) {
|
public StorageHelper(final Context context) {
|
||||||
this.account = context.getAccount();
|
this.account = context.getAccount();
|
||||||
this.dependencies = context.getDependencies();
|
this.dependencies = context.getDependencies();
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
this.storageSyncLoopDetector = new StorageSyncLoopDetector(account::isMultiDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void syncDataWithStorage() throws IOException {
|
public void syncDataWithStorage() throws IOException {
|
||||||
@ -84,6 +87,7 @@ public class StorageHelper {
|
|||||||
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
||||||
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
||||||
localManifestVersion);
|
localManifestVersion);
|
||||||
|
final var fetchedRemoteManifest = result instanceof ManifestIfDifferentVersionResult.DifferentVersion;
|
||||||
|
|
||||||
var needsForcePush = false;
|
var needsForcePush = false;
|
||||||
final var remoteManifest = switch (result) {
|
final var remoteManifest = switch (result) {
|
||||||
@ -146,7 +150,10 @@ public class StorageHelper {
|
|||||||
needsForcePush = true;
|
needsForcePush = true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
needsMultiDeviceSync = writeToStorage(storageKey,
|
||||||
|
remoteManifest,
|
||||||
|
needsForcePush,
|
||||||
|
fetchedRemoteManifest);
|
||||||
} catch (RetryLaterException e) {
|
} catch (RetryLaterException e) {
|
||||||
// TODO retry later
|
// TODO retry later
|
||||||
return;
|
return;
|
||||||
@ -288,7 +295,8 @@ public class StorageHelper {
|
|||||||
private boolean writeToStorage(
|
private boolean writeToStorage(
|
||||||
final StorageKey storageKey,
|
final StorageKey storageKey,
|
||||||
final SignalStorageManifest remoteManifest,
|
final SignalStorageManifest remoteManifest,
|
||||||
final boolean needsForcePush
|
final boolean needsForcePush,
|
||||||
|
final boolean fetchedRemoteManifest
|
||||||
) throws IOException, RetryLaterException {
|
) throws IOException, RetryLaterException {
|
||||||
final WriteOperationResult remoteWriteOperation;
|
final WriteOperationResult remoteWriteOperation;
|
||||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||||
@ -326,6 +334,19 @@ public class StorageHelper {
|
|||||||
|
|
||||||
if (remoteWriteOperation.isEmpty()) {
|
if (remoteWriteOperation.isEmpty()) {
|
||||||
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
||||||
|
storageSyncLoopDetector.onConverged();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var loopCheck = storageSyncLoopDetector.onWriteAttempt(remoteWriteOperation,
|
||||||
|
fetchedRemoteManifest,
|
||||||
|
false);
|
||||||
|
if (loopCheck instanceof StorageSyncLoopDetector.Decision.Denied denied) {
|
||||||
|
logger.warn(
|
||||||
|
"Skipping remote write, another device is likely undoing it. Cause: {}, level: {}. WriteOperationResult :: {}",
|
||||||
|
denied.cause(),
|
||||||
|
denied.level(),
|
||||||
|
remoteWriteOperation);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,11 +365,18 @@ public class StorageHelper {
|
|||||||
remoteWriteOperation.deletes());
|
remoteWriteOperation.deletes());
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case WriteStorageRecordsResult.ConflictError ignored -> {
|
case WriteStorageRecordsResult.ConflictError ignored -> {
|
||||||
|
storageSyncLoopDetector.onWriteFailed();
|
||||||
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||||
throw new RetryLaterException();
|
throw new RetryLaterException();
|
||||||
}
|
}
|
||||||
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
case WriteStorageRecordsResult.NetworkError networkError -> {
|
||||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
storageSyncLoopDetector.onWriteFailed();
|
||||||
|
throw networkError.getException();
|
||||||
|
}
|
||||||
|
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> {
|
||||||
|
storageSyncLoopDetector.onWriteFailed();
|
||||||
|
throw statusCodeError.getException();
|
||||||
|
}
|
||||||
case WriteStorageRecordsResult.Success ignored -> {
|
case WriteStorageRecordsResult.Success ignored -> {
|
||||||
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
||||||
storeManifestLocally(remoteWriteOperation.manifest());
|
storeManifestLocally(remoteWriteOperation.manifest());
|
||||||
@ -424,7 +452,7 @@ public class StorageHelper {
|
|||||||
final var stickerPacks = account.getStickerStore()
|
final var stickerPacks = account.getStickerStore()
|
||||||
.getStickerPacks(connection)
|
.getStickerPacks(connection)
|
||||||
.stream()
|
.stream()
|
||||||
.filter(pack -> pack.isInstalled() || pack.deletedTimestamp() > 0)
|
.filter(pack -> pack.storageId() != null)
|
||||||
.toList();
|
.toList();
|
||||||
newStickerPackStorageIds = generateStickerPackStorageIds(stickerPacks);
|
newStickerPackStorageIds = generateStickerPackStorageIds(stickerPacks);
|
||||||
for (final var stickerPack : stickerPacks) {
|
for (final var stickerPack : stickerPacks) {
|
||||||
@ -688,6 +716,13 @@ public class StorageHelper {
|
|||||||
final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
|
final var contactRecordProcessor = new ContactRecordProcessor(account, connection, context.getJobExecutor());
|
||||||
final var stickerPackRecordProcessor = new StickerPackRecordProcessor(account, connection);
|
final var stickerPackRecordProcessor = new StickerPackRecordProcessor(account, connection);
|
||||||
|
|
||||||
|
final var contactRecords = records.stream()
|
||||||
|
.filter(record -> record.getProto().contact != null)
|
||||||
|
.map(record -> StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
||||||
|
record.getId()))
|
||||||
|
.toList();
|
||||||
|
contactRecordProcessor.prepare(contactRecords);
|
||||||
|
|
||||||
for (final var record : records) {
|
for (final var record : records) {
|
||||||
if (record.getProto().account != null) {
|
if (record.getProto().account != null) {
|
||||||
logger.debug("Reading record {} of type account", record.getId());
|
logger.debug("Reading record {} of type account", record.getId());
|
||||||
|
|||||||
@ -237,18 +237,19 @@ public class SyncHelper {
|
|||||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
||||||
if (address.aci().isPresent() || address.number().isPresent()) {
|
if (address.aci().isPresent() || address.number().isPresent()) {
|
||||||
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
||||||
address.number().orElse(null)));
|
address.number().orElse(null),
|
||||||
|
record.second().blockedAt()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var groupIds = new ArrayList<byte[]>();
|
var groups = new ArrayList<BlockedListMessage.Group>();
|
||||||
for (var record : account.getGroupStore().getGroups()) {
|
for (var record : account.getGroupStore().getGroups()) {
|
||||||
if (record.isBlocked()) {
|
if (record.isBlocked()) {
|
||||||
groupIds.add(record.getGroupId().serialize());
|
groups.add(new BlockedListMessage.Group(record.getGroupId().serialize(), record.getBlockedAt()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return context.getSendHelper()
|
return context.getSendHelper()
|
||||||
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
|
.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groups)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public SendMessageResult sendVerifiedMessage(
|
public SendMessageResult sendVerifiedMessage(
|
||||||
|
|||||||
@ -610,6 +610,11 @@ public class ManagerImpl implements Manager {
|
|||||||
context.getGroupHelper().deleteGroup(groupId);
|
context.getGroupHelper().deleteGroup(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendGroupMessageResults terminateGroup(GroupId groupId) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
||||||
|
return context.getGroupHelper().terminateGroup(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
String name,
|
String name,
|
||||||
@ -912,6 +917,10 @@ public class ManagerImpl implements Manager {
|
|||||||
if (!(groupInfo instanceof GroupInfoV2 groupInfoV2)) {
|
if (!(groupInfo instanceof GroupInfoV2 groupInfoV2)) {
|
||||||
throw new IOException("Stories are only supported for V2 groups");
|
throw new IOException("Stories are only supported for V2 groups");
|
||||||
}
|
}
|
||||||
|
if (groupInfoV2.isTerminated()) {
|
||||||
|
// Other clients drop messages sent to a terminated group.
|
||||||
|
throw new IOException("Cannot send a story to a group that has been terminated");
|
||||||
|
}
|
||||||
|
|
||||||
final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment);
|
final var uploadedAttachment = context.getAttachmentHelper().uploadAttachment(attachment);
|
||||||
final var groupContext = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
|
final var groupContext = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
|
||||||
|
|||||||
@ -214,6 +214,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager, Closeable {
|
|||||||
pniIdentity,
|
pniIdentity,
|
||||||
profileKey,
|
profileKey,
|
||||||
accountEntropyPool,
|
accountEntropyPool,
|
||||||
|
msg.authCredentialSalt == null ? null : msg.authCredentialSalt.toByteArray(),
|
||||||
mediaRootBackupKey);
|
mediaRootBackupKey);
|
||||||
|
|
||||||
if (msg.readReceipts != null) {
|
if (msg.readReceipts != null) {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import java.util.UUID;
|
|||||||
public class AccountDatabase extends Database {
|
public class AccountDatabase extends Database {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||||
private static final long DATABASE_VERSION = 30;
|
private static final long DATABASE_VERSION = 31;
|
||||||
|
|
||||||
private AccountDatabase(final HikariDataSource dataSource) {
|
private AccountDatabase(final HikariDataSource dataSource) {
|
||||||
super(logger, DATABASE_VERSION, dataSource);
|
super(logger, DATABASE_VERSION, dataSource);
|
||||||
@ -643,6 +643,16 @@ public class AccountDatabase extends Database {
|
|||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 31) {
|
||||||
|
logger.debug("Updating database: Add blocked-at timestamps");
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("""
|
||||||
|
ALTER TABLE recipient ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE group_v1 ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE group_v2 ADD blocked_at INTEGER NOT NULL DEFAULT 0;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void createUuidMappingTable(
|
private static void createUuidMappingTable(
|
||||||
|
|||||||
@ -116,7 +116,7 @@ public class SignalAccount implements Closeable {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||||
|
|
||||||
private static final int MINIMUM_STORAGE_VERSION = 1;
|
private static final int MINIMUM_STORAGE_VERSION = 1;
|
||||||
private static final int CURRENT_STORAGE_VERSION = 10;
|
private static final int CURRENT_STORAGE_VERSION = 11;
|
||||||
|
|
||||||
private final Object LOCK = new Object();
|
private final Object LOCK = new Object();
|
||||||
|
|
||||||
@ -141,6 +141,7 @@ public class SignalAccount implements Closeable {
|
|||||||
private MasterKey pinMasterKey;
|
private MasterKey pinMasterKey;
|
||||||
private StorageKey storageKey;
|
private StorageKey storageKey;
|
||||||
private AccountEntropyPool accountEntropyPool;
|
private AccountEntropyPool accountEntropyPool;
|
||||||
|
private byte[] authCredentialSalt;
|
||||||
private MediaRootBackupKey mediaRootBackupKey;
|
private MediaRootBackupKey mediaRootBackupKey;
|
||||||
private ProfileKey profileKey;
|
private ProfileKey profileKey;
|
||||||
|
|
||||||
@ -301,6 +302,7 @@ public class SignalAccount implements Closeable {
|
|||||||
final IdentityKeyPair pniIdentity,
|
final IdentityKeyPair pniIdentity,
|
||||||
final ProfileKey profileKey,
|
final ProfileKey profileKey,
|
||||||
final AccountEntropyPool accountEntropyPool,
|
final AccountEntropyPool accountEntropyPool,
|
||||||
|
final byte[] authCredentialSalt,
|
||||||
final MediaRootBackupKey mediaRootBackupKey
|
final MediaRootBackupKey mediaRootBackupKey
|
||||||
) {
|
) {
|
||||||
this.deviceId = 0;
|
this.deviceId = 0;
|
||||||
@ -325,6 +327,7 @@ public class SignalAccount implements Closeable {
|
|||||||
this.pinMasterKey = null;
|
this.pinMasterKey = null;
|
||||||
this.accountEntropyPool = null;
|
this.accountEntropyPool = null;
|
||||||
}
|
}
|
||||||
|
this.authCredentialSalt = authCredentialSalt;
|
||||||
this.mediaRootBackupKey = mediaRootBackupKey;
|
this.mediaRootBackupKey = mediaRootBackupKey;
|
||||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||||
this.setStorageManifest(null);
|
this.setStorageManifest(null);
|
||||||
@ -361,6 +364,7 @@ public class SignalAccount implements Closeable {
|
|||||||
) {
|
) {
|
||||||
this.pinMasterKey = masterKey;
|
this.pinMasterKey = masterKey;
|
||||||
this.accountEntropyPool = null;
|
this.accountEntropyPool = null;
|
||||||
|
this.authCredentialSalt = null;
|
||||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||||
this.setStorageManifest(null);
|
this.setStorageManifest(null);
|
||||||
this.storageKey = null;
|
this.storageKey = null;
|
||||||
@ -526,6 +530,9 @@ public class SignalAccount implements Closeable {
|
|||||||
if (storage.accountEntropyPool != null) {
|
if (storage.accountEntropyPool != null) {
|
||||||
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
||||||
}
|
}
|
||||||
|
if (storage.authCredentialSalt != null) {
|
||||||
|
authCredentialSalt = base64.decode(storage.authCredentialSalt);
|
||||||
|
}
|
||||||
if (storage.mediaRootBackupKey != null) {
|
if (storage.mediaRootBackupKey != null) {
|
||||||
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
||||||
}
|
}
|
||||||
@ -906,6 +913,7 @@ public class SignalAccount implements Closeable {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
contact.blocked,
|
contact.blocked,
|
||||||
|
0,
|
||||||
contact.archived,
|
contact.archived,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -1014,6 +1022,7 @@ public class SignalAccount implements Closeable {
|
|||||||
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
||||||
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
||||||
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
||||||
|
authCredentialSalt == null ? null : base64.encodeToString(authCredentialSalt),
|
||||||
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
||||||
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
||||||
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
||||||
@ -1228,6 +1237,11 @@ public class SignalAccount implements Closeable {
|
|||||||
return pniAccountData.getSignalServiceAccountDataStore();
|
return pniAccountData.getSignalServiceAccountDataStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SignalServiceAccountDataStore pniOrNull() {
|
||||||
|
return getPni() != null ? pniAccountData.getSignalServiceAccountDataStore() : null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isMultiDevice() {
|
public boolean isMultiDevice() {
|
||||||
return SignalAccount.this.isMultiDevice();
|
return SignalAccount.this.isMultiDevice();
|
||||||
@ -1648,6 +1662,10 @@ public class SignalAccount implements Closeable {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getAuthCredentialSalt() {
|
||||||
|
return authCredentialSalt;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRecoveryPassword() {
|
public String getRecoveryPassword() {
|
||||||
final var masterKey = getPinBackedMasterKey();
|
final var masterKey = getPinBackedMasterKey();
|
||||||
if (masterKey == null) {
|
if (masterKey == null) {
|
||||||
@ -1987,6 +2005,7 @@ public class SignalAccount implements Closeable {
|
|||||||
String pinMasterKey,
|
String pinMasterKey,
|
||||||
String storageKey,
|
String storageKey,
|
||||||
String accountEntropyPool,
|
String accountEntropyPool,
|
||||||
|
String authCredentialSalt,
|
||||||
String mediaRootBackupKey,
|
String mediaRootBackupKey,
|
||||||
String profileKey,
|
String profileKey,
|
||||||
String usernameLinkEntropy,
|
String usernameLinkEntropy,
|
||||||
|
|||||||
@ -58,6 +58,10 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
|
|
||||||
public abstract void setBlocked(boolean blocked);
|
public abstract void setBlocked(boolean blocked);
|
||||||
|
|
||||||
|
public abstract long getBlockedAt();
|
||||||
|
|
||||||
|
public abstract void setBlockedAt(long blockedAt);
|
||||||
|
|
||||||
public abstract boolean isProfileSharingEnabled();
|
public abstract boolean isProfileSharingEnabled();
|
||||||
|
|
||||||
public abstract void setProfileSharingEnabled(boolean profileSharingEnabled);
|
public abstract void setProfileSharingEnabled(boolean profileSharingEnabled);
|
||||||
@ -66,6 +70,8 @@ public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 {
|
|||||||
|
|
||||||
public abstract boolean isAnnouncementGroup();
|
public abstract boolean isAnnouncementGroup();
|
||||||
|
|
||||||
|
public abstract boolean isTerminated();
|
||||||
|
|
||||||
public abstract GroupPermission getPermissionAddMember();
|
public abstract GroupPermission getPermissionAddMember();
|
||||||
|
|
||||||
public abstract GroupPermission getPermissionEditDetails();
|
public abstract GroupPermission getPermissionEditDetails();
|
||||||
|
|||||||
@ -24,6 +24,7 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
public String color;
|
public String color;
|
||||||
public int messageExpirationTime;
|
public int messageExpirationTime;
|
||||||
public boolean blocked;
|
public boolean blocked;
|
||||||
|
private long blockedAt;
|
||||||
public boolean archived;
|
public boolean archived;
|
||||||
private byte[] storageRecord;
|
private byte[] storageRecord;
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
final String color,
|
final String color,
|
||||||
final int messageExpirationTime,
|
final int messageExpirationTime,
|
||||||
final boolean blocked,
|
final boolean blocked,
|
||||||
|
final long blockedAt,
|
||||||
final boolean archived,
|
final boolean archived,
|
||||||
final byte[] storageRecord
|
final byte[] storageRecord
|
||||||
) {
|
) {
|
||||||
@ -49,6 +51,7 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
this.color = color;
|
this.color = color;
|
||||||
this.messageExpirationTime = messageExpirationTime;
|
this.messageExpirationTime = messageExpirationTime;
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
|
this.blockedAt = blockedAt;
|
||||||
this.archived = archived;
|
this.archived = archived;
|
||||||
this.storageRecord = storageRecord;
|
this.storageRecord = storageRecord;
|
||||||
}
|
}
|
||||||
@ -91,9 +94,24 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBlocked(final boolean blocked) {
|
public void setBlocked(final boolean blocked) {
|
||||||
|
if (blocked && !this.blocked) {
|
||||||
|
blockedAt = System.currentTimeMillis();
|
||||||
|
} else if (!blocked) {
|
||||||
|
blockedAt = 0;
|
||||||
|
}
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getBlockedAt() {
|
||||||
|
return blockedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockedAt(final long blockedAt) {
|
||||||
|
this.blockedAt = blockedAt;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isProfileSharingEnabled() {
|
public boolean isProfileSharingEnabled() {
|
||||||
return true;
|
return true;
|
||||||
@ -113,6 +131,11 @@ public final class GroupInfoV1 extends GroupInfo {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminated() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupPermission getPermissionAddMember() {
|
public GroupPermission getPermissionAddMember() {
|
||||||
return GroupPermission.EVERY_MEMBER;
|
return GroupPermission.EVERY_MEMBER;
|
||||||
|
|||||||
@ -23,6 +23,7 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
private final GroupMasterKey masterKey;
|
private final GroupMasterKey masterKey;
|
||||||
private final DistributionId distributionId;
|
private final DistributionId distributionId;
|
||||||
private boolean blocked;
|
private boolean blocked;
|
||||||
|
private long blockedAt;
|
||||||
private boolean profileSharingEnabled;
|
private boolean profileSharingEnabled;
|
||||||
private DecryptedGroup group;
|
private DecryptedGroup group;
|
||||||
private byte[] storageRecord;
|
private byte[] storageRecord;
|
||||||
@ -47,6 +48,7 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
final DecryptedGroup group,
|
final DecryptedGroup group,
|
||||||
final DistributionId distributionId,
|
final DistributionId distributionId,
|
||||||
final boolean blocked,
|
final boolean blocked,
|
||||||
|
final long blockedAt,
|
||||||
final boolean profileSharingEnabled,
|
final boolean profileSharingEnabled,
|
||||||
final boolean permissionDenied,
|
final boolean permissionDenied,
|
||||||
final byte[] storageRecord,
|
final byte[] storageRecord,
|
||||||
@ -57,6 +59,7 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
this.group = group;
|
this.group = group;
|
||||||
this.distributionId = distributionId;
|
this.distributionId = distributionId;
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
|
this.blockedAt = blockedAt;
|
||||||
this.profileSharingEnabled = profileSharingEnabled;
|
this.profileSharingEnabled = profileSharingEnabled;
|
||||||
this.permissionDenied = permissionDenied;
|
this.permissionDenied = permissionDenied;
|
||||||
this.storageRecord = storageRecord;
|
this.storageRecord = storageRecord;
|
||||||
@ -186,9 +189,24 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBlocked(final boolean blocked) {
|
public void setBlocked(final boolean blocked) {
|
||||||
|
if (blocked && !this.blocked) {
|
||||||
|
blockedAt = System.currentTimeMillis();
|
||||||
|
} else if (!blocked) {
|
||||||
|
blockedAt = 0;
|
||||||
|
}
|
||||||
this.blocked = blocked;
|
this.blocked = blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getBlockedAt() {
|
||||||
|
return blockedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlockedAt(final long blockedAt) {
|
||||||
|
this.blockedAt = blockedAt;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isProfileSharingEnabled() {
|
public boolean isProfileSharingEnabled() {
|
||||||
return profileSharingEnabled;
|
return profileSharingEnabled;
|
||||||
@ -211,6 +229,11 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||||||
return this.group != null && this.group.isAnnouncementGroup == EnabledState.ENABLED;
|
return this.group != null && this.group.isAnnouncementGroup == EnabledState.ENABLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminated() {
|
||||||
|
return this.group != null && Boolean.TRUE.equals(this.group.terminated);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupPermission getPermissionAddMember() {
|
public GroupPermission getPermissionAddMember() {
|
||||||
final var accessControl = getAccessControl();
|
final var accessControl = getAccessControl();
|
||||||
|
|||||||
@ -63,6 +63,7 @@ public class GroupStore {
|
|||||||
distribution_id BLOB UNIQUE NOT NULL,
|
distribution_id BLOB UNIQUE NOT NULL,
|
||||||
endorsement_expiration_time INTEGER NOT NULL DEFAULT 0,
|
endorsement_expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
|
blocked_at INTEGER NOT NULL DEFAULT 0,
|
||||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||||
permission_denied INTEGER NOT NULL DEFAULT FALSE
|
permission_denied INTEGER NOT NULL DEFAULT FALSE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
@ -83,6 +84,7 @@ public class GroupStore {
|
|||||||
color TEXT,
|
color TEXT,
|
||||||
expiration_time INTEGER NOT NULL DEFAULT 0,
|
expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
|
blocked_at INTEGER NOT NULL DEFAULT 0,
|
||||||
archived INTEGER NOT NULL DEFAULT FALSE
|
archived INTEGER NOT NULL DEFAULT FALSE
|
||||||
) STRICT;
|
) STRICT;
|
||||||
CREATE TABLE group_v1_member (
|
CREATE TABLE group_v1_member (
|
||||||
@ -401,6 +403,7 @@ public class GroupStore {
|
|||||||
deleteGroup(connection, groupInfoV1.getGroupId());
|
deleteGroup(connection, groupInfoV1.getGroupId());
|
||||||
final var groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, recipientResolver);
|
final var groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey, recipientResolver);
|
||||||
groupInfoV2.setBlocked(groupInfoV1.isBlocked());
|
groupInfoV2.setBlocked(groupInfoV1.isBlocked());
|
||||||
|
groupInfoV2.setBlockedAt(groupInfoV1.getBlockedAt());
|
||||||
updateGroup(connection, groupInfoV2);
|
updateGroup(connection, groupInfoV2);
|
||||||
logger.debug("Locally migrated group {} to group v2, id: {}",
|
logger.debug("Locally migrated group {} to group v2, id: {}",
|
||||||
groupInfoV1.getGroupId().toBase64(),
|
groupInfoV1.getGroupId().toBase64(),
|
||||||
@ -614,9 +617,9 @@ public class GroupStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
final var sql = """
|
final var sql = """
|
||||||
INSERT INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, archived, storage_id)
|
INSERT INTO %s (_id, group_id, group_id_v2, name, color, expiration_time, blocked, blocked_at, archived, storage_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, group_id_v2=excluded.group_id_v2, name=excluded.name, color=excluded.color, expiration_time=excluded.expiration_time, blocked=excluded.blocked, archived=excluded.archived, storage_id=excluded.storage_id
|
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, group_id_v2=excluded.group_id_v2, name=excluded.name, color=excluded.color, expiration_time=excluded.expiration_time, blocked=excluded.blocked, blocked_at=excluded.blocked_at, archived=excluded.archived, storage_id=excluded.storage_id
|
||||||
RETURNING _id
|
RETURNING _id
|
||||||
""".formatted(TABLE_GROUP_V1);
|
""".formatted(TABLE_GROUP_V1);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
@ -631,8 +634,9 @@ public class GroupStore {
|
|||||||
statement.setString(5, groupV1.color);
|
statement.setString(5, groupV1.color);
|
||||||
statement.setLong(6, groupV1.getMessageExpirationTimer());
|
statement.setLong(6, groupV1.getMessageExpirationTimer());
|
||||||
statement.setBoolean(7, groupV1.isBlocked());
|
statement.setBoolean(7, groupV1.isBlocked());
|
||||||
statement.setBoolean(8, groupV1.archived);
|
statement.setLong(8, groupV1.getBlockedAt());
|
||||||
statement.setBytes(9, KeyUtils.createRawStorageId());
|
statement.setBoolean(9, groupV1.archived);
|
||||||
|
statement.setBytes(10, KeyUtils.createRawStorageId());
|
||||||
final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
final var generatedKey = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
||||||
|
|
||||||
if (internalId == null) {
|
if (internalId == null) {
|
||||||
@ -658,9 +662,9 @@ public class GroupStore {
|
|||||||
} else if (group instanceof GroupInfoV2 groupV2) {
|
} else if (group instanceof GroupInfoV2 groupV2) {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, permission_denied, storage_id, profile_sharing)
|
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, blocked_at, permission_denied, storage_id, profile_sharing)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, master_key=excluded.master_key, group_data=excluded.group_data, distribution_id=excluded.distribution_id, blocked=excluded.blocked, permission_denied=excluded.permission_denied, storage_id=excluded.storage_id, profile_sharing=excluded.profile_sharing
|
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, master_key=excluded.master_key, group_data=excluded.group_data, distribution_id=excluded.distribution_id, blocked=excluded.blocked, blocked_at=excluded.blocked_at, permission_denied=excluded.permission_denied, storage_id=excluded.storage_id, profile_sharing=excluded.profile_sharing
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V2);
|
).formatted(TABLE_GROUP_V2);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
@ -678,9 +682,10 @@ public class GroupStore {
|
|||||||
}
|
}
|
||||||
statement.setBytes(5, UuidUtil.toByteArray(groupV2.getDistributionId().asUuid()));
|
statement.setBytes(5, UuidUtil.toByteArray(groupV2.getDistributionId().asUuid()));
|
||||||
statement.setBoolean(6, groupV2.isBlocked());
|
statement.setBoolean(6, groupV2.isBlocked());
|
||||||
statement.setBoolean(7, groupV2.isPermissionDenied());
|
statement.setLong(7, groupV2.getBlockedAt());
|
||||||
statement.setBytes(8, KeyUtils.createRawStorageId());
|
statement.setBoolean(8, groupV2.isPermissionDenied());
|
||||||
statement.setBoolean(9, groupV2.isProfileSharingEnabled());
|
statement.setBytes(9, KeyUtils.createRawStorageId());
|
||||||
|
statement.setBoolean(10, groupV2.isProfileSharingEnabled());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -691,7 +696,7 @@ public class GroupStore {
|
|||||||
private List<GroupInfoV2> getGroupsV2() {
|
private List<GroupInfoV2> getGroupsV2() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V2);
|
).formatted(TABLE_GROUP_V2);
|
||||||
@ -709,7 +714,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
public GroupInfoV2 getGroup(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id = ?
|
WHERE g.group_id = ?
|
||||||
"""
|
"""
|
||||||
@ -743,7 +748,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV2 getGroupV2(Connection connection, StorageId storageId) throws SQLException {
|
public GroupInfoV2 getGroupV2(Connection connection, StorageId storageId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.profile_sharing, g.permission_denied, g.storage_record
|
SELECT g.group_id, g.master_key, g.group_data, g.distribution_id, g.blocked, g.blocked_at, g.profile_sharing, g.permission_denied, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.storage_id = ?
|
WHERE g.storage_id = ?
|
||||||
"""
|
"""
|
||||||
@ -766,6 +771,7 @@ public class GroupStore {
|
|||||||
final var groupData = resultSet.getBytes("group_data");
|
final var groupData = resultSet.getBytes("group_data");
|
||||||
final var distributionId = resultSet.getBytes("distribution_id");
|
final var distributionId = resultSet.getBytes("distribution_id");
|
||||||
final var blocked = resultSet.getBoolean("blocked");
|
final var blocked = resultSet.getBoolean("blocked");
|
||||||
|
final var blockedAt = resultSet.getLong("blocked_at");
|
||||||
final var profileSharingEnabled = resultSet.getBoolean("profile_sharing");
|
final var profileSharingEnabled = resultSet.getBoolean("profile_sharing");
|
||||||
final var permissionDenied = resultSet.getBoolean("permission_denied");
|
final var permissionDenied = resultSet.getBoolean("permission_denied");
|
||||||
final var storageRecord = resultSet.getBytes("storage_record");
|
final var storageRecord = resultSet.getBytes("storage_record");
|
||||||
@ -774,6 +780,7 @@ public class GroupStore {
|
|||||||
groupData == null ? null : DecryptedGroup.ADAPTER.decode(groupData),
|
groupData == null ? null : DecryptedGroup.ADAPTER.decode(groupData),
|
||||||
DistributionId.from(UuidUtil.parseOrThrow(distributionId)),
|
DistributionId.from(UuidUtil.parseOrThrow(distributionId)),
|
||||||
blocked,
|
blocked,
|
||||||
|
blockedAt,
|
||||||
profileSharingEnabled,
|
profileSharingEnabled,
|
||||||
permissionDenied,
|
permissionDenied,
|
||||||
storageRecord,
|
storageRecord,
|
||||||
@ -800,7 +807,7 @@ public class GroupStore {
|
|||||||
private List<GroupInfoV1> getGroupsV1() {
|
private List<GroupInfoV1> getGroupsV1() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1);
|
).formatted(TABLE_GROUP_V1_MEMBER, TABLE_GROUP_V1);
|
||||||
@ -818,7 +825,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException {
|
public GroupInfoV1 getGroup(Connection connection, GroupIdV1 groupIdV1) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id = ?
|
WHERE g.group_id = ?
|
||||||
"""
|
"""
|
||||||
@ -852,7 +859,7 @@ public class GroupStore {
|
|||||||
public GroupInfoV1 getGroupV1(Connection connection, StorageId storageId) throws SQLException {
|
public GroupInfoV1 getGroupV1(Connection connection, StorageId storageId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.storage_id = ?
|
WHERE g.storage_id = ?
|
||||||
"""
|
"""
|
||||||
@ -882,6 +889,7 @@ public class GroupStore {
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
final var expirationTime = resultSet.getInt("expiration_time");
|
final var expirationTime = resultSet.getInt("expiration_time");
|
||||||
final var blocked = resultSet.getBoolean("blocked");
|
final var blocked = resultSet.getBoolean("blocked");
|
||||||
|
final var blockedAt = resultSet.getLong("blocked_at");
|
||||||
final var archived = resultSet.getBoolean("archived");
|
final var archived = resultSet.getBoolean("archived");
|
||||||
final var storageRecord = resultSet.getBytes("storage_record");
|
final var storageRecord = resultSet.getBytes("storage_record");
|
||||||
return new GroupInfoV1(GroupId.v1(groupId),
|
return new GroupInfoV1(GroupId.v1(groupId),
|
||||||
@ -891,6 +899,7 @@ public class GroupStore {
|
|||||||
color,
|
color,
|
||||||
expirationTime,
|
expirationTime,
|
||||||
blocked,
|
blocked,
|
||||||
|
blockedAt,
|
||||||
archived,
|
archived,
|
||||||
storageRecord);
|
storageRecord);
|
||||||
}
|
}
|
||||||
@ -902,7 +911,7 @@ public class GroupStore {
|
|||||||
private GroupInfoV1 getGroupV1ByV2Id(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
private GroupInfoV1 getGroupV1ByV2Id(Connection connection, GroupIdV2 groupIdV2) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.archived, g.storage_record
|
SELECT g.group_id, g.group_id_v2, g.name, g.color, (select group_concat(gm.recipient_id) from %s gm where gm.group_id = g._id) as members, g.expiration_time, g.blocked, g.blocked_at, g.archived, g.storage_record
|
||||||
FROM %s g
|
FROM %s g
|
||||||
WHERE g.group_id_v2 = ?
|
WHERE g.group_id_v2 = ?
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -59,6 +59,7 @@ public class LegacyGroupStore {
|
|||||||
g1.color,
|
g1.color,
|
||||||
g1.messageExpirationTime,
|
g1.messageExpirationTime,
|
||||||
g1.blocked,
|
g1.blocked,
|
||||||
|
0,
|
||||||
g1.archived,
|
g1.archived,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
@ -77,6 +78,7 @@ public class LegacyGroupStore {
|
|||||||
loadDecryptedGroupLocked(groupId, groupCachePath),
|
loadDecryptedGroupLocked(groupId, groupCachePath),
|
||||||
g2.distributionId == null ? DistributionId.create() : DistributionId.from(g2.distributionId),
|
g2.distributionId == null ? DistributionId.create() : DistributionId.from(g2.distributionId),
|
||||||
g2.blocked,
|
g2.blocked,
|
||||||
|
0,
|
||||||
true,
|
true,
|
||||||
g2.permissionDenied,
|
g2.permissionDenied,
|
||||||
null,
|
null,
|
||||||
|
|||||||
@ -50,6 +50,7 @@ public class LegacyRecipientStore2 {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
r.contact.blocked,
|
r.contact.blocked,
|
||||||
|
0,
|
||||||
r.contact.archived,
|
r.contact.archived,
|
||||||
r.contact.profileSharingEnabled,
|
r.contact.profileSharingEnabled,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@ -96,6 +96,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
expiration_time_version INTEGER DEFAULT 1 NOT NULL,
|
expiration_time_version INTEGER DEFAULT 1 NOT NULL,
|
||||||
mute_until INTEGER NOT NULL DEFAULT 0,
|
mute_until INTEGER NOT NULL DEFAULT 0,
|
||||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||||
|
blocked_at INTEGER NOT NULL DEFAULT 0,
|
||||||
archived INTEGER NOT NULL DEFAULT FALSE,
|
archived INTEGER NOT NULL DEFAULT FALSE,
|
||||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||||
hide_story INTEGER NOT NULL DEFAULT FALSE,
|
hide_story INTEGER NOT NULL DEFAULT FALSE,
|
||||||
@ -351,7 +352,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
public List<Pair<RecipientId, Contact>> getContacts() {
|
public List<Pair<RecipientId, Contact>> getContacts() {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
|
SELECT r._id, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp
|
||||||
FROM %s r
|
FROM %s r
|
||||||
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
|
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s AND r.hidden = FALSE
|
||||||
"""
|
"""
|
||||||
@ -376,7 +377,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.pni_signature_verified,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -397,7 +398,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.pni_signature_verified,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -447,7 +448,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
r.number, r.aci, r.pni, r.username,
|
r.number, r.aci, r.pni, r.username,
|
||||||
r.profile_key, r.profile_key_credential,
|
r.profile_key, r.profile_key_credential,
|
||||||
r.pni_signature_verified,
|
r.pni_signature_verified,
|
||||||
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
|
r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp,
|
||||||
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
|
||||||
r.discoverable,
|
r.discoverable,
|
||||||
r.storage_record
|
r.storage_record
|
||||||
@ -892,7 +893,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, expiration_time_version = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?, hidden = ?
|
SET given_name = ?, family_name = ?, nick_name = ?, expiration_time = ?, expiration_time_version = ?, mute_until = ?, hide_story = ?, profile_sharing = ?, color = ?, blocked = ?, blocked_at = ?, archived = ?, unregistered_timestamp = ?, nick_name_given_name = ?, nick_name_family_name = ?, note = ?, hidden = ?
|
||||||
WHERE _id = ?
|
WHERE _id = ?
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
@ -907,25 +908,49 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
statement.setBoolean(8, contact != null && contact.isProfileSharingEnabled());
|
statement.setBoolean(8, contact != null && contact.isProfileSharingEnabled());
|
||||||
statement.setString(9, contact == null ? null : contact.color());
|
statement.setString(9, contact == null ? null : contact.color());
|
||||||
statement.setBoolean(10, contact != null && contact.isBlocked());
|
statement.setBoolean(10, contact != null && contact.isBlocked());
|
||||||
statement.setBoolean(11, contact != null && contact.isArchived());
|
statement.setLong(11, contact == null ? 0 : contact.blockedAt());
|
||||||
|
statement.setBoolean(12, contact != null && contact.isArchived());
|
||||||
if (contact == null || contact.unregisteredTimestamp() == null) {
|
if (contact == null || contact.unregisteredTimestamp() == null) {
|
||||||
statement.setNull(12, Types.INTEGER);
|
statement.setNull(13, Types.INTEGER);
|
||||||
} else {
|
} else {
|
||||||
statement.setLong(12, contact.unregisteredTimestamp());
|
statement.setLong(13, contact.unregisteredTimestamp());
|
||||||
}
|
}
|
||||||
statement.setString(13, contact == null ? null : contact.nickNameGivenName());
|
statement.setString(14, contact == null ? null : contact.nickNameGivenName());
|
||||||
statement.setString(14, contact == null ? null : contact.nickNameFamilyName());
|
statement.setString(15, contact == null ? null : contact.nickNameFamilyName());
|
||||||
statement.setString(15, contact == null ? null : contact.note());
|
statement.setString(16, contact == null ? null : contact.note());
|
||||||
statement.setBoolean(16, contact != null && contact.isHidden());
|
statement.setBoolean(17, contact != null && contact.isHidden());
|
||||||
statement.setLong(17, recipientId.id());
|
statement.setLong(18, recipientId.id());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
if (contact != null && contact.unregisteredTimestamp() != null) {
|
if (contact != null && contact.unregisteredTimestamp() != null) {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
markUnregisteredAndSplitIfNecessary(connection, recipientId, contact.unregisteredTimestamp());
|
||||||
}
|
}
|
||||||
rotateStorageId(connection, recipientId);
|
rotateStorageId(connection, recipientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void splitForStorageSyncIfNecessary(final Connection connection, final ACI aci) throws SQLException {
|
||||||
|
final var recipient = findByServiceId(connection, aci);
|
||||||
|
if (recipient.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var recipientId = recipient.get().id();
|
||||||
|
final var address = recipient.get().address();
|
||||||
|
if (address.pni().isEmpty() && address.number().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Splitting {} for storage sync", recipientId);
|
||||||
|
final var splitAddress = new RecipientAddress(Optional.empty(),
|
||||||
|
address.pni(),
|
||||||
|
address.number(),
|
||||||
|
Optional.empty());
|
||||||
|
updateRecipientAddress(connection,
|
||||||
|
recipientId,
|
||||||
|
new RecipientAddress(address.aci(), Optional.empty(), Optional.empty(), address.username()));
|
||||||
|
resolveRecipientTrusted(connection, splitAddress);
|
||||||
|
}
|
||||||
|
|
||||||
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
|
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final Collection<StorageId> storageIds
|
final Collection<StorageId> storageIds
|
||||||
@ -1061,7 +1086,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
if (recipientAddress.get().address().aci().isEmpty() || (
|
if (recipientAddress.get().address().aci().isEmpty() || (
|
||||||
contact != null && contact.unregisteredTimestamp() != null
|
contact != null && contact.unregisteredTimestamp() != null
|
||||||
)) {
|
)) {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
markUnregisteredAndSplitIfNecessary(connection, recipientId, System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1095,7 +1120,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
if (registered) {
|
if (registered) {
|
||||||
markRegistered(connection, recipientId);
|
markRegistered(connection, recipientId);
|
||||||
} else {
|
} else {
|
||||||
markUnregisteredAndSplitIfNecessary(connection, recipientId);
|
markUnregisteredAndSplitIfNecessary(connection, recipientId, System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
connection.commit();
|
connection.commit();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@ -1105,9 +1130,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
|
|
||||||
private void markUnregisteredAndSplitIfNecessary(
|
private void markUnregisteredAndSplitIfNecessary(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final RecipientId recipientId
|
final RecipientId recipientId,
|
||||||
|
final long unregisteredTimestamp
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
markUnregistered(connection, recipientId);
|
markUnregistered(connection, recipientId, unregisteredTimestamp);
|
||||||
final var address = resolveRecipientAddress(connection, recipientId);
|
final var address = resolveRecipientAddress(connection, recipientId);
|
||||||
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
||||||
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
||||||
@ -1154,7 +1180,11 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markUnregistered(final Connection connection, final RecipientId recipientId) throws SQLException {
|
private void markUnregistered(
|
||||||
|
final Connection connection,
|
||||||
|
final RecipientId recipientId,
|
||||||
|
final long unregisteredTimestamp
|
||||||
|
) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
UPDATE %s
|
UPDATE %s
|
||||||
@ -1163,7 +1193,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
"""
|
"""
|
||||||
).formatted(TABLE_RECIPIENT);
|
).formatted(TABLE_RECIPIENT);
|
||||||
try (final var statement = connection.prepareStatement(sql)) {
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
statement.setLong(1, System.currentTimeMillis());
|
statement.setLong(1, unregisteredTimestamp);
|
||||||
statement.setLong(2, recipientId.id());
|
statement.setLong(2, recipientId.id());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
@ -1488,9 +1518,14 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
RecipientId toBeMergedRecipientId
|
RecipientId toBeMergedRecipientId
|
||||||
) throws SQLException {
|
) throws SQLException {
|
||||||
final var contact = getContact(connection, recipientId);
|
final var contact = getContact(connection, recipientId);
|
||||||
|
final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
final var toBeMergedContact = getContact(connection, toBeMergedRecipientId);
|
|
||||||
storeContact(connection, recipientId, toBeMergedContact);
|
storeContact(connection, recipientId, toBeMergedContact);
|
||||||
|
} else if (toBeMergedContact != null) {
|
||||||
|
final var mergedContact = mergeContacts(contact, toBeMergedContact);
|
||||||
|
if (!contact.equals(mergedContact)) {
|
||||||
|
storeContact(connection, recipientId, mergedContact);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final var profileKey = getProfileKey(connection, recipientId);
|
final var profileKey = getProfileKey(connection, recipientId);
|
||||||
@ -1515,6 +1550,24 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
|
recipientsMerged.put(toBeMergedRecipientId.id(), recipientId.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Contact mergeContacts(final Contact primary, final Contact secondary) {
|
||||||
|
final var profileSharingEnabled = primary.isProfileSharingEnabled() || secondary.isProfileSharingEnabled();
|
||||||
|
return Contact.newBuilder(primary)
|
||||||
|
.withGivenName(secondary.givenName())
|
||||||
|
.withFamilyName(secondary.familyName())
|
||||||
|
.withMessageExpirationTime(primary.messageExpirationTime() > 0
|
||||||
|
? primary.messageExpirationTime()
|
||||||
|
: secondary.messageExpirationTime())
|
||||||
|
.withMessageExpirationTimeVersion(Math.max(primary.messageExpirationTimeVersion(),
|
||||||
|
secondary.messageExpirationTimeVersion()))
|
||||||
|
.withMuteUntil(primary.muteUntil() > 0 ? primary.muteUntil() : secondary.muteUntil())
|
||||||
|
.withIsBlocked(primary.isBlocked() || secondary.isBlocked())
|
||||||
|
.withBlockedAt(Math.max(primary.blockedAt(), secondary.blockedAt()))
|
||||||
|
.withIsProfileSharingEnabled(profileSharingEnabled)
|
||||||
|
.withIsHidden(profileSharingEnabled ? false : primary.isHidden())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<RecipientWithAddress> findByNumber(
|
private Optional<RecipientWithAddress> findByNumber(
|
||||||
final Connection connection,
|
final Connection connection,
|
||||||
final String number
|
final String number
|
||||||
@ -1594,7 +1647,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
|
private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
|
||||||
final var sql = (
|
final var sql = (
|
||||||
"""
|
"""
|
||||||
SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp
|
SELECT r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.blocked_at, r.archived, r.hidden, r.unregistered_timestamp
|
||||||
FROM %s r
|
FROM %s r
|
||||||
WHERE r._id = ? AND (%s)
|
WHERE r._id = ? AND (%s)
|
||||||
"""
|
"""
|
||||||
@ -1699,6 +1752,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||||||
resultSet.getLong("mute_until"),
|
resultSet.getLong("mute_until"),
|
||||||
resultSet.getBoolean("hide_story"),
|
resultSet.getBoolean("hide_story"),
|
||||||
resultSet.getBoolean("blocked"),
|
resultSet.getBoolean("blocked"),
|
||||||
|
resultSet.getLong("blocked_at"),
|
||||||
resultSet.getBoolean("archived"),
|
resultSet.getBoolean("archived"),
|
||||||
resultSet.getBoolean("profile_sharing"),
|
resultSet.getBoolean("profile_sharing"),
|
||||||
resultSet.getBoolean("hidden"),
|
resultSet.getBoolean("hidden"),
|
||||||
|
|||||||
@ -410,7 +410,7 @@ public class SessionStore implements SignalServiceSessionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isActive(SessionRecord record) {
|
private static boolean isActive(SessionRecord record) {
|
||||||
return record != null && record.hasSenderChain(0.0);
|
return record != null && record.hasSenderChain();
|
||||||
}
|
}
|
||||||
|
|
||||||
record Key(String address, int deviceId) {}
|
record Key(String address, int deviceId) {}
|
||||||
|
|||||||
@ -298,7 +298,7 @@ public class StickerStore {
|
|||||||
"""
|
"""
|
||||||
SELECT s.pack_id
|
SELECT s.pack_id
|
||||||
FROM %s s
|
FROM %s s
|
||||||
WHERE s.storage_id IS NULL AND (s.installed = TRUE OR s.deleted_timestamp > 0)
|
WHERE s.storage_id IS NULL AND s.installed = TRUE
|
||||||
"""
|
"""
|
||||||
).formatted(TABLE_STICKER);
|
).formatted(TABLE_STICKER);
|
||||||
final var updateSql = (
|
final var updateSql = (
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id
|
|||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@ -56,6 +57,29 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
this.selfNumber = account.getNumber();
|
this.selfNumber = account.getNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void prepare(final Collection<SignalContactRecord> remoteRecords) throws SQLException {
|
||||||
|
for (final var remoteRecord : remoteRecords) {
|
||||||
|
if (isInvalid(remoteRecord)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final var remote = remoteRecord.getProto();
|
||||||
|
final var aci = ACI.parseOrNull(remote.aci, remote.aciBinary);
|
||||||
|
final var pni = PNI.parseOrNull(remote.pni, remote.pniBinary);
|
||||||
|
if (shouldSplitForStorageSync(remote.unregisteredAtTimestamp, aci, pni, remote.e164)) {
|
||||||
|
account.getRecipientStore().splitForStorageSyncIfNecessary(connection, aci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean shouldSplitForStorageSync(
|
||||||
|
final long unregisteredAtTimestamp,
|
||||||
|
final ACI aci,
|
||||||
|
final PNI pni,
|
||||||
|
final String e164
|
||||||
|
) {
|
||||||
|
return unregisteredAtTimestamp > 0 && aci != null && pni == null && e164.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error cases:
|
* Error cases:
|
||||||
* - You can't have a contact record without an ACI or PNI.
|
* - You can't have a contact record without an ACI or PNI.
|
||||||
@ -205,6 +229,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
.identityState(identityState)
|
.identityState(identityState)
|
||||||
.identityKey(identityKey)
|
.identityKey(identityKey)
|
||||||
.blocked(remote.blocked)
|
.blocked(remote.blocked)
|
||||||
|
.blockedAtTimestamp(remote.blockedAtTimestamp)
|
||||||
.whitelisted(remote.whitelisted)
|
.whitelisted(remote.whitelisted)
|
||||||
.archived(remote.archived)
|
.archived(remote.archived)
|
||||||
.markedUnread(remote.markedUnread)
|
.markedUnread(remote.markedUnread)
|
||||||
@ -283,7 +308,9 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
|
final var contactNickGivenName = contact == null ? null : contact.nickNameGivenName();
|
||||||
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
|
final var contactNickFamilyName = contact == null ? null : contact.nickNameFamilyName();
|
||||||
final var contactNote = contact == null ? null : contact.note();
|
final var contactNote = contact == null ? null : contact.note();
|
||||||
|
final var blockedAt = contact == null ? 0 : contact.blockedAt();
|
||||||
if (blocked != contactProto.blocked
|
if (blocked != contactProto.blocked
|
||||||
|
|| blockedAt != contactProto.blockedAtTimestamp
|
||||||
|| profileShared != contactProto.whitelisted
|
|| profileShared != contactProto.whitelisted
|
||||||
|| archived != contactProto.archived
|
|| archived != contactProto.archived
|
||||||
|| hidden != contactProto.hidden
|
|| hidden != contactProto.hidden
|
||||||
@ -301,6 +328,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||||||
logger.debug("Storing new or updated contact {}", recipientId);
|
logger.debug("Storing new or updated contact {}", recipientId);
|
||||||
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||||
final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
|
final var newContact = contactBuilder.withIsBlocked(contactProto.blocked)
|
||||||
|
.withBlockedAt(contactProto.blocked ? contactProto.blockedAtTimestamp : 0)
|
||||||
.withIsProfileSharingEnabled(contactProto.whitelisted)
|
.withIsProfileSharingEnabled(contactProto.whitelisted)
|
||||||
.withIsArchived(contactProto.archived)
|
.withIsArchived(contactProto.archived)
|
||||||
.withIsHidden(contactProto.hidden)
|
.withIsHidden(contactProto.hidden)
|
||||||
|
|||||||
@ -114,6 +114,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||||||
final var group = account.getGroupStore().getOrCreateGroupV1(connection, groupIdV1);
|
final var group = account.getGroupStore().getOrCreateGroupV1(connection, groupIdV1);
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
group.setBlocked(groupV1Proto.blocked);
|
group.setBlocked(groupV1Proto.blocked);
|
||||||
|
group.setBlockedAt(0);
|
||||||
account.getGroupStore().updateGroup(connection, group);
|
account.getGroupStore().updateGroup(connection, group);
|
||||||
account.getGroupStore()
|
account.getGroupStore()
|
||||||
.storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
|
.storeStorageRecord(connection, group.getGroupId(), groupV1Record.getId(), groupV1Proto.encode());
|
||||||
|
|||||||
@ -56,6 +56,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||||||
final var mergedBuilder = remote.newBuilder()
|
final var mergedBuilder = remote.newBuilder()
|
||||||
.masterKey(remote.masterKey)
|
.masterKey(remote.masterKey)
|
||||||
.blocked(remote.blocked)
|
.blocked(remote.blocked)
|
||||||
|
.blockedAtTimestamp(remote.blockedAtTimestamp)
|
||||||
.whitelisted(remote.whitelisted)
|
.whitelisted(remote.whitelisted)
|
||||||
.archived(remote.archived)
|
.archived(remote.archived)
|
||||||
.markedUnread(remote.markedUnread)
|
.markedUnread(remote.markedUnread)
|
||||||
@ -93,6 +94,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||||||
|
|
||||||
final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
|
final var group = account.getGroupStore().getGroupOrPartialMigrate(connection, groupMasterKey);
|
||||||
group.setBlocked(groupV2Proto.blocked);
|
group.setBlocked(groupV2Proto.blocked);
|
||||||
|
group.setBlockedAt(groupV2Proto.blocked ? groupV2Proto.blockedAtTimestamp : 0);
|
||||||
group.setProfileSharingEnabled(groupV2Proto.whitelisted);
|
group.setProfileSharingEnabled(groupV2Proto.whitelisted);
|
||||||
account.getGroupStore().updateGroup(connection, group);
|
account.getGroupStore().updateGroup(connection, group);
|
||||||
account.getGroupStore()
|
account.getGroupStore()
|
||||||
|
|||||||
@ -63,16 +63,17 @@ public class StickerPackRecordProcessor extends DefaultStorageRecordProcessor<Si
|
|||||||
final var remote = remoteRecord.getProto();
|
final var remote = remoteRecord.getProto();
|
||||||
final var local = localRecord.getProto();
|
final var local = localRecord.getProto();
|
||||||
|
|
||||||
final var isRemoteDeleted = remote.deletedAtTimestamp > 0;
|
if (shouldKeepLocalDeletion(remote.deletedAtTimestamp, local.deletedAtTimestamp)) {
|
||||||
final var isLocalDeleted = local.deletedAtTimestamp > 0;
|
|
||||||
|
|
||||||
if (isRemoteDeleted && isLocalDeleted && local.deletedAtTimestamp > remote.deletedAtTimestamp) {
|
|
||||||
return localRecord;
|
return localRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
return remoteRecord;
|
return remoteRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean shouldKeepLocalDeletion(final long remoteDeletedAt, final long localDeletedAt) {
|
||||||
|
return remoteDeletedAt > 0 && localDeletedAt > 0 && localDeletedAt < remoteDeletedAt;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void insertLocal(final SignalStickerPackRecord record) throws SQLException {
|
protected void insertLocal(final SignalStickerPackRecord record) throws SQLException {
|
||||||
account.getStickerStore().upsertFromStorageSync(connection, record);
|
account.getStickerStore().upsertFromStorageSync(connection, record);
|
||||||
|
|||||||
@ -0,0 +1,139 @@
|
|||||||
|
package org.asamk.signal.manager.syncStorage;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.util.LeakyBucket;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
public final class StorageSyncLoopDetector {
|
||||||
|
|
||||||
|
private static final int FINGERPRINT_HISTORY = 3;
|
||||||
|
|
||||||
|
private final BooleanSupplier isMultiDevice;
|
||||||
|
private final List<Integer> recentFingerprints = new ArrayList<>();
|
||||||
|
private final LeakyBucket contentBucket = new LeakyBucket(3,
|
||||||
|
Duration.ofHours(1).toMillis(),
|
||||||
|
new InMemoryBucketState());
|
||||||
|
private final LeakyBucket rateBucket = new LeakyBucket(100,
|
||||||
|
Duration.ofMinutes(10).toMillis(),
|
||||||
|
new InMemoryBucketState());
|
||||||
|
|
||||||
|
public StorageSyncLoopDetector(final BooleanSupplier isMultiDevice) {
|
||||||
|
this.isMultiDevice = isMultiDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Decision onWriteAttempt(
|
||||||
|
final WriteOperationResult write,
|
||||||
|
final boolean fetchedRemoteManifest,
|
||||||
|
final boolean isRetry
|
||||||
|
) {
|
||||||
|
return onWriteAttempt(write, fetchedRemoteManifest, isRetry, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized Decision onWriteAttempt(
|
||||||
|
final WriteOperationResult write,
|
||||||
|
final boolean fetchedRemoteManifest,
|
||||||
|
final boolean isRetry,
|
||||||
|
final long now
|
||||||
|
) {
|
||||||
|
if (!isMultiDevice.getAsBoolean() || isRetry) {
|
||||||
|
return Decision.Allowed.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var fingerprint = fingerprint(write);
|
||||||
|
final var chargeContent = fetchedRemoteManifest && fingerprint != null && recentFingerprints.contains(
|
||||||
|
fingerprint);
|
||||||
|
|
||||||
|
if (chargeContent && !contentBucket.hasRoom(now)) {
|
||||||
|
return new Decision.Denied(Cause.REPEATED_PAYLOAD, contentBucket.level(now));
|
||||||
|
}
|
||||||
|
if (fetchedRemoteManifest && !rateBucket.hasRoom(now)) {
|
||||||
|
return new Decision.Denied(Cause.WRITE_RATE, rateBucket.level(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chargeContent) {
|
||||||
|
contentBucket.use(now);
|
||||||
|
}
|
||||||
|
if (fetchedRemoteManifest) {
|
||||||
|
rateBucket.use(now);
|
||||||
|
}
|
||||||
|
if (fingerprint != null) {
|
||||||
|
remember(fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decision.Allowed.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onWriteFailed() {
|
||||||
|
onWriteFailed(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void onWriteFailed(final long now) {
|
||||||
|
contentBucket.refund(now);
|
||||||
|
rateBucket.refund(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void onConverged() {
|
||||||
|
contentBucket.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void remember(final int fingerprint) {
|
||||||
|
recentFingerprints.remove(Integer.valueOf(fingerprint));
|
||||||
|
recentFingerprints.addFirst(fingerprint);
|
||||||
|
if (recentFingerprints.size() > FINGERPRINT_HISTORY) {
|
||||||
|
recentFingerprints.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Integer fingerprint(final WriteOperationResult write) {
|
||||||
|
if (write.inserts().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return write.inserts()
|
||||||
|
.stream()
|
||||||
|
.map(record -> Arrays.hashCode(record.getProto().encode()))
|
||||||
|
.sorted()
|
||||||
|
.toList()
|
||||||
|
.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class InMemoryBucketState implements LeakyBucket.State {
|
||||||
|
|
||||||
|
private int level;
|
||||||
|
private long levelUpdatedAt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int level() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long levelUpdatedAt() {
|
||||||
|
return levelUpdatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(final int level, final long levelUpdatedAt) {
|
||||||
|
this.level = level;
|
||||||
|
this.levelUpdatedAt = levelUpdatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Cause {
|
||||||
|
REPEATED_PAYLOAD,
|
||||||
|
WRITE_RATE
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed interface Decision {
|
||||||
|
|
||||||
|
enum Allowed implements Decision {
|
||||||
|
INSTANCE
|
||||||
|
}
|
||||||
|
|
||||||
|
record Denied(Cause cause, int level) implements Decision {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -123,6 +123,7 @@ public final class StorageSyncModels {
|
|||||||
.nickname(getNicknameRemoteRecord(recipient.getContact()))
|
.nickname(getNicknameRemoteRecord(recipient.getContact()))
|
||||||
.note(emptyIfNull(recipient.getContact().note()))
|
.note(emptyIfNull(recipient.getContact().note()))
|
||||||
.blocked(recipient.getContact().isBlocked())
|
.blocked(recipient.getContact().isBlocked())
|
||||||
|
.blockedAtTimestamp(recipient.getContact().blockedAt())
|
||||||
.whitelisted(recipient.getContact().isProfileSharingEnabled())
|
.whitelisted(recipient.getContact().isProfileSharingEnabled())
|
||||||
.mutedUntilTimestamp(recipient.getContact().muteUntil())
|
.mutedUntilTimestamp(recipient.getContact().muteUntil())
|
||||||
.hideStory(recipient.getContact().hideStory())
|
.hideStory(recipient.getContact().hideStory())
|
||||||
@ -161,6 +162,7 @@ public final class StorageSyncModels {
|
|||||||
final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
|
final var builder = SignalGroupV2Record.Companion.newBuilder(group.getStorageRecord());
|
||||||
builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
|
builder.masterKey(ByteString.of(group.getMasterKey().serialize()));
|
||||||
builder.blocked(group.isBlocked());
|
builder.blocked(group.isBlocked());
|
||||||
|
builder.blockedAtTimestamp(group.getBlockedAt());
|
||||||
builder.whitelisted(group.isProfileSharingEnabled());
|
builder.whitelisted(group.isProfileSharingEnabled());
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ public class AttachmentUtils {
|
|||||||
height = image.getHeight();
|
height = image.getHeight();
|
||||||
}
|
}
|
||||||
fis.getChannel().position(0);
|
fis.getChannel().position(0);
|
||||||
} catch (IOException e) {
|
} catch (IOException | LinkageError e) {
|
||||||
logger.debug("Failed to probe image dimensions, sending without width/height: {}", e.getMessage());
|
logger.debug("Failed to probe image dimensions, sending without width/height: {}", e.getMessage());
|
||||||
fis.getChannel().position(0);
|
fis.getChannel().position(0);
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ public class AttachmentUtils {
|
|||||||
width = image.getWidth();
|
width = image.getWidth();
|
||||||
height = image.getHeight();
|
height = image.getHeight();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException | LinkageError e) {
|
||||||
logger.debug("Failed to probe image dimensions, sending without width/height: {}", e.getMessage());
|
logger.debug("Failed to probe image dimensions, sending without width/height: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
return new ProbedStream(new ByteArrayInputStream(bytes), width, height);
|
return new ProbedStream(new ByteArrayInputStream(bytes), width, height);
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
package org.asamk.signal.manager.util;
|
||||||
|
|
||||||
|
public final class LeakyBucket {
|
||||||
|
|
||||||
|
private final int capacity;
|
||||||
|
private final long dripIntervalMillis;
|
||||||
|
private final State state;
|
||||||
|
|
||||||
|
public LeakyBucket(final int capacity, final long dripIntervalMillis, final State state) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.dripIntervalMillis = dripIntervalMillis;
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int level(final long now) {
|
||||||
|
return calculateStateForCurrentTime(now).level();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasRoom(final long now) {
|
||||||
|
return level(now) < capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void use(final long now) {
|
||||||
|
final var currentState = calculateStateForCurrentTime(now);
|
||||||
|
state.update(currentState.level() + 1, currentState.levelUpdatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refund(final long now) {
|
||||||
|
final var currentState = calculateStateForCurrentTime(now);
|
||||||
|
state.update(Math.max(currentState.level() - 1, 0), currentState.levelUpdatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
state.update(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Snapshot calculateStateForCurrentTime(final long now) {
|
||||||
|
final var level = state.level();
|
||||||
|
final var levelUpdatedAt = state.levelUpdatedAt();
|
||||||
|
final var elapsed = now - levelUpdatedAt;
|
||||||
|
|
||||||
|
if (level <= 0 || elapsed < 0) {
|
||||||
|
return new Snapshot(0, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
final var drips = elapsed / dripIntervalMillis;
|
||||||
|
return new Snapshot((int) Math.max(level - drips, 0), levelUpdatedAt + dripIntervalMillis * drips);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Snapshot(int level, long levelUpdatedAt) {}
|
||||||
|
|
||||||
|
public interface State {
|
||||||
|
|
||||||
|
int level();
|
||||||
|
|
||||||
|
long levelUpdatedAt();
|
||||||
|
|
||||||
|
void update(int level, long levelUpdatedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package org.asamk.signal.manager.storage.groups;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.Group;
|
||||||
|
import org.asamk.signal.manager.api.GroupId;
|
||||||
|
import org.asamk.signal.manager.groups.GroupUtils;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||||
|
import org.asamk.signal.manager.storage.recipients.TestRecipientId;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.signal.core.models.ServiceId;
|
||||||
|
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
|
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||||
|
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
|
||||||
|
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class GroupInfoTerminatedTest {
|
||||||
|
|
||||||
|
private static final RecipientResolver UNUSED_RESOLVER = new RecipientResolver() {
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveRecipient(final RecipientAddress address) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveRecipient(final long recipientId) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveRecipient(final String identifier) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecipientId resolveRecipient(final ServiceId serviceId) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static GroupMasterKey masterKey() {
|
||||||
|
final var bytes = new byte[32];
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
bytes[i] = (byte) (i + 1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new GroupMasterKey(bytes);
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GroupInfoV2 groupV2(final DecryptedGroup group) {
|
||||||
|
final var masterKey = masterKey();
|
||||||
|
return new GroupInfoV2(GroupUtils.getGroupIdV2(masterKey),
|
||||||
|
masterKey,
|
||||||
|
group,
|
||||||
|
DistributionId.create(),
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
UNUSED_RESOLVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v1GroupsAreNeverTerminated() {
|
||||||
|
final var group = new GroupInfoV1(GroupId.v1(new byte[16]));
|
||||||
|
assertFalse(group.isTerminated());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v2ReadsTerminatedFlagFromDecryptedGroup() {
|
||||||
|
assertTrue(groupV2(new DecryptedGroup.Builder().terminated(true).build()).isTerminated());
|
||||||
|
assertFalse(groupV2(new DecryptedGroup.Builder().terminated(false).build()).isTerminated());
|
||||||
|
assertFalse(groupV2(new DecryptedGroup.Builder().build()).isTerminated());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void v2IsNotTerminatedWhenGroupStateMissing() {
|
||||||
|
final var masterKey = masterKey();
|
||||||
|
final var group = new GroupInfoV2(GroupUtils.getGroupIdV2(masterKey), masterKey, UNUSED_RESOLVER);
|
||||||
|
assertFalse(group.isTerminated());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void groupApiRecordCarriesTerminatedFromModel() {
|
||||||
|
final RecipientId self = TestRecipientId.createTestId(1);
|
||||||
|
|
||||||
|
final var terminated = Group.from(groupV2(new DecryptedGroup.Builder().terminated(true).build()),
|
||||||
|
recipientId -> null,
|
||||||
|
self);
|
||||||
|
assertTrue(terminated.isTerminated());
|
||||||
|
|
||||||
|
final var live = Group.from(groupV2(new DecryptedGroup.Builder().terminated(false).build()),
|
||||||
|
recipientId -> null,
|
||||||
|
self);
|
||||||
|
assertFalse(live.isTerminated());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
package org.asamk.signal.manager.storage.recipients;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.Contact;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
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 RecipientStoreTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mergeContactsUsesAndroidMergePolicy() {
|
||||||
|
final var primary = Contact.newBuilder()
|
||||||
|
.withGivenName("Primary given")
|
||||||
|
.withFamilyName("Primary family")
|
||||||
|
.withNickName("Primary system nickname")
|
||||||
|
.withNickNameGivenName("Primary nickname given")
|
||||||
|
.withNickNameFamilyName("Primary nickname family")
|
||||||
|
.withNote("Primary note")
|
||||||
|
.withColor("Primary color")
|
||||||
|
.withMessageExpirationTimeVersion(2)
|
||||||
|
.withHideStory(true)
|
||||||
|
.withIsBlocked(false)
|
||||||
|
.withBlockedAt(100)
|
||||||
|
.withIsArchived(true)
|
||||||
|
.withIsHidden(true)
|
||||||
|
.withUnregisteredTimestamp(300L)
|
||||||
|
.build();
|
||||||
|
final var secondary = Contact.newBuilder()
|
||||||
|
.withGivenName("Secondary given")
|
||||||
|
.withFamilyName("Secondary family")
|
||||||
|
.withNickName("Secondary system nickname")
|
||||||
|
.withNickNameGivenName("Secondary nickname given")
|
||||||
|
.withNickNameFamilyName("Secondary nickname family")
|
||||||
|
.withNote("Secondary note")
|
||||||
|
.withColor("Secondary color")
|
||||||
|
.withMessageExpirationTime(60)
|
||||||
|
.withMessageExpirationTimeVersion(3)
|
||||||
|
.withMuteUntil(400)
|
||||||
|
.withIsBlocked(true)
|
||||||
|
.withBlockedAt(200)
|
||||||
|
.withIsProfileSharingEnabled(true)
|
||||||
|
.withIsHidden(true)
|
||||||
|
.withUnregisteredTimestamp(500L)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final var merged = RecipientStore.mergeContacts(primary, secondary);
|
||||||
|
|
||||||
|
assertEquals("Secondary given", merged.givenName());
|
||||||
|
assertEquals("Secondary family", merged.familyName());
|
||||||
|
assertEquals("Primary system nickname", merged.nickName());
|
||||||
|
assertEquals("Primary nickname given", merged.nickNameGivenName());
|
||||||
|
assertEquals("Primary nickname family", merged.nickNameFamilyName());
|
||||||
|
assertEquals("Primary note", merged.note());
|
||||||
|
assertEquals("Primary color", merged.color());
|
||||||
|
assertEquals(60, merged.messageExpirationTime());
|
||||||
|
assertEquals(3, merged.messageExpirationTimeVersion());
|
||||||
|
assertEquals(400, merged.muteUntil());
|
||||||
|
assertTrue(merged.hideStory());
|
||||||
|
assertTrue(merged.isBlocked());
|
||||||
|
assertEquals(200, merged.blockedAt());
|
||||||
|
assertTrue(merged.isArchived());
|
||||||
|
assertTrue(merged.isProfileSharingEnabled());
|
||||||
|
assertFalse(merged.isHidden());
|
||||||
|
assertEquals(300L, merged.unregisteredTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mergeContactsPrefersConfiguredPrimaryValues() {
|
||||||
|
final var primary = Contact.newBuilder()
|
||||||
|
.withColor("Primary color")
|
||||||
|
.withMessageExpirationTime(30)
|
||||||
|
.withMessageExpirationTimeVersion(4)
|
||||||
|
.withMuteUntil(100)
|
||||||
|
.withIsHidden(true)
|
||||||
|
.build();
|
||||||
|
final var secondary = Contact.newBuilder()
|
||||||
|
.withColor("Secondary color")
|
||||||
|
.withMessageExpirationTime(60)
|
||||||
|
.withMessageExpirationTimeVersion(3)
|
||||||
|
.withMuteUntil(200)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final var merged = RecipientStore.mergeContacts(primary, secondary);
|
||||||
|
|
||||||
|
assertEquals("Primary color", merged.color());
|
||||||
|
assertEquals(30, merged.messageExpirationTime());
|
||||||
|
assertEquals(4, merged.messageExpirationTimeVersion());
|
||||||
|
assertEquals(100, merged.muteUntil());
|
||||||
|
assertTrue(merged.isHidden());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package org.asamk.signal.manager.syncStorage;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.signal.core.models.ServiceId.ACI;
|
||||||
|
import org.signal.core.models.ServiceId.PNI;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class StorageRecordProcessorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void splitsOnlyUnregisteredAciOnlyRecords() {
|
||||||
|
final var aci = ACI.from(UUID.randomUUID());
|
||||||
|
final var pni = PNI.from(UUID.randomUUID());
|
||||||
|
|
||||||
|
assertTrue(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, null, ""));
|
||||||
|
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(0, aci, null, ""));
|
||||||
|
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, null, null, ""));
|
||||||
|
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, pni, ""));
|
||||||
|
assertFalse(ContactRecordProcessor.shouldSplitForStorageSync(1, aci, null, "+12025550123"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsOlderLocalStickerDeletion() {
|
||||||
|
assertTrue(StickerPackRecordProcessor.shouldKeepLocalDeletion(200, 100));
|
||||||
|
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(100, 200));
|
||||||
|
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(200, 0));
|
||||||
|
assertFalse(StickerPackRecordProcessor.shouldKeepLocalDeletion(0, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,102 @@
|
|||||||
|
package org.asamk.signal.manager.syncStorage;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||||
|
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||||
|
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
|
||||||
|
class StorageSyncLoopDetectorTest {
|
||||||
|
|
||||||
|
private static final long NOW = 1_700_000_000_000L;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repeatedPayloadIsDeniedAfterThreeCharges() {
|
||||||
|
final var detector = new StorageSyncLoopDetector(() -> true);
|
||||||
|
final var write = writeWithInsert(1);
|
||||||
|
|
||||||
|
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
for (var index = 0; index < 3; index++) {
|
||||||
|
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.REPEATED_PAYLOAD, 3),
|
||||||
|
detector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void storageIdsAreExcludedFromPayloadFingerprint() {
|
||||||
|
final var detector = new StorageSyncLoopDetector(() -> true);
|
||||||
|
|
||||||
|
assertAllowed(detector.onWriteAttempt(writeWithInsert(1), true, false, NOW));
|
||||||
|
assertAllowed(detector.onWriteAttempt(writeWithInsert(2), true, false, NOW));
|
||||||
|
assertAllowed(detector.onWriteAttempt(writeWithInsert(3), true, false, NOW));
|
||||||
|
assertAllowed(detector.onWriteAttempt(writeWithInsert(4), true, false, NOW));
|
||||||
|
|
||||||
|
assertInstanceOf(StorageSyncLoopDetector.Decision.Denied.class,
|
||||||
|
detector.onWriteAttempt(writeWithInsert(5), true, false, NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void convergenceClearsTheContentBucket() {
|
||||||
|
final var detector = new StorageSyncLoopDetector(() -> true);
|
||||||
|
final var write = writeWithInsert(1);
|
||||||
|
for (var index = 0; index < 4; index++) {
|
||||||
|
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
detector.onConverged();
|
||||||
|
|
||||||
|
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void failedWritesAreRefunded() {
|
||||||
|
final var detector = new StorageSyncLoopDetector(() -> true);
|
||||||
|
final var write = writeWithInsert(1);
|
||||||
|
|
||||||
|
for (var index = 0; index < 20; index++) {
|
||||||
|
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
detector.onWriteFailed(NOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rateBucketLimitsDeleteOnlyWrites() {
|
||||||
|
final var detector = new StorageSyncLoopDetector(() -> true);
|
||||||
|
final var write = new WriteOperationResult(null, List.of(), List.of(new byte[]{1}));
|
||||||
|
|
||||||
|
for (var index = 0; index < 100; index++) {
|
||||||
|
assertAllowed(detector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(new StorageSyncLoopDetector.Decision.Denied(StorageSyncLoopDetector.Cause.WRITE_RATE, 100),
|
||||||
|
detector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void retriesAndSingleDeviceWritesAreExempt() {
|
||||||
|
final var retryDetector = new StorageSyncLoopDetector(() -> true);
|
||||||
|
final var singleDeviceDetector = new StorageSyncLoopDetector(() -> false);
|
||||||
|
final var write = writeWithInsert(1);
|
||||||
|
|
||||||
|
for (var index = 0; index < 20; index++) {
|
||||||
|
assertAllowed(retryDetector.onWriteAttempt(write, true, true, NOW));
|
||||||
|
assertAllowed(singleDeviceDetector.onWriteAttempt(write, true, false, NOW));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WriteOperationResult writeWithInsert(final int storageIdByte) {
|
||||||
|
final var storageId = StorageId.forType(new byte[]{(byte) storageIdByte}, 99);
|
||||||
|
final var record = new SignalStorageRecord(storageId, new StorageRecord.Builder().build());
|
||||||
|
return new WriteOperationResult(null, List.of(record), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertAllowed(final StorageSyncLoopDetector.Decision decision) {
|
||||||
|
assertEquals(StorageSyncLoopDetector.Decision.Allowed.INSTANCE, decision);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package org.asamk.signal.manager.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class LeakyBucketTest {
|
||||||
|
|
||||||
|
private static final long NOW = 1_700_000_000_000L;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dripsLevelsAndCarriesPartialIntervals() {
|
||||||
|
final var state = new TestState();
|
||||||
|
final var bucket = new LeakyBucket(3, Duration.ofHours(1).toMillis(), state);
|
||||||
|
|
||||||
|
bucket.use(NOW);
|
||||||
|
bucket.use(NOW);
|
||||||
|
bucket.use(NOW);
|
||||||
|
|
||||||
|
assertEquals(2, bucket.level(NOW + Duration.ofMinutes(90).toMillis()));
|
||||||
|
assertEquals(1, bucket.level(NOW + Duration.ofMinutes(121).toMillis()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void refundNeverDropsBelowZero() {
|
||||||
|
final var bucket = new LeakyBucket(1, 1_000, new TestState());
|
||||||
|
|
||||||
|
bucket.refund(NOW);
|
||||||
|
|
||||||
|
assertEquals(0, bucket.level(NOW));
|
||||||
|
assertTrue(bucket.hasRoom(NOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clockMovingBackwardsRefillsTheBucket() {
|
||||||
|
final var bucket = new LeakyBucket(1, 1_000, new TestState());
|
||||||
|
bucket.use(NOW);
|
||||||
|
|
||||||
|
assertTrue(bucket.hasRoom(NOW - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TestState implements LeakyBucket.State {
|
||||||
|
|
||||||
|
private int level;
|
||||||
|
private long levelUpdatedAt;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int level() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long levelUpdatedAt() {
|
||||||
|
return levelUpdatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(final int level, final long levelUpdatedAt) {
|
||||||
|
this.level = level;
|
||||||
|
this.levelUpdatedAt = levelUpdatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -452,6 +452,7 @@ Groups have the following (case-sensitive) properties:
|
|||||||
* IsBlocked<b> : true=member will not receive group messages; false=not blocked
|
* IsBlocked<b> : true=member will not receive group messages; false=not blocked
|
||||||
* IsMember<b> (read-only) : always true (object path exists only for group members)
|
* IsMember<b> (read-only) : always true (object path exists only for group members)
|
||||||
* IsAdmin<b> (read-only) : true=member has admin privileges; false=not admin
|
* IsAdmin<b> (read-only) : true=member has admin privileges; false=not admin
|
||||||
|
* IsTerminated<b> (read-only) : true=group was permanently ended by an admin; nobody (not even an admin) can send messages or start calls, sending is rejected locally
|
||||||
* MessageExpirationTimer<i> : int32 representing message expiration time for group
|
* MessageExpirationTimer<i> : int32 representing message expiration time for group
|
||||||
* Members<as> (read-only) : String array of group members' phone numbers
|
* Members<as> (read-only) : String array of group members' phone numbers
|
||||||
* PendingMembers<as> (read-only) : String array of pending members' phone numbers
|
* PendingMembers<as> (read-only) : String array of pending members' phone numbers
|
||||||
@ -502,6 +503,11 @@ Exceptions: Failure
|
|||||||
quitGroup() -> <>::
|
quitGroup() -> <>::
|
||||||
Exceptions: Failure, LastGroupAdmin
|
Exceptions: Failure, LastGroupAdmin
|
||||||
|
|
||||||
|
terminateGroup() -> <>::
|
||||||
|
Permanently terminate the group for all members (requires admin privileges). Afterwards nobody can send messages or start calls; members keep access to the existing history.
|
||||||
|
|
||||||
|
Exceptions: Failure
|
||||||
|
|
||||||
removeAdmins(recipients<as>) -> <>::
|
removeAdmins(recipients<as>) -> <>::
|
||||||
* recipients : String array of phone numbers
|
* recipients : String array of phone numbers
|
||||||
|
|
||||||
|
|||||||
@ -786,6 +786,16 @@ Specify the recipient group ID in base64 encoding.
|
|||||||
*--delete*::
|
*--delete*::
|
||||||
Delete local group data completely after quitting group.
|
Delete local group data completely after quitting group.
|
||||||
|
|
||||||
|
=== terminateGroup
|
||||||
|
|
||||||
|
Permanently terminate a group for all members.
|
||||||
|
This requires admin privileges.
|
||||||
|
Afterwards no member (not even an admin) can send messages or start calls in the group; members keep access to the existing message history.
|
||||||
|
Only supported for Signal group v2 groups.
|
||||||
|
|
||||||
|
*-g* GROUP, *--group-id* GROUP::
|
||||||
|
Specify the group ID in base64 encoding.
|
||||||
|
|
||||||
=== listGroups
|
=== listGroups
|
||||||
|
|
||||||
Show a list of known groups and related information.
|
Show a list of known groups and related information.
|
||||||
|
|||||||
@ -595,6 +595,7 @@ public interface Signal extends DBusInterface {
|
|||||||
@DBusProperty(name = "IsBlocked", type = Boolean.class)
|
@DBusProperty(name = "IsBlocked", type = Boolean.class)
|
||||||
@DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ)
|
@DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ)
|
||||||
@DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ)
|
@DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ)
|
||||||
|
@DBusProperty(name = "IsTerminated", type = Boolean.class, access = DBusProperty.Access.READ)
|
||||||
@DBusProperty(name = "MessageExpirationTimer", type = Integer.class)
|
@DBusProperty(name = "MessageExpirationTimer", type = Integer.class)
|
||||||
@DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ)
|
@DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ)
|
||||||
@DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ)
|
@DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ)
|
||||||
@ -611,6 +612,8 @@ public interface Signal extends DBusInterface {
|
|||||||
|
|
||||||
void deleteGroup() throws Error.Failure;
|
void deleteGroup() throws Error.Failure;
|
||||||
|
|
||||||
|
void terminateGroup() throws Error.Failure;
|
||||||
|
|
||||||
void addMembers(List<String> recipients) throws Error.Failure;
|
void addMembers(List<String> recipients) throws Error.Failure;
|
||||||
|
|
||||||
void removeMembers(List<String> recipients) throws Error.Failure;
|
void removeMembers(List<String> recipients) throws Error.Failure;
|
||||||
|
|||||||
@ -61,6 +61,7 @@ public class Commands {
|
|||||||
addCommand(new SubmitRateLimitChallengeCommand());
|
addCommand(new SubmitRateLimitChallengeCommand());
|
||||||
addCommand(new StartChangeNumberCommand());
|
addCommand(new StartChangeNumberCommand());
|
||||||
addCommand(new StartLinkCommand());
|
addCommand(new StartLinkCommand());
|
||||||
|
addCommand(new TerminateGroupCommand());
|
||||||
addCommand(new TrustCommand());
|
addCommand(new TrustCommand());
|
||||||
addCommand(new UnblockCommand());
|
addCommand(new UnblockCommand());
|
||||||
addCommand(new UnregisterCommand());
|
addCommand(new UnregisterCommand());
|
||||||
|
|||||||
@ -79,12 +79,13 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
|||||||
final var groupInviteLink = group.groupInviteLinkUrl();
|
final var groupInviteLink = group.groupInviteLinkUrl();
|
||||||
|
|
||||||
writer.println(
|
writer.println(
|
||||||
"Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Banned: {} Message expiration: {} Link: {}",
|
"Id: {} Name: {} Description: {} Active: {} Blocked: {} Terminated: {} Members: {} Pending members: {} Requesting members: {} Banned: {} Message expiration: {} Link: {}",
|
||||||
group.groupId().toBase64(),
|
group.groupId().toBase64(),
|
||||||
group.title(),
|
group.title(),
|
||||||
group.description(),
|
group.description(),
|
||||||
group.isMember(),
|
group.isMember(),
|
||||||
group.isBlocked(),
|
group.isBlocked(),
|
||||||
|
group.isTerminated(),
|
||||||
resolveMembers(group.members()),
|
resolveMembers(group.members()),
|
||||||
resolveMemberAddress(group.pendingMembers()),
|
resolveMemberAddress(group.pendingMembers()),
|
||||||
resolveMemberAddress(group.requestingMembers()),
|
resolveMemberAddress(group.requestingMembers()),
|
||||||
@ -92,11 +93,12 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
|||||||
group.messageExpirationTimer() == 0 ? "disabled" : group.messageExpirationTimer() + "s",
|
group.messageExpirationTimer() == 0 ? "disabled" : group.messageExpirationTimer() + "s",
|
||||||
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
|
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
|
||||||
} else {
|
} else {
|
||||||
writer.println("Id: {} Name: {} Active: {} Blocked: {}",
|
writer.println("Id: {} Name: {} Active: {} Blocked: {} Terminated: {}",
|
||||||
group.groupId().toBase64(),
|
group.groupId().toBase64(),
|
||||||
group.title(),
|
group.title(),
|
||||||
group.isMember(),
|
group.isMember(),
|
||||||
group.isBlocked());
|
group.isBlocked(),
|
||||||
|
group.isTerminated());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +135,8 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
|||||||
group.permissionAddMember().name(),
|
group.permissionAddMember().name(),
|
||||||
group.permissionEditDetails().name(),
|
group.permissionEditDetails().name(),
|
||||||
group.permissionSendMessage().name(),
|
group.permissionSendMessage().name(),
|
||||||
groupInviteLink == null ? null : groupInviteLink.getUrl());
|
groupInviteLink == null ? null : groupInviteLink.getUrl(),
|
||||||
|
group.isTerminated());
|
||||||
}).toList();
|
}).toList();
|
||||||
jsonWriter.write(jsonGroups);
|
jsonWriter.write(jsonGroups);
|
||||||
}
|
}
|
||||||
@ -161,7 +164,8 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
|
|||||||
String permissionAddMember,
|
String permissionAddMember,
|
||||||
String permissionEditDetails,
|
String permissionEditDetails,
|
||||||
String permissionSendMessage,
|
String permissionSendMessage,
|
||||||
String groupInviteLink
|
String groupInviteLink,
|
||||||
|
boolean isTerminated
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private record JsonGroupMemberAddress(String number, String uuid) {}
|
private record JsonGroupMemberAddress(String number, String uuid) {}
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
package org.asamk.signal.commands;
|
||||||
|
|
||||||
|
import net.sourceforge.argparse4j.inf.Namespace;
|
||||||
|
import net.sourceforge.argparse4j.inf.Subparser;
|
||||||
|
|
||||||
|
import org.asamk.signal.commands.exceptions.CommandException;
|
||||||
|
import org.asamk.signal.commands.exceptions.IOErrorException;
|
||||||
|
import org.asamk.signal.commands.exceptions.UserErrorException;
|
||||||
|
import org.asamk.signal.manager.Manager;
|
||||||
|
import org.asamk.signal.manager.api.GroupNotFoundException;
|
||||||
|
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
||||||
|
import org.asamk.signal.output.OutputWriter;
|
||||||
|
import org.asamk.signal.util.CommandUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.asamk.signal.util.SendMessageResultUtils.outputResult;
|
||||||
|
|
||||||
|
public class TerminateGroupCommand implements JsonRpcLocalCommand {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "terminateGroup";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void attachToSubparser(final Subparser subparser) {
|
||||||
|
subparser.help(
|
||||||
|
"Permanently terminate a group for all members. Requires admin privileges; afterwards nobody can send messages or start calls.");
|
||||||
|
subparser.addArgument("-g", "--group-id", "--group").required(true).help("Specify the group ID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleCommand(
|
||||||
|
final Namespace ns,
|
||||||
|
final Manager m,
|
||||||
|
final OutputWriter outputWriter
|
||||||
|
) throws CommandException {
|
||||||
|
final var groupId = CommandUtil.getGroupId(ns.getString("group-id"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final var results = m.terminateGroup(groupId);
|
||||||
|
outputResult(outputWriter, results);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOErrorException("Failed to send message: "
|
||||||
|
+ e.getMessage()
|
||||||
|
+ " ("
|
||||||
|
+ e.getClass().getSimpleName()
|
||||||
|
+ ")", e);
|
||||||
|
} catch (GroupNotFoundException | NotAGroupMemberException e) {
|
||||||
|
throw new UserErrorException("Failed to terminate group: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -327,6 +327,21 @@ public class DbusManagerImpl implements Manager {
|
|||||||
group.deleteGroup();
|
group.deleteGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SendGroupMessageResults terminateGroup(
|
||||||
|
final GroupId groupId
|
||||||
|
) throws IOException, GroupNotFoundException, NotAGroupMemberException {
|
||||||
|
final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
|
||||||
|
try {
|
||||||
|
group.terminateGroup();
|
||||||
|
} catch (Signal.Error.GroupNotFound e) {
|
||||||
|
throw new GroupNotFoundException(groupId);
|
||||||
|
} catch (Signal.Error.NotAGroupMember e) {
|
||||||
|
throw new NotAGroupMemberException(groupId, group.Get("org.asamk.Signal.Group", "Name"));
|
||||||
|
}
|
||||||
|
return new SendGroupMessageResults(0, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||||
final String name,
|
final String name,
|
||||||
@ -824,6 +839,7 @@ public class DbusManagerImpl implements Manager {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
contactBlocked,
|
contactBlocked,
|
||||||
|
0,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -871,7 +887,8 @@ public class DbusManagerImpl implements Manager {
|
|||||||
GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
|
GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
|
||||||
GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
|
GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
|
||||||
(boolean) group.get("IsMember").getValue(),
|
(boolean) group.get("IsMember").getValue(),
|
||||||
(boolean) group.get("IsAdmin").getValue());
|
(boolean) group.get("IsAdmin").getValue(),
|
||||||
|
group.get("IsTerminated") != null && (boolean) group.get("IsTerminated").getValue());
|
||||||
} catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
|
} catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1308,6 +1308,7 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||||||
new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked),
|
new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked),
|
||||||
new DbusProperty<>("IsMember", () -> getGroup().isMember()),
|
new DbusProperty<>("IsMember", () -> getGroup().isMember()),
|
||||||
new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()),
|
new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()),
|
||||||
|
new DbusProperty<>("IsTerminated", () -> getGroup().isTerminated()),
|
||||||
new DbusProperty<>("MessageExpirationTimer",
|
new DbusProperty<>("MessageExpirationTimer",
|
||||||
() -> getGroup().messageExpirationTimer(),
|
() -> getGroup().messageExpirationTimer(),
|
||||||
this::setMessageExpirationTime),
|
this::setMessageExpirationTime),
|
||||||
@ -1375,6 +1376,19 @@ public class DbusSignalImpl implements Signal, AutoCloseable {
|
|||||||
updateGroups();
|
updateGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void terminateGroup() throws Error.Failure {
|
||||||
|
try {
|
||||||
|
m.terminateGroup(groupId);
|
||||||
|
} catch (GroupNotFoundException e) {
|
||||||
|
throw new Error.GroupNotFound(e.getMessage());
|
||||||
|
} catch (NotAGroupMemberException e) {
|
||||||
|
throw new Error.NotAGroupMember(e.getMessage());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new Error.Failure(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addMembers(final List<String> recipients) throws Error.Failure {
|
public void addMembers(final List<String> recipients) throws Error.Failure {
|
||||||
final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
|
final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber());
|
||||||
|
|||||||
@ -13,7 +13,8 @@ record JsonAttachment(
|
|||||||
Integer width,
|
Integer width,
|
||||||
Integer height,
|
Integer height,
|
||||||
String caption,
|
String caption,
|
||||||
Long uploadTimestamp
|
Long uploadTimestamp,
|
||||||
|
boolean isVoiceNote
|
||||||
) {
|
) {
|
||||||
|
|
||||||
static JsonAttachment from(MessageEnvelope.Data.Attachment attachment) {
|
static JsonAttachment from(MessageEnvelope.Data.Attachment attachment) {
|
||||||
@ -32,6 +33,7 @@ record JsonAttachment(
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
caption,
|
caption,
|
||||||
uploadTimestamp);
|
uploadTimestamp,
|
||||||
|
attachment.isVoiceNote());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ record JsonDataMessage(
|
|||||||
Integer expiresInSeconds,
|
Integer expiresInSeconds,
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL) Boolean isExpirationUpdate,
|
@JsonInclude(JsonInclude.Include.NON_NULL) Boolean isExpirationUpdate,
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL) Boolean viewOnce,
|
@JsonInclude(JsonInclude.Include.NON_NULL) Boolean viewOnce,
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL) JsonGroupCallUpdate groupCallUpdate,
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL) Boolean isEndSession,
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL) Boolean isProfileKeyUpdate,
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL) Boolean hasProfileKey,
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL) JsonReaction reaction,
|
@JsonInclude(JsonInclude.Include.NON_NULL) JsonReaction reaction,
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL) JsonQuote quote,
|
@JsonInclude(JsonInclude.Include.NON_NULL) JsonQuote quote,
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL) JsonPayment payment,
|
@JsonInclude(JsonInclude.Include.NON_NULL) JsonPayment payment,
|
||||||
@ -47,6 +51,10 @@ record JsonDataMessage(
|
|||||||
final var expiresInSeconds = dataMessage.expiresInSeconds();
|
final var expiresInSeconds = dataMessage.expiresInSeconds();
|
||||||
final var isExpirationUpdate = dataMessage.isExpirationUpdate();
|
final var isExpirationUpdate = dataMessage.isExpirationUpdate();
|
||||||
final var viewOnce = dataMessage.isViewOnce();
|
final var viewOnce = dataMessage.isViewOnce();
|
||||||
|
final var groupCallUpdate = dataMessage.groupCallUpdate().map(JsonGroupCallUpdate::from).orElse(null);
|
||||||
|
final var isEndSession = dataMessage.isEndSession();
|
||||||
|
final var isProfileKeyUpdate = dataMessage.isProfileKeyUpdate();
|
||||||
|
final var hasProfileKey = dataMessage.hasProfileKey();
|
||||||
final var reaction = dataMessage.reaction().map(JsonReaction::from).orElse(null);
|
final var reaction = dataMessage.reaction().map(JsonReaction::from).orElse(null);
|
||||||
final var quote = dataMessage.quote().isPresent() ? JsonQuote.from(dataMessage.quote().get()) : null;
|
final var quote = dataMessage.quote().isPresent() ? JsonQuote.from(dataMessage.quote().get()) : null;
|
||||||
final var payment = dataMessage.payment().isPresent() ? JsonPayment.from(dataMessage.payment().get()) : null;
|
final var payment = dataMessage.payment().isPresent() ? JsonPayment.from(dataMessage.payment().get()) : null;
|
||||||
@ -86,6 +94,10 @@ record JsonDataMessage(
|
|||||||
expiresInSeconds,
|
expiresInSeconds,
|
||||||
isExpirationUpdate,
|
isExpirationUpdate,
|
||||||
viewOnce,
|
viewOnce,
|
||||||
|
groupCallUpdate,
|
||||||
|
isEndSession,
|
||||||
|
isProfileKeyUpdate,
|
||||||
|
hasProfileKey,
|
||||||
reaction,
|
reaction,
|
||||||
quote,
|
quote,
|
||||||
payment,
|
payment,
|
||||||
|
|||||||
13
src/main/java/org/asamk/signal/json/JsonGroupCallUpdate.java
Normal file
13
src/main/java/org/asamk/signal/json/JsonGroupCallUpdate.java
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package org.asamk.signal.json;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
|
||||||
|
import io.micronaut.jsonschema.JsonSchema;
|
||||||
|
|
||||||
|
@JsonSchema(title = "GroupCallUpdate")
|
||||||
|
record JsonGroupCallUpdate(String eraId) {
|
||||||
|
|
||||||
|
static JsonGroupCallUpdate from(MessageEnvelope.Data.GroupCallUpdate groupCallUpdate) {
|
||||||
|
return new JsonGroupCallUpdate(groupCallUpdate.eraId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3768,6 +3768,32 @@
|
|||||||
"java.lang.String"
|
"java.lang.String"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "<init>",
|
||||||
|
"parameterTypes": [
|
||||||
|
"int",
|
||||||
|
"long",
|
||||||
|
"java.lang.String",
|
||||||
|
"boolean",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"int",
|
||||||
|
"boolean",
|
||||||
|
"java.lang.String",
|
||||||
|
"org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData",
|
||||||
|
"org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String",
|
||||||
|
"java.lang.String"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "<init>",
|
"name": "<init>",
|
||||||
"parameterTypes": [
|
"parameterTypes": [
|
||||||
@ -3818,6 +3844,10 @@
|
|||||||
"name": "aciAccountData",
|
"name": "aciAccountData",
|
||||||
"parameterTypes": []
|
"parameterTypes": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "authCredentialSalt",
|
||||||
|
"parameterTypes": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "deviceId",
|
"name": "deviceId",
|
||||||
"parameterTypes": []
|
"parameterTypes": []
|
||||||
|
|||||||
47
src/test/java/org/asamk/signal/json/JsonAttachmentTest.java
Normal file
47
src/test/java/org/asamk/signal/json/JsonAttachmentTest.java
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package org.asamk.signal.json;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
import org.asamk.signal.util.Util;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class JsonAttachmentTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void serializesVoiceNoteFlag() {
|
||||||
|
assertVoiceNoteFlag(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void serializesNonVoiceNoteFlag() {
|
||||||
|
assertVoiceNoteFlag(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertVoiceNoteFlag(final boolean isVoiceNote) {
|
||||||
|
final var attachment = new MessageEnvelope.Data.Attachment(Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
"audio/aac",
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
isVoiceNote,
|
||||||
|
false,
|
||||||
|
false);
|
||||||
|
|
||||||
|
final var jsonAttachment = JsonAttachment.from(attachment);
|
||||||
|
final var json = Util.createJsonObjectMapper().valueToTree(jsonAttachment);
|
||||||
|
|
||||||
|
assertEquals(isVoiceNote, jsonAttachment.isVoiceNote());
|
||||||
|
assertTrue(json.has("isVoiceNote"));
|
||||||
|
assertEquals(isVoiceNote, json.get("isVoiceNote").booleanValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/test/java/org/asamk/signal/json/JsonDataMessageTest.java
Normal file
77
src/test/java/org/asamk/signal/json/JsonDataMessageTest.java
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package org.asamk.signal.json;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
||||||
|
import org.asamk.signal.util.Util;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
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 JsonDataMessageTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void serializesProtocolMetadata() {
|
||||||
|
final var dataMessage = dataMessage(Optional.of(new MessageEnvelope.Data.GroupCallUpdate("era-id")),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true);
|
||||||
|
|
||||||
|
final var json = Util.createJsonObjectMapper().valueToTree(JsonDataMessage.from(dataMessage, null));
|
||||||
|
|
||||||
|
assertEquals("era-id", json.get("groupCallUpdate").get("eraId").textValue());
|
||||||
|
assertTrue(json.get("isEndSession").booleanValue());
|
||||||
|
assertTrue(json.get("isProfileKeyUpdate").booleanValue());
|
||||||
|
assertTrue(json.get("hasProfileKey").booleanValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void omitsAbsentGroupCallUpdateAndSerializesFalseFlags() {
|
||||||
|
final var dataMessage = dataMessage(Optional.empty(), false, false, false);
|
||||||
|
|
||||||
|
final var json = Util.createJsonObjectMapper().valueToTree(JsonDataMessage.from(dataMessage, null));
|
||||||
|
|
||||||
|
assertFalse(json.has("groupCallUpdate"));
|
||||||
|
assertFalse(json.get("isEndSession").booleanValue());
|
||||||
|
assertFalse(json.get("isProfileKeyUpdate").booleanValue());
|
||||||
|
assertFalse(json.get("hasProfileKey").booleanValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MessageEnvelope.Data dataMessage(
|
||||||
|
final Optional<MessageEnvelope.Data.GroupCallUpdate> groupCallUpdate,
|
||||||
|
final boolean isEndSession,
|
||||||
|
final boolean isProfileKeyUpdate,
|
||||||
|
final boolean hasProfileKey
|
||||||
|
) {
|
||||||
|
return new MessageEnvelope.Data(1L,
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
groupCallUpdate,
|
||||||
|
Optional.empty(),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
isEndSession,
|
||||||
|
isProfileKeyUpdate,
|
||||||
|
hasProfileKey,
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
List.of(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
List.of(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user