mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-28 18:10:38 +00:00
Compare commits
144 commits
Author | SHA1 | Date | |
---|---|---|---|
|
f6d81e3c05 | ||
|
42f10670b6 | ||
|
b453d7a0b9 | ||
|
f9a36c6e04 | ||
|
be48afb2b5 | ||
|
a0960fcabd | ||
|
dbc454ba9e | ||
|
2225e69277 | ||
|
783201d12e | ||
|
3e981d66e9 | ||
|
7c7fc76a64 | ||
|
c924d5c03a | ||
|
dc787be17b | ||
|
3d4070a139 | ||
|
dbdff83132 | ||
|
4ce194afe2 | ||
|
ca33249170 | ||
|
a96626c468 | ||
|
d54be747da | ||
|
ff846bc678 | ||
|
1b7f755590 | ||
|
887ed3bb44 | ||
|
3180eba836 | ||
|
cb06cbdcca | ||
|
069325af47 | ||
|
e7ca02f1fb | ||
|
fa9bb3c210 | ||
|
e6113d4d96 | ||
|
6cc3a6f561 | ||
|
70c79eac01 | ||
|
5dc66f839d | ||
|
a0d5744c49 | ||
|
6b60a6d5a5 | ||
|
0257344940 | ||
|
17cd99be59 | ||
|
2f8328847c | ||
|
7e9727aa38 | ||
|
bf87fcc652 | ||
|
6b46314eab | ||
|
e89803464b | ||
|
a9bb8d9aae | ||
|
74909408c4 | ||
|
bb124a922d | ||
|
56e11d0857 | ||
|
d0d0021f57 | ||
|
7aafb05995 | ||
|
e594f3b237 | ||
|
bb86830a61 | ||
|
bcc1eadc7d | ||
|
4fd9e55c3c | ||
|
a2900085c9 | ||
|
5e11cf1c50 | ||
|
4e455d85d6 | ||
|
1e685c7cab | ||
|
ce813e4529 | ||
|
bd7948e246 | ||
|
b998f322f5 | ||
|
db2182aa7d | ||
|
69a9b30732 | ||
|
3dc8844cb4 | ||
|
adb6787d5b | ||
|
14b07be0dc | ||
|
6befda7ef1 | ||
|
67302eb9c3 | ||
|
f18015ff2e | ||
|
1295ef69ca | ||
|
f26a0d2891 | ||
|
2b150112ff | ||
|
7aede7c17f | ||
|
b92cbc6a7c | ||
|
68b7416e57 | ||
|
4feba68afd | ||
|
4eb34c7a93 | ||
|
26fd3e379a | ||
|
93d281e712 | ||
|
985af6e445 | ||
|
5693d871f7 | ||
|
dba8cf7a6f | ||
|
141d3326ab | ||
|
d3d2caac5a | ||
|
e1f4dae5c2 | ||
|
cf5c943127 | ||
|
ed79e0b377 | ||
|
a089a5ef04 | ||
|
90145655f4 | ||
|
3cd07ae9cd | ||
|
8aa71c132f | ||
|
b579935846 | ||
|
dfa886fae9 | ||
|
f04f789231 | ||
|
a6ec71dc31 | ||
|
47d65586cd | ||
|
b8d8413a22 | ||
|
5e16123632 | ||
|
d57442bd2a | ||
|
70313c45a9 | ||
|
f14c204764 | ||
|
71d3b83a1c | ||
|
148bf7dee2 | ||
|
2d1ba6b4ca | ||
|
055a8ee8b9 | ||
|
407a20d4bb | ||
|
05cd6aee6a | ||
|
a1378507b2 | ||
|
78cd0b13de | ||
|
c25468a71e | ||
|
a5d2e1ea23 | ||
|
6acf16ef4e | ||
|
e11e093020 | ||
|
74c2604dc8 | ||
|
e4af0be0ad | ||
|
5ac5938c8b | ||
|
94269744ad | ||
|
7a25ae5b9c | ||
|
cbd92654cf | ||
|
bd95373a70 | ||
|
d982633215 | ||
|
f91ca82902 | ||
|
c55ee85c5c | ||
|
a3776c88bd | ||
|
4a781656b4 | ||
|
11d38f29ef | ||
|
22a0ff976a | ||
|
c05b47e4d0 | ||
|
ac145e6a27 | ||
|
f00b8523d9 | ||
|
c3f8d68ceb | ||
|
9d92a3e06b | ||
|
f2df600d38 | ||
|
24d344fda4 | ||
|
0a296e77a0 | ||
|
ba147a48f8 | ||
|
77a5c454b7 | ||
|
2c68b5a9e1 | ||
|
68c9d84d19 | ||
|
fe752e0c79 | ||
|
26b5a4c582 | ||
|
10ee295ea3 | ||
|
6a5ea5fc01 | ||
|
ff6cb5262a | ||
|
f2005593ec | ||
|
3533500b73 | ||
|
e5251ae158 | ||
|
a5e272be3f |
207 changed files with 4354 additions and 2326 deletions
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [ '21', '23' ]
|
||||
java: [ '21', '24' ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -26,11 +26,22 @@ jobs:
|
|||
distribution: 'zulu'
|
||||
java-version: ${{ matrix.java }}
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
dependency-graph: generate-and-submit
|
||||
- name: Install asciidoc
|
||||
run: sudo apt update && sudo apt --no-install-recommends install -y asciidoc-base
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew --no-daemon build
|
||||
- name: Build man page
|
||||
run: |
|
||||
cd man
|
||||
make install
|
||||
- name: Add man page to archive
|
||||
run: |
|
||||
version=$(tar tf build/distributions/signal-cli-*.tar | head -n1 | sed 's|signal-cli-\([^/]*\)/.*|\1|')
|
||||
echo $version
|
||||
tar --transform="flags=r;s|man|signal-cli-${version}/man|" -rf build/distributions/signal-cli-${version}.tar man/man{1,5}
|
||||
- name: Compress archive
|
||||
run: gzip -n -9 build/distributions/signal-cli-*.tar
|
||||
- name: Archive production artifacts
|
||||
|
@ -58,3 +69,28 @@ jobs:
|
|||
with:
|
||||
name: signal-cli-native
|
||||
path: build/native/nativeCompile/signal-cli
|
||||
|
||||
build-client:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu
|
||||
- macos
|
||||
- windows
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./client
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install rust
|
||||
run: rustup default stable
|
||||
- name: Build client
|
||||
run: cargo build --release --verbose
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: signal-cli-client-${{ matrix.os }}
|
||||
path: |
|
||||
client/target/release/signal-cli-client
|
||||
client/target/release/signal-cli-client.exe
|
||||
|
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
@ -43,7 +43,7 @@ jobs:
|
|||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
@ -57,4 +57,4 @@ jobs:
|
|||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -182,7 +182,7 @@ jobs:
|
|||
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
|
||||
rm -r signal-cli-archive-* signal-cli-native
|
||||
mkdir -p build/install/
|
||||
mv ./signal-cli-*/ build/install/signal-cli
|
||||
mv ./signal-cli-"${GITHUB_REF_NAME#v}"/ build/install/signal-cli
|
||||
|
||||
- name: Build Image
|
||||
id: build_image
|
||||
|
|
115
CHANGELOG.md
115
CHANGELOG.md
|
@ -1,5 +1,120 @@
|
|||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.13.18] - 2025-07-16
|
||||
|
||||
Requires libsignal-client version 0.76.3.
|
||||
|
||||
### Added
|
||||
|
||||
- Added `--view-once` parameter to send command to send view once images
|
||||
|
||||
### Fixed
|
||||
|
||||
- Handle rate limit exception correctly when querying usernames
|
||||
|
||||
### Improved
|
||||
|
||||
- Shut down when dbus daemon connection goes away unexpectedly
|
||||
- In daemon mode, exit immediately if account check fails at startup
|
||||
- Improve behavior when sending to devices that have no available prekeys
|
||||
|
||||
## [0.13.17] - 2025-06-28
|
||||
|
||||
Requires libsignal-client version 0.76.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix issue when loading an older inactive group
|
||||
- Close attachment input streams after upload
|
||||
- Fix storage sync behavior with unhandled fields
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve behavior when pin data doesn't exist on the server
|
||||
|
||||
## [0.13.16] - 2025-06-07
|
||||
|
||||
Requires libsignal-client version 0.73.2.
|
||||
|
||||
### Changed
|
||||
|
||||
- Ensure every sent message gets a unique timestamp
|
||||
|
||||
## [0.13.15] - 2025-05-08
|
||||
|
||||
Requires libsignal-client version 0.70.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix native access warning with Java 24
|
||||
- Fix storage sync loop due to old removed e164 field
|
||||
|
||||
### Changed
|
||||
|
||||
- Increased compatibility of native build with older/virtual CPUs
|
||||
|
||||
## [0.13.14] - 2025-04-06
|
||||
|
||||
Requires libsignal-client version 0.68.1.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix pre key import from old data files
|
||||
|
||||
### Changed
|
||||
|
||||
- Use websocket connection instead of HTTP for more requests
|
||||
- Improve handling of messages with decryption error
|
||||
|
||||
## [0.13.13] - 2025-02-28
|
||||
|
||||
Requires libsignal-client version 0.66.2.
|
||||
|
||||
### Added
|
||||
- Allow setting nickname and note with `updateContact` command
|
||||
|
||||
### Fixed
|
||||
- Fix syncing nickname, note and expiration timer
|
||||
- Fix check for registered users with a proxy
|
||||
- Improve handling of storage records not yet supported by signal-cli
|
||||
- Fix contact sync for networks requiring proxy
|
||||
|
||||
## [0.13.12] - 2025-01-18
|
||||
|
||||
Requires libsignal-client version 0.65.2.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix sync of contact nick name
|
||||
- Fix incorrectly marking recipients as unregistered after sync
|
||||
- Fix cause of database deadlock (Thanks @dukhaSlayer)
|
||||
- Fix parsing of account query param in events http endpoint
|
||||
|
||||
### Changed
|
||||
|
||||
- Enable sqlite WAL journal\_mode for improved performance
|
||||
|
||||
## [0.13.11] - 2024-12-26
|
||||
|
||||
Requires libsignal-client version 0.64.0.
|
||||
|
||||
### Fixed
|
||||
- Fix issue with receiving messages that have an invalid destination
|
||||
|
||||
## [0.13.10] - 2024-11-30
|
||||
|
||||
Requires libsignal-client version 0.62.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix receiving some unusual contact sync messages
|
||||
- Fix receiving expiration timer updates
|
||||
|
||||
### Improved
|
||||
- Add support for new storage encryption scheme
|
||||
|
||||
## [0.13.9] - 2024-10-28
|
||||
|
||||
### Fixed
|
||||
|
|
21
README.md
21
README.md
|
@ -11,6 +11,10 @@ For this use-case, it has a daemon mode with JSON-RPC interface ([man page](http
|
|||
and D-BUS interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)) .
|
||||
For the JSON-RPC interface there's also a simple [example client](https://github.com/AsamK/signal-cli/tree/master/client), written in Rust.
|
||||
|
||||
signal-cli needs to be kept up-to-date to keep up with Signal-Server changes.
|
||||
The official Signal clients expire after three months and then the Signal-Server can make incompatible changes.
|
||||
So signal-cli releases older than three months may not work correctly.
|
||||
|
||||
## Installation
|
||||
|
||||
You can [build signal-cli](#building) yourself or use
|
||||
|
@ -55,8 +59,15 @@ of all country codes.)
|
|||
|
||||
signal-cli -a ACCOUNT register
|
||||
|
||||
You can register Signal using a landline number. In this case you can skip SMS verification process and jump directly
|
||||
to the voice call verification by adding the `--voice` switch at the end of above register command.
|
||||
You can register Signal using a landline number. In this case, you need to follow the procedure below:
|
||||
* Attempt a SMS verification process first (`signal-cli -a ACCOUNT register`)
|
||||
* You will get an error `400 (InvalidTransportModeException)`, this is normal
|
||||
* Wait 60 seconds
|
||||
* Attempt a voice call verification by adding the `--voice` switch and wait for the call:
|
||||
|
||||
```sh
|
||||
signal-cli -a ACCOUNT register --voice
|
||||
```
|
||||
|
||||
Registering may require solving a CAPTCHA
|
||||
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
|
||||
|
@ -72,6 +83,12 @@ of all country codes.)
|
|||
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT
|
||||
```
|
||||
|
||||
* Send a message to a username, usernames need to be prefixed with `u:`
|
||||
|
||||
```sh
|
||||
signal-cli -a ACCOUNT send -m "This is a message" u:USERNAME.000
|
||||
```
|
||||
|
||||
* Pipe the message content from another process.
|
||||
|
||||
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
|
||||
|
|
|
@ -3,10 +3,13 @@ plugins {
|
|||
application
|
||||
eclipse
|
||||
`check-lib-versions`
|
||||
id("org.graalvm.buildtools.native") version "0.10.3"
|
||||
id("org.graalvm.buildtools.native") version "0.10.6"
|
||||
}
|
||||
|
||||
version = "0.13.9"
|
||||
allprojects {
|
||||
group = "org.asamk"
|
||||
version = "0.13.19-SNAPSHOT"
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
|
@ -21,6 +24,7 @@ java {
|
|||
|
||||
application {
|
||||
mainClass.set("org.asamk.signal.Main")
|
||||
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
graalvmNative {
|
||||
|
@ -29,6 +33,7 @@ graalvmNative {
|
|||
buildArgs.add("--install-exit-handlers")
|
||||
buildArgs.add("-Dfile.encoding=UTF-8")
|
||||
buildArgs.add("-J-Dfile.encoding=UTF-8")
|
||||
buildArgs.add("-march=compatibility")
|
||||
resources.autodetect()
|
||||
configurationFileDirectories.from(file("graalvm-config-dir"))
|
||||
if (System.getenv("GRAALVM_HOME") == null) {
|
||||
|
@ -43,7 +48,41 @@ graalvmNative {
|
|||
}
|
||||
}
|
||||
|
||||
val artifactType = Attribute.of("artifactType", String::class.java)
|
||||
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
|
||||
dependencies {
|
||||
attributesSchema {
|
||||
attribute(minified)
|
||||
}
|
||||
artifactTypes.getByName("jar") {
|
||||
attributes.attribute(minified, false)
|
||||
}
|
||||
}
|
||||
|
||||
configurations.runtimeClasspath.configure {
|
||||
attributes {
|
||||
attribute(minified, true)
|
||||
}
|
||||
}
|
||||
val excludePatterns = mapOf(
|
||||
"libsignal-client" to setOf(
|
||||
"libsignal_jni_testing_amd64.so",
|
||||
"signal_jni_testing_amd64.dll",
|
||||
"libsignal_jni_testing_amd64.dylib",
|
||||
"libsignal_jni_testing_aarch64.dylib",
|
||||
)
|
||||
)
|
||||
|
||||
dependencies {
|
||||
registerTransform(JarFileExcluder::class) {
|
||||
from.attribute(minified, false).attribute(artifactType, "jar")
|
||||
to.attribute(minified, true).attribute(artifactType, "jar")
|
||||
|
||||
parameters {
|
||||
excludeFilesByArtifact = excludePatterns
|
||||
}
|
||||
}
|
||||
|
||||
implementation(libs.bouncycastle)
|
||||
implementation(libs.jackson.databind)
|
||||
implementation(libs.argparse4j)
|
||||
|
@ -51,7 +90,7 @@ dependencies {
|
|||
implementation(libs.slf4j.api)
|
||||
implementation(libs.slf4j.jul)
|
||||
implementation(libs.logback)
|
||||
implementation(project(":lib"))
|
||||
implementation(project(":libsignal-cli"))
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
@ -75,12 +114,13 @@ tasks.withType<Jar> {
|
|||
attributes(
|
||||
"Implementation-Title" to project.name,
|
||||
"Implementation-Version" to project.version,
|
||||
"Main-Class" to application.mainClass.get()
|
||||
"Main-Class" to application.mainClass.get(),
|
||||
"Enable-Native-Access" to "ALL-UNNAMED",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
task("fatJar", type = Jar::class) {
|
||||
tasks.register("fatJar", type = Jar::class) {
|
||||
archiveBaseName.set("${project.name}-fat")
|
||||
exclude(
|
||||
"META-INF/*.SF",
|
||||
|
@ -89,9 +129,11 @@ task("fatJar", type = Jar::class) {
|
|||
"META-INF/NOTICE*",
|
||||
"META-INF/LICENSE*",
|
||||
"META-INF/INDEX.LIST",
|
||||
"**/module-info.class"
|
||||
"**/module-info.class",
|
||||
)
|
||||
duplicatesStrategy = DuplicatesStrategy.WARN
|
||||
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
|
||||
doFirst {
|
||||
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
|
||||
}
|
||||
with(tasks.jar.get())
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
import groovy.util.XmlSlurper
|
||||
import groovy.util.slurpersupport.GPathResult
|
||||
import org.codehaus.groovy.runtime.ResourceGroovyMethods
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.artifacts.Dependency
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
|
||||
class CheckLibVersionsPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
|
@ -28,10 +26,10 @@ class CheckLibVersionsPlugin : Plugin<Project> {
|
|||
val name = dependency.name
|
||||
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
|
||||
try {
|
||||
val url = ResourceGroovyMethods.toURL(metaDataUrl)
|
||||
val metaDataText = ResourceGroovyMethods.getText(url)
|
||||
val metadata = XmlSlurper().parseText(metaDataText)
|
||||
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest")
|
||||
val dbf = DocumentBuilderFactory.newInstance()
|
||||
val db = dbf.newDocumentBuilder()
|
||||
val doc = db.parse(metaDataUrl);
|
||||
val newest = doc.getElementsByTagName("latest").item(0).textContent
|
||||
if (version != newest.toString()) {
|
||||
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
|
||||
}
|
||||
|
|
53
buildSrc/src/main/kotlin/ExcludeFileFromJar.kt
Normal file
53
buildSrc/src/main/kotlin/ExcludeFileFromJar.kt
Normal file
|
@ -0,0 +1,53 @@
|
|||
import org.gradle.api.artifacts.transform.*
|
||||
import org.gradle.api.file.FileSystemLocation
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.PathSensitive
|
||||
import org.gradle.api.tasks.PathSensitivity
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
@CacheableTransform
|
||||
abstract class JarFileExcluder : TransformAction<JarFileExcluder.Parameters> {
|
||||
interface Parameters : TransformParameters {
|
||||
@get:Input
|
||||
var excludeFilesByArtifact: Map<String, Set<String>>
|
||||
}
|
||||
|
||||
@get:PathSensitive(PathSensitivity.NAME_ONLY)
|
||||
@get:InputArtifact
|
||||
abstract val inputArtifact: Provider<FileSystemLocation>
|
||||
|
||||
override
|
||||
fun transform(outputs: TransformOutputs) {
|
||||
val fileName = inputArtifact.get().asFile.name
|
||||
for (entry in parameters.excludeFilesByArtifact) {
|
||||
if (fileName.startsWith(entry.key)) {
|
||||
val nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf("."))
|
||||
excludeFiles(inputArtifact.get().asFile, entry.value, outputs.file("${nameWithoutExtension}.jar"))
|
||||
return
|
||||
}
|
||||
}
|
||||
outputs.file(inputArtifact)
|
||||
}
|
||||
|
||||
private fun excludeFiles(artifact: File, excludeFiles: Set<String>, jarFile: File) {
|
||||
ZipInputStream(FileInputStream(artifact)).use { input ->
|
||||
ZipOutputStream(FileOutputStream(jarFile)).use { output ->
|
||||
var entry = input.nextEntry
|
||||
while (entry != null) {
|
||||
if (!excludeFiles.contains(entry.name)) {
|
||||
output.putNextEntry(entry)
|
||||
input.copyTo(output)
|
||||
output.closeEntry()
|
||||
}
|
||||
|
||||
entry = input.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1026
client/Cargo.lock
generated
1026
client/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,18 +1,17 @@
|
|||
[package]
|
||||
name = "signal-cli-client"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
|
||||
log = "0.4"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
|
||||
jsonrpsee = { version = "0.24", features = [
|
||||
jsonrpsee = { version = "0.25", features = [
|
||||
"macros",
|
||||
"async-client",
|
||||
"http-client",
|
||||
|
@ -20,4 +19,4 @@ jsonrpsee = { version = "0.24", features = [
|
|||
bytes = "1"
|
||||
tokio-util = "0.7"
|
||||
futures-util = "0.3"
|
||||
thiserror = "1"
|
||||
thiserror = "2"
|
||||
|
|
|
@ -15,6 +15,7 @@ pub struct Cli {
|
|||
pub json_rpc_tcp: Option<Option<SocketAddr>>,
|
||||
|
||||
/// UNIX socket address and port of signal-cli daemon
|
||||
#[cfg(unix)]
|
||||
#[arg(long, conflicts_with = "json_rpc_tcp")]
|
||||
pub json_rpc_socket: Option<Option<OsString>>,
|
||||
|
||||
|
@ -84,6 +85,8 @@ pub enum CliCommands {
|
|||
},
|
||||
GetUserStatus {
|
||||
recipient: Vec<String>,
|
||||
#[arg(long)]
|
||||
username: Vec<String>,
|
||||
},
|
||||
JoinGroup {
|
||||
#[arg(long)]
|
||||
|
@ -176,6 +179,9 @@ pub enum CliCommands {
|
|||
#[arg(short = 'a', long)]
|
||||
attachment: Vec<String>,
|
||||
|
||||
#[arg(long)]
|
||||
view_once: bool,
|
||||
|
||||
#[arg(long)]
|
||||
mention: Vec<String>,
|
||||
|
||||
|
@ -413,7 +419,7 @@ pub enum CliCommands {
|
|||
#[arg(long = "about-emoji")]
|
||||
about_emoji: Option<String>,
|
||||
|
||||
#[arg(long = "mobile-coin-address")]
|
||||
#[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")]
|
||||
mobile_coin_address: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
|
|
|
@ -70,6 +70,7 @@ pub trait Rpc {
|
|||
&self,
|
||||
account: Option<String>,
|
||||
recipients: Vec<String>,
|
||||
usernames: Vec<String>,
|
||||
) -> Result<Value, ErrorObjectOwned>;
|
||||
|
||||
#[method(name = "joinGroup", param_kind = map)]
|
||||
|
@ -182,6 +183,7 @@ pub trait Rpc {
|
|||
endSession: bool,
|
||||
message: String,
|
||||
attachments: Vec<String>,
|
||||
viewOnce: bool,
|
||||
mentions: Vec<String>,
|
||||
textStyle: Vec<String>,
|
||||
quoteTimestamp: Option<u64>,
|
||||
|
@ -190,10 +192,10 @@ pub trait Rpc {
|
|||
quoteMention: Vec<String>,
|
||||
quoteTextStyle: Vec<String>,
|
||||
quoteAttachment: Vec<String>,
|
||||
preview_url: Option<String>,
|
||||
preview_title: Option<String>,
|
||||
preview_description: Option<String>,
|
||||
preview_image: Option<String>,
|
||||
previewUrl: Option<String>,
|
||||
previewTitle: Option<String>,
|
||||
previewDescription: Option<String>,
|
||||
previewImage: Option<String>,
|
||||
sticker: Option<String>,
|
||||
storyTimestamp: Option<u64>,
|
||||
storyAuthor: Option<String>,
|
||||
|
@ -409,6 +411,7 @@ pub async fn connect_tcp(
|
|||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn connect_unix(
|
||||
socket_path: impl AsRef<Path>,
|
||||
) -> Result<impl SubscriptionClientT, std::io::Error> {
|
||||
|
@ -417,6 +420,6 @@ pub async fn connect_unix(
|
|||
Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
|
||||
}
|
||||
|
||||
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT, Error> {
|
||||
pub async fn connect_http(uri: &str) -> Result<impl SubscriptionClientT + use<>, Error> {
|
||||
HttpClientBuilder::default().build(uri)
|
||||
}
|
||||
|
|
|
@ -60,8 +60,13 @@ async fn handle_command(
|
|||
.delete_local_account_data(cli.account, ignore_registered)
|
||||
.await
|
||||
}
|
||||
CliCommands::GetUserStatus { recipient } => {
|
||||
client.get_user_status(cli.account, recipient).await
|
||||
CliCommands::GetUserStatus {
|
||||
recipient,
|
||||
username,
|
||||
} => {
|
||||
client
|
||||
.get_user_status(cli.account, recipient, username)
|
||||
.await
|
||||
}
|
||||
CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
|
||||
CliCommands::Link { name } => {
|
||||
|
@ -70,7 +75,7 @@ async fn handle_command(
|
|||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
|
||||
.device_link_uri;
|
||||
println!("{}", url);
|
||||
println!("{url}");
|
||||
client.finish_link(url, name).await
|
||||
}
|
||||
CliCommands::ListAccounts => client.list_accounts().await,
|
||||
|
@ -139,6 +144,7 @@ async fn handle_command(
|
|||
end_session,
|
||||
message,
|
||||
attachment,
|
||||
view_once,
|
||||
mention,
|
||||
text_style,
|
||||
quote_timestamp,
|
||||
|
@ -165,6 +171,7 @@ async fn handle_command(
|
|||
end_session,
|
||||
message.unwrap_or_default(),
|
||||
attachment,
|
||||
view_once,
|
||||
mention,
|
||||
text_style,
|
||||
quote_timestamp,
|
||||
|
@ -477,23 +484,30 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
|
|||
|
||||
handle_command(cli, client).await
|
||||
} else {
|
||||
let socket_path = cli
|
||||
.json_rpc_socket
|
||||
.clone()
|
||||
.unwrap_or(None)
|
||||
.or_else(|| {
|
||||
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
||||
PathBuf::from(runtime_dir)
|
||||
.join(DEFAULT_SOCKET_SUFFIX)
|
||||
.into()
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Err(RpcError::Custom("Invalid socket".into()))
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let socket_path = cli
|
||||
.json_rpc_socket
|
||||
.clone()
|
||||
.unwrap_or(None)
|
||||
.or_else(|| {
|
||||
std::env::var_os("XDG_RUNTIME_DIR").map(|runtime_dir| {
|
||||
PathBuf::from(runtime_dir)
|
||||
.join(DEFAULT_SOCKET_SUFFIX)
|
||||
.into()
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
||||
let client = jsonrpc::connect_unix(socket_path)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
.unwrap_or_else(|| ("/run".to_owned() + DEFAULT_SOCKET_SUFFIX).into());
|
||||
let client = jsonrpc::connect_unix(socket_path)
|
||||
.await
|
||||
.map_err(|e| RpcError::Custom(format!("Failed to connect to socket: {e}")))?;
|
||||
|
||||
handle_command(cli, client).await
|
||||
handle_command(cli, client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
|
||||
use jsonrpsee::core::{
|
||||
async_trait,
|
||||
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
|
||||
};
|
||||
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod ipc;
|
||||
mod stream_codec;
|
||||
pub mod tcp;
|
||||
|
@ -21,7 +19,6 @@ struct Sender<T: Send + Sink<String>> {
|
|||
inner: T,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
|
||||
for Sender<T>
|
||||
{
|
||||
|
@ -31,7 +28,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
|
|||
self.inner
|
||||
.send(body)
|
||||
.await
|
||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
||||
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -39,7 +36,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
|
|||
self.inner
|
||||
.close()
|
||||
.await
|
||||
.map_err(|e| Errors::Other(format!("{:?}", e)))?;
|
||||
.map_err(|e| Errors::Other(format!("{e:?}")))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +45,6 @@ struct Receiver<T: Send + Stream> {
|
|||
inner: T,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
|
||||
for Receiver<T>
|
||||
{
|
||||
|
@ -58,7 +54,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
|
|||
match self.inner.next().await {
|
||||
None => Err(Errors::Closed),
|
||||
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
|
||||
Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))),
|
||||
Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ impl Decoder for StreamCodec {
|
|||
|
||||
match str::from_utf8(line.as_ref()) {
|
||||
Ok(s) => Ok(Some(s.to_string())),
|
||||
Err(_) => Err(io::Error::new(io::ErrorKind::Other, "invalid UTF-8")),
|
||||
Err(_) => Err(io::Error::other("invalid UTF-8")),
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
|
|
|
@ -45,6 +45,33 @@
|
|||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<releases>
|
||||
<release version="0.13.18" date="2025-07-16">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.18</url>
|
||||
</release>
|
||||
<release version="0.13.17" date="2025-06-28">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.17</url>
|
||||
</release>
|
||||
<release version="0.13.16" date="2025-06-07">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.16</url>
|
||||
</release>
|
||||
<release version="0.13.15" date="2025-05-08">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.15</url>
|
||||
</release>
|
||||
<release version="0.13.14" date="2025-04-06">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.14</url>
|
||||
</release>
|
||||
<release version="0.13.13" date="2025-02-28">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.13</url>
|
||||
</release>
|
||||
<release version="0.13.12" date="2025-01-18">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.12</url>
|
||||
</release>
|
||||
<release version="0.13.11" date="2024-12-26">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.11</url>
|
||||
</release>
|
||||
<release version="0.13.10" date="2024-11-30">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.10</url>
|
||||
</release>
|
||||
<release version="0.13.9" date="2024-10-28">
|
||||
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.9</url>
|
||||
</release>
|
||||
|
|
|
@ -27,6 +27,10 @@
|
|||
{
|
||||
"name":"java.lang.ClassNotFoundException"
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Enum",
|
||||
"methods":[{"name":"ordinal","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"java.lang.IllegalArgumentException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
|
@ -48,9 +52,13 @@
|
|||
{
|
||||
"name":"java.lang.String"
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Thread",
|
||||
"methods":[{"name":"currentThread","parameterTypes":[] }, {"name":"getStackTrace","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Throwable",
|
||||
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }]
|
||||
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"setStackTrace","parameterTypes":["java.lang.StackTraceElement[]"] }, {"name":"toString","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"java.lang.UnsatisfiedLinkError",
|
||||
|
@ -88,7 +96,11 @@
|
|||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.internal.CompletableFuture",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }]
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"complete","parameterTypes":["java.lang.Object"] }, {"name":"completeExceptionally","parameterTypes":["java.lang.Throwable"] }, {"name":"setCancellationId","parameterTypes":["long"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.internal.NativeHandleGuard$SimpleOwner",
|
||||
"methods":[{"name":"unsafeNativeHandleWithoutGuard","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.net.CdsiLookupResponse",
|
||||
|
@ -110,6 +122,14 @@
|
|||
{
|
||||
"name":"org.signal.libsignal.net.ChatService$ResponseAndDebugInfo"
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.net.NetworkException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.net.RetryLaterException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["long"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.protocol.DuplicateMessageException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
|
@ -187,6 +207,9 @@
|
|||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
|
||||
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$IdentityChange"
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
|
||||
"fields":[{"name":"unsafeHandle"}]
|
||||
|
@ -228,6 +251,10 @@
|
|||
"name":"org.signal.libsignal.usernames.CannotBeEmptyException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.usernames.DiscriminatorCannotBeZeroException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.usernames.MissingSeparatorException",
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
|
||||
|
|
|
@ -39,9 +39,24 @@
|
|||
{
|
||||
"name":"[Ljava.sql.Statement;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.commands.ListStickerPacksCommand$JsonStickerPack$JsonSticker;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonAttachment;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonCallMessage$IceUpdate;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonContactAddress;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonContactEmail;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonContactPhone;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonMention;"
|
||||
},
|
||||
|
@ -51,6 +66,9 @@
|
|||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonQuotedAttachment;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonSharedContact;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.json.JsonSyncReadMessage;"
|
||||
},
|
||||
|
@ -60,6 +78,9 @@
|
|||
{
|
||||
"name":"[Lorg.asamk.signal.manager.storage.accounts.AccountsStorage$Account;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.asamk.signal.manager.storage.stickerPacks.JsonStickerPack$JsonSticker;"
|
||||
},
|
||||
{
|
||||
"name":"[Lorg.whispersystems.signalservice.api.groupsv2.TemporalCredential;"
|
||||
},
|
||||
|
@ -124,6 +145,13 @@
|
|||
"name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.squareup.wire.Message",
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"com.squareup.wire.ProtoAdapter"
|
||||
},
|
||||
{
|
||||
"name":"com.squareup.wire.internal.ImmutableList",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -209,9 +237,14 @@
|
|||
{
|
||||
"name":"java.io.FilePermission"
|
||||
},
|
||||
{
|
||||
"name":"java.io.OutputStream"
|
||||
},
|
||||
{
|
||||
"name":"java.io.Serializable",
|
||||
"allDeclaredMethods":true
|
||||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredClasses":true
|
||||
},
|
||||
{
|
||||
"name":"java.lang.Boolean",
|
||||
|
@ -426,6 +459,12 @@
|
|||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true
|
||||
},
|
||||
{
|
||||
"name":"java.util.ImmutableCollections$List12",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true
|
||||
},
|
||||
{
|
||||
"name":"java.util.ImmutableCollections$ListN",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -577,6 +616,9 @@
|
|||
{
|
||||
"name":"kotlin.String"
|
||||
},
|
||||
{
|
||||
"name":"kotlin.Unit"
|
||||
},
|
||||
{
|
||||
"name":"kotlin.collections.AbstractCollection",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -629,6 +671,13 @@
|
|||
{
|
||||
"name":"long[]"
|
||||
},
|
||||
{
|
||||
"name":"okhttp3.internal.connection.RealConnectionPool",
|
||||
"fields":[{"name":"addressStates"}]
|
||||
},
|
||||
{
|
||||
"name":"okio.BufferedSink"
|
||||
},
|
||||
{
|
||||
"name":"okio.ByteString"
|
||||
},
|
||||
|
@ -990,7 +1039,7 @@
|
|||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true,
|
||||
"methods":[{"name":"display","parameterTypes":[] }, {"name":"family","parameterTypes":[] }, {"name":"given","parameterTypes":[] }, {"name":"middle","parameterTypes":[] }, {"name":"prefix","parameterTypes":[] }, {"name":"suffix","parameterTypes":[] }]
|
||||
"methods":[{"name":"display","parameterTypes":[] }, {"name":"family","parameterTypes":[] }, {"name":"given","parameterTypes":[] }, {"name":"middle","parameterTypes":[] }, {"name":"nickname","parameterTypes":[] }, {"name":"prefix","parameterTypes":[] }, {"name":"suffix","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.asamk.signal.json.JsonContactPhone",
|
||||
|
@ -1025,7 +1074,7 @@
|
|||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true,
|
||||
"methods":[{"name":"groupId","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
|
||||
"methods":[{"name":"groupId","parameterTypes":[] }, {"name":"groupName","parameterTypes":[] }, {"name":"revision","parameterTypes":[] }, {"name":"type","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.asamk.signal.json.JsonMention",
|
||||
|
@ -1247,7 +1296,7 @@
|
|||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"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"] }, {"name":"<init>","parameterTypes":["int","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"] }, {"name":"<init>","parameterTypes":["int","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"] }, {"name":"aciAccountData","parameterTypes":[] }, {"name":"deviceId","parameterTypes":[] }, {"name":"encryptedDeviceName","parameterTypes":[] }, {"name":"isMultiDevice","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"password","parameterTypes":[] }, {"name":"pinMasterKey","parameterTypes":[] }, {"name":"pniAccountData","parameterTypes":[] }, {"name":"profileKey","parameterTypes":[] }, {"name":"registered","parameterTypes":[] }, {"name":"registrationLockPin","parameterTypes":[] }, {"name":"serviceEnvironment","parameterTypes":[] }, {"name":"storageKey","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"usernameLinkEntropy","parameterTypes":[] }, {"name":"usernameLinkServerId","parameterTypes":[] }, {"name":"version","parameterTypes":[] }]
|
||||
"methods":[{"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"] }, {"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"] }, {"name":"<init>","parameterTypes":["int","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"] }, {"name":"<init>","parameterTypes":["int","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"] }, {"name":"accountEntropyPool","parameterTypes":[] }, {"name":"aciAccountData","parameterTypes":[] }, {"name":"deviceId","parameterTypes":[] }, {"name":"encryptedDeviceName","parameterTypes":[] }, {"name":"isMultiDevice","parameterTypes":[] }, {"name":"mediaRootBackupKey","parameterTypes":[] }, {"name":"number","parameterTypes":[] }, {"name":"password","parameterTypes":[] }, {"name":"pinMasterKey","parameterTypes":[] }, {"name":"pniAccountData","parameterTypes":[] }, {"name":"profileKey","parameterTypes":[] }, {"name":"registered","parameterTypes":[] }, {"name":"registrationLockPin","parameterTypes":[] }, {"name":"serviceEnvironment","parameterTypes":[] }, {"name":"storageKey","parameterTypes":[] }, {"name":"timestamp","parameterTypes":[] }, {"name":"username","parameterTypes":[] }, {"name":"usernameLinkEntropy","parameterTypes":[] }, {"name":"usernameLinkServerId","parameterTypes":[] }, {"name":"version","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.asamk.signal.manager.storage.SignalAccount$Storage$AccountData",
|
||||
|
@ -1364,6 +1413,12 @@
|
|||
"name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfile",
|
||||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true
|
||||
},
|
||||
{
|
||||
"name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -1499,6 +1554,10 @@
|
|||
"name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.CONTEXT$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.CompositeSignatures$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
|
@ -1559,14 +1618,30 @@
|
|||
"name":"org.bouncycastle.jcajce.provider.asymmetric.LMS$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLDSA$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.MLKEM$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.NTRU$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.NoSig$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.SLHDSA$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.bouncycastle.jcajce.provider.asymmetric.SPHINCSPlus$Mappings",
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
|
@ -1979,7 +2054,10 @@
|
|||
"name":"org.signal.libsignal.protocol.IdentityKey"
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.protocol.ServiceId"
|
||||
"name":"org.signal.libsignal.protocol.ServiceId",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true
|
||||
},
|
||||
{
|
||||
"name":"org.signal.libsignal.protocol.SignalProtocolAddress"
|
||||
|
@ -2241,7 +2319,7 @@
|
|||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true,
|
||||
"methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }]
|
||||
"methods":[{"name":"getAnnouncementGroup","parameterTypes":[] }, {"name":"getAttachmentBackfill","parameterTypes":[] }, {"name":"getChangeNumber","parameterTypes":[] }, {"name":"getDeleteSync","parameterTypes":[] }, {"name":"getGiftBadges","parameterTypes":[] }, {"name":"getPaymentActivation","parameterTypes":[] }, {"name":"getPni","parameterTypes":[] }, {"name":"getSenderKey","parameterTypes":[] }, {"name":"getStorage","parameterTypes":[] }, {"name":"getStorageServiceEncryptionV2","parameterTypes":[] }, {"name":"getStories","parameterTypes":[] }, {"name":"getVersionedExpirationTimer","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest",
|
||||
|
@ -2268,6 +2346,20 @@
|
|||
{
|
||||
"name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -2328,7 +2420,14 @@
|
|||
"name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite",
|
||||
"allDeclaredFields":true,
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true
|
||||
"allDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","byte[]","byte[]","byte[]","byte[]","byte[]","boolean","boolean","byte[]","java.util.List"] }, {"name":"getAbout","parameterTypes":[] }, {"name":"getAboutEmoji","parameterTypes":[] }, {"name":"getAvatar","parameterTypes":[] }, {"name":"getBadgeIds","parameterTypes":[] }, {"name":"getCommitment","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPaymentAddress","parameterTypes":[] }, {"name":"getPhoneNumberSharing","parameterTypes":[] }, {"name":"getSameAvatar","parameterTypes":[] }, {"name":"getVersion","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.provisioning.ProvisioningMessage",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.push.ServiceId",
|
||||
|
@ -2373,6 +2472,12 @@
|
|||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.ratelimit.SubmitRecaptchaChallengePayload",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse",
|
||||
"allDeclaredFields":true,
|
||||
|
@ -2851,7 +2956,28 @@
|
|||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }]
|
||||
"methods":[{"name":"<init>","parameterTypes":[] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.String","java.lang.String","java.lang.String","org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.Long","java.lang.Long"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$BadgeEntitlement",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long"] }, {"name":"<init>","parameterTypes":["java.lang.String","java.lang.Boolean","java.lang.Long","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.push.WhoAmIResponse$Entitlements",
|
||||
"allDeclaredFields":true,
|
||||
"queryAllDeclaredMethods":true,
|
||||
"queryAllDeclaredConstructors":true,
|
||||
"methods":[{"name":"<init>","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement"] }, {"name":"<init>","parameterTypes":["java.util.List","org.whispersystems.signalservice.internal.push.WhoAmIResponse$BackupEntitlement","int","kotlin.jvm.internal.DefaultConstructorMarker"] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto",
|
||||
|
@ -2875,7 +3001,26 @@
|
|||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord",
|
||||
"allDeclaredFields":true
|
||||
"allDeclaredFields":true,
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$BackupTierHistory"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Builder"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$Companion"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$IAPSubscriberData"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$NotificationProfileManualOverride"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PhoneNumberSharingMode"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$PinnedConversation",
|
||||
|
@ -2889,9 +3034,22 @@
|
|||
"name":"org.whispersystems.signalservice.internal.storage.protos.AccountRecord$UsernameLink",
|
||||
"allDeclaredFields":true
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.AvatarColor"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord",
|
||||
"allDeclaredFields":true
|
||||
"allDeclaredFields":true,
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Builder"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Companion"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$IdentityState"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ContactRecord$Name",
|
||||
|
@ -2899,11 +3057,30 @@
|
|||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record",
|
||||
"allDeclaredFields":true
|
||||
"allDeclaredFields":true,
|
||||
"fields":[{"name":"archived"}, {"name":"blocked"}, {"name":"id"}, {"name":"markedUnread"}, {"name":"mutedUntilTimestamp"}, {"name":"whitelisted"}],
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Builder"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV1Record$Companion"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record",
|
||||
"allDeclaredFields":true
|
||||
"allDeclaredFields":true,
|
||||
"fields":[{"name":"archived"}, {"name":"avatarColor"}, {"name":"blocked"}, {"name":"dontNotifyForMentionsIfMuted"}, {"name":"hideStory"}, {"name":"markedUnread"}, {"name":"masterKey"}, {"name":"mutedUntilTimestamp"}, {"name":"storySendMode"}, {"name":"whitelisted"}],
|
||||
"methods":[{"name":"adapter","parameterTypes":[] }, {"name":"unknownFields","parameterTypes":[] }]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Builder"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$Companion"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.GroupV2Record$StorySendMode"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord",
|
||||
|
@ -2913,6 +3090,9 @@
|
|||
"name":"org.whispersystems.signalservice.internal.storage.protos.ManifestRecord$Identifier",
|
||||
"fields":[{"name":"raw_"}, {"name":"type_"}]
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.OptionalBool"
|
||||
},
|
||||
{
|
||||
"name":"org.whispersystems.signalservice.internal.storage.protos.Payments",
|
||||
"allDeclaredFields":true
|
||||
|
|
17
gradle/libs.versions.toml
Normal file
17
gradle/libs.versions.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[versions]
|
||||
slf4j = "2.0.17"
|
||||
|
||||
[libraries]
|
||||
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.81"
|
||||
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.19.1"
|
||||
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
|
||||
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
|
||||
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
|
||||
slf4j-jul = { module = "org.slf4j:jul-to-slf4j", version.ref = "slf4j" }
|
||||
logback = "ch.qos.logback:logback-classic:1.5.18"
|
||||
|
||||
signalservice = "com.github.turasa:signal-service-java:2.15.3_unofficial_127"
|
||||
sqlite = "org.xerial:sqlite-jdbc:3.50.2.0"
|
||||
hikari = "com.zaxxer:HikariCP:6.3.0"
|
||||
junit-jupiter = "org.junit.jupiter:junit-jupiter:5.13.2"
|
||||
junit-launcher = "org.junit.platform:junit-platform-launcher:1.13.2"
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
11
gradlew
vendored
11
gradlew
vendored
|
@ -1,7 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -86,8 +86,7 @@ done
|
|||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@ -115,7 +114,7 @@ case "$( uname )" in #(
|
|||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
|
@ -206,7 +205,7 @@ fi
|
|||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
|
4
gradlew.bat
vendored
4
gradlew.bat
vendored
|
@ -70,11 +70,11 @@ goto fail
|
|||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.asamk.signal.manager;
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.asamk.signal.manager.api.AlreadyReceivingException;
|
||||
import org.asamk.signal.manager.api.AttachmentInvalidException;
|
||||
import org.asamk.signal.manager.api.CaptchaRejectedException;
|
||||
|
@ -28,6 +30,7 @@ import org.asamk.signal.manager.api.NotAGroupMemberException;
|
|||
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.api.ReceiveConfig;
|
||||
|
@ -49,7 +52,6 @@ import org.asamk.signal.manager.api.UsernameStatus;
|
|||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
|
@ -65,7 +67,7 @@ import java.util.Set;
|
|||
public interface Manager extends Closeable {
|
||||
|
||||
static boolean isValidNumber(final String e164Number, final String countryCode) {
|
||||
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
|
||||
return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
|
||||
}
|
||||
|
||||
static boolean isSignalClientAvailable() {
|
||||
|
@ -94,7 +96,7 @@ public interface Manager extends Closeable {
|
|||
*/
|
||||
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
|
||||
|
||||
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames);
|
||||
Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
|
||||
|
||||
void updateAccountAttributes(
|
||||
String deviceName,
|
||||
|
@ -130,19 +132,24 @@ public interface Manager extends Closeable {
|
|||
void deleteUsername() throws IOException;
|
||||
|
||||
void startChangeNumber(
|
||||
String newNumber, boolean voiceVerification, String captcha
|
||||
String newNumber,
|
||||
boolean voiceVerification,
|
||||
String captcha
|
||||
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException;
|
||||
|
||||
void finishChangeNumber(
|
||||
String newNumber, String verificationCode, String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException;
|
||||
String newNumber,
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException;
|
||||
|
||||
void unregister() throws IOException;
|
||||
|
||||
void deleteAccount() throws IOException;
|
||||
|
||||
void submitRateLimitRecaptchaChallenge(
|
||||
String challenge, String captcha
|
||||
String challenge,
|
||||
String captcha
|
||||
) throws IOException, CaptchaRejectedException;
|
||||
|
||||
List<Device> getLinkedDevices() throws IOException;
|
||||
|
@ -156,17 +163,21 @@ public interface Manager extends Closeable {
|
|||
List<Group> getGroups();
|
||||
|
||||
SendGroupMessageResults quitGroup(
|
||||
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
|
||||
GroupId groupId,
|
||||
Set<RecipientIdentifier.Single> groupAdmins
|
||||
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
|
||||
|
||||
void deleteGroup(GroupId groupId) throws IOException;
|
||||
|
||||
Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String name, Set<RecipientIdentifier.Single> members, String avatarFile
|
||||
String name,
|
||||
Set<RecipientIdentifier.Single> members,
|
||||
String avatarFile
|
||||
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
|
||||
|
||||
SendGroupMessageResults updateGroup(
|
||||
final GroupId groupId, final UpdateGroup updateGroup
|
||||
final GroupId groupId,
|
||||
final UpdateGroup updateGroup
|
||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||
|
||||
Pair<GroupId, SendGroupMessageResults> joinGroup(
|
||||
|
@ -174,27 +185,29 @@ public interface Manager extends Closeable {
|
|||
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
|
||||
|
||||
SendMessageResults sendTypingMessage(
|
||||
TypingAction action, Set<RecipientIdentifier> recipients
|
||||
TypingAction action,
|
||||
Set<RecipientIdentifier> recipients
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
||||
|
||||
SendMessageResults sendReadReceipt(
|
||||
RecipientIdentifier.Single sender, List<Long> messageIds
|
||||
);
|
||||
SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
|
||||
|
||||
SendMessageResults sendViewedReceipt(
|
||||
RecipientIdentifier.Single sender, List<Long> messageIds
|
||||
);
|
||||
SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
|
||||
|
||||
SendMessageResults sendMessage(
|
||||
Message message, Set<RecipientIdentifier> recipients, boolean notifySelf
|
||||
Message message,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
boolean notifySelf
|
||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||
|
||||
SendMessageResults sendEditMessage(
|
||||
Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
|
||||
Message message,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
long editTargetTimestamp
|
||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
|
||||
|
||||
SendMessageResults sendRemoteDeleteMessage(
|
||||
long targetSentTimestamp, Set<RecipientIdentifier> recipients
|
||||
long targetSentTimestamp,
|
||||
Set<RecipientIdentifier> recipients
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
|
||||
|
||||
SendMessageResults sendMessageReaction(
|
||||
|
@ -207,13 +220,16 @@ public interface Manager extends Closeable {
|
|||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
|
||||
|
||||
SendMessageResults sendPaymentNotificationMessage(
|
||||
byte[] receipt, String note, RecipientIdentifier.Single recipient
|
||||
byte[] receipt,
|
||||
String note,
|
||||
RecipientIdentifier.Single recipient
|
||||
) throws IOException;
|
||||
|
||||
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
|
||||
|
||||
SendMessageResults sendMessageRequestResponse(
|
||||
MessageEnvelope.Sync.MessageRequestResponse.Type type, Set<RecipientIdentifier> recipientIdentifiers
|
||||
MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
||||
Set<RecipientIdentifier> recipientIdentifiers
|
||||
);
|
||||
|
||||
void hideRecipient(RecipientIdentifier.Single recipient);
|
||||
|
@ -223,22 +239,30 @@ public interface Manager extends Closeable {
|
|||
void deleteContact(RecipientIdentifier.Single recipient);
|
||||
|
||||
void setContactName(
|
||||
RecipientIdentifier.Single recipient, String givenName, final String familyName
|
||||
final RecipientIdentifier.Single recipient,
|
||||
final String givenName,
|
||||
final String familyName,
|
||||
final String nickGivenName,
|
||||
final String nickFamilyName,
|
||||
final String note
|
||||
) throws NotPrimaryDeviceException, UnregisteredRecipientException;
|
||||
|
||||
void setContactsBlocked(
|
||||
Collection<RecipientIdentifier.Single> recipient, boolean blocked
|
||||
Collection<RecipientIdentifier.Single> recipient,
|
||||
boolean blocked
|
||||
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
|
||||
|
||||
void setGroupsBlocked(
|
||||
Collection<GroupId> groupId, boolean blocked
|
||||
Collection<GroupId> groupId,
|
||||
boolean blocked
|
||||
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
|
||||
|
||||
/**
|
||||
* Change the expiration timer for a contact
|
||||
*/
|
||||
void setExpirationTimer(
|
||||
RecipientIdentifier.Single recipient, int messageExpirationTimer
|
||||
RecipientIdentifier.Single recipient,
|
||||
int messageExpirationTimer
|
||||
) throws IOException, UnregisteredRecipientException;
|
||||
|
||||
/**
|
||||
|
@ -277,7 +301,9 @@ public interface Manager extends Closeable {
|
|||
* Receive new messages from server, returns if no new message arrive in a timespan of timeout.
|
||||
*/
|
||||
void receiveMessages(
|
||||
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
|
||||
Optional<Duration> timeout,
|
||||
Optional<Integer> maxMessages,
|
||||
ReceiveMessageHandler handler
|
||||
) throws IOException, AlreadyReceivingException;
|
||||
|
||||
void stopReceiveMessages();
|
||||
|
@ -309,7 +335,8 @@ public interface Manager extends Closeable {
|
|||
* @param recipient account of the identity
|
||||
*/
|
||||
boolean trustIdentityVerified(
|
||||
RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode
|
||||
RecipientIdentifier.Single recipient,
|
||||
IdentityVerificationCode verificationCode
|
||||
) throws UnregisteredRecipientException;
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.asamk.signal.manager;
|
|||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
|
@ -13,12 +14,15 @@ import java.io.IOException;
|
|||
public interface RegistrationManager extends Closeable {
|
||||
|
||||
void register(
|
||||
boolean voiceVerification, String captcha, final boolean forceRegister
|
||||
boolean voiceVerification,
|
||||
String captcha,
|
||||
final boolean forceRegister
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
|
||||
|
||||
void verifyAccount(
|
||||
String verificationCode, String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException;
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
|
||||
|
||||
void deleteLocalAccountData() throws IOException;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.asamk.signal.manager;
|
|||
|
||||
import org.asamk.signal.manager.api.AccountCheckException;
|
||||
import org.asamk.signal.manager.api.NotRegisteredException;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
|
@ -63,19 +64,28 @@ public class SignalAccountFiles {
|
|||
return accountsStore.getAllNumbers();
|
||||
}
|
||||
|
||||
public MultiAccountManager initMultiAccountManager() throws IOException {
|
||||
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||
public MultiAccountManager initMultiAccountManager() throws IOException, AccountCheckException {
|
||||
final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
|
||||
try {
|
||||
return initManager(a.number(), a.path());
|
||||
} catch (NotRegisteredException | IOException | AccountCheckException e) {
|
||||
return new Pair<Manager, Throwable>(initManager(a.number(), a.path()), null);
|
||||
} catch (NotRegisteredException e) {
|
||||
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||
return null;
|
||||
} catch (Throwable e) {
|
||||
} catch (AccountCheckException | IOException e) {
|
||||
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
|
||||
throw e;
|
||||
return new Pair<Manager, Throwable>(null, e);
|
||||
}
|
||||
}).filter(Objects::nonNull).toList();
|
||||
|
||||
for (final var pair : managerPairs) {
|
||||
if (pair.second() instanceof IOException e) {
|
||||
throw e;
|
||||
} else if (pair.second() instanceof AccountCheckException e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
final var managers = managerPairs.stream().map(Pair::first).toList();
|
||||
return new MultiAccountManagerImpl(managers, this);
|
||||
}
|
||||
|
||||
|
@ -85,7 +95,8 @@ public class SignalAccountFiles {
|
|||
}
|
||||
|
||||
private Manager initManager(
|
||||
String number, String accountPath
|
||||
String number,
|
||||
String accountPath
|
||||
) throws IOException, NotRegisteredException, AccountCheckException {
|
||||
if (accountPath == null) {
|
||||
throw new NotRegisteredException();
|
||||
|
@ -152,7 +163,8 @@ public class SignalAccountFiles {
|
|||
}
|
||||
|
||||
public RegistrationManager initRegistrationManager(
|
||||
String number, Consumer<Manager> newManagerListener
|
||||
String number,
|
||||
Consumer<Manager> newManagerListener
|
||||
) throws IOException {
|
||||
final var accountPath = accountsStore.getPathByNumber(number);
|
||||
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
|
||||
|
|
|
@ -19,9 +19,7 @@ public class RenewSessionAction implements HandleAction {
|
|||
@Override
|
||||
public void execute(Context context) throws Throwable {
|
||||
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
|
||||
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) {
|
||||
context.getSendHelper().sendNullMessage(recipientId);
|
||||
}
|
||||
context.getSendHelper().sendNullMessage(recipientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -13,7 +13,9 @@ public class ResendMessageAction implements HandleAction {
|
|||
private final MessageSendLogEntry messageSendLogEntry;
|
||||
|
||||
public ResendMessageAction(
|
||||
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
|
||||
final RecipientId recipientId,
|
||||
final long timestamp,
|
||||
final MessageSendLogEntry messageSendLogEntry
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.timestamp = timestamp;
|
||||
|
|
|
@ -15,7 +15,9 @@ public class SendReceiptAction implements HandleAction {
|
|||
private final List<Long> timestamps = new ArrayList<>();
|
||||
|
||||
public SendReceiptAction(
|
||||
final RecipientId recipientId, final SignalServiceReceiptMessage.Type type, final long timestamp
|
||||
final RecipientId recipientId,
|
||||
final SignalServiceReceiptMessage.Type type,
|
||||
final long timestamp
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.type = type;
|
||||
|
|
|
@ -7,7 +7,6 @@ import org.signal.libsignal.metadata.ProtocolException;
|
|||
import org.signal.libsignal.protocol.message.CiphertextMessage;
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.internal.push.Envelope;
|
||||
|
||||
import java.util.Optional;
|
||||
|
@ -15,29 +14,21 @@ import java.util.Optional;
|
|||
public class SendRetryMessageRequestAction implements HandleAction {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final ServiceId serviceId;
|
||||
private final ProtocolException protocolException;
|
||||
private final SignalServiceEnvelope envelope;
|
||||
private final ServiceId accountId;
|
||||
|
||||
public SendRetryMessageRequestAction(
|
||||
final RecipientId recipientId,
|
||||
final ServiceId serviceId,
|
||||
final ProtocolException protocolException,
|
||||
final SignalServiceEnvelope envelope,
|
||||
final ServiceId accountId
|
||||
final SignalServiceEnvelope envelope
|
||||
) {
|
||||
this.recipientId = recipientId;
|
||||
this.serviceId = serviceId;
|
||||
this.protocolException = protocolException;
|
||||
this.envelope = envelope;
|
||||
this.accountId = accountId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Context context) throws Throwable {
|
||||
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
|
||||
|
||||
int senderDevice = protocolException.getSenderDevice();
|
||||
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
|
||||
protocolException.getGroupId().get())) : Optional.empty();
|
||||
|
|
|
@ -49,8 +49,12 @@ public record Contact(
|
|||
builder.givenName = copy.givenName();
|
||||
builder.familyName = copy.familyName();
|
||||
builder.nickName = copy.nickName();
|
||||
builder.nickNameGivenName = copy.nickNameGivenName();
|
||||
builder.nickNameFamilyName = copy.nickNameFamilyName();
|
||||
builder.note = copy.note();
|
||||
builder.color = copy.color();
|
||||
builder.messageExpirationTime = copy.messageExpirationTime();
|
||||
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
|
||||
builder.muteUntil = copy.muteUntil();
|
||||
builder.hideStory = copy.hideStory();
|
||||
builder.isBlocked = copy.isBlocked();
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
|
|||
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
|
||||
import java.net.URI;
|
||||
|
@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
|
|||
}
|
||||
ECPublicKey deviceKey;
|
||||
try {
|
||||
deviceKey = Curve.decodePoint(publicKeyBytes, 0);
|
||||
deviceKey = new ECPublicKey(publicKeyBytes);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidDeviceLinkException("Invalid device link", e);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@ public record Group(
|
|||
) {
|
||||
|
||||
public static Group from(
|
||||
final GroupInfo groupInfo, final RecipientAddressResolver recipientStore, final RecipientId selfRecipientId
|
||||
final GroupInfo groupInfo,
|
||||
final RecipientAddressResolver recipientStore,
|
||||
final RecipientId selfRecipientId
|
||||
) {
|
||||
return new Group(groupInfo.getGroupId(),
|
||||
groupInfo.getTitle(),
|
||||
|
|
|
@ -2,6 +2,10 @@ package org.asamk.signal.manager.api;
|
|||
|
||||
public class InvalidNumberException extends Exception {
|
||||
|
||||
public InvalidNumberException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
InvalidNumberException(String message, Throwable e) {
|
||||
super(message, e);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.util.Optional;
|
|||
public record Message(
|
||||
String messageText,
|
||||
List<String> attachments,
|
||||
boolean viewOnce,
|
||||
List<Mention> mentions,
|
||||
Optional<Quote> quote,
|
||||
Optional<Sticker> sticker,
|
||||
|
|
|
@ -338,7 +338,8 @@ public record MessageEnvelope(
|
|||
}
|
||||
|
||||
static Attachment from(
|
||||
SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider
|
||||
SignalServiceDataMessage.Quote.QuotedAttachment a,
|
||||
final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
return new Attachment(Optional.empty(),
|
||||
Optional.empty(),
|
||||
|
@ -510,9 +511,7 @@ public record MessageEnvelope(
|
|||
|
||||
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
|
||||
|
||||
static Preview from(
|
||||
SignalServicePreview preview, final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) {
|
||||
return new Preview(preview.getTitle(),
|
||||
preview.getDescription(),
|
||||
preview.getDate(),
|
||||
|
@ -612,11 +611,12 @@ public record MessageEnvelope(
|
|||
RecipientResolver recipientResolver,
|
||||
RecipientAddressResolver addressResolver
|
||||
) {
|
||||
return new Blocked(blockedListMessage.getAddresses()
|
||||
.stream()
|
||||
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d))
|
||||
.toApiRecipientAddress())
|
||||
.toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList());
|
||||
return new Blocked(blockedListMessage.individuals.stream()
|
||||
.map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(),
|
||||
null,
|
||||
d.getE164(),
|
||||
null))
|
||||
.toList(), blockedListMessage.groupIds.stream().map(GroupId::unknownVersion).toList());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -832,9 +832,7 @@ public record MessageEnvelope(
|
|||
Optional<TextAttachment> textAttachment
|
||||
) {
|
||||
|
||||
public static Story from(
|
||||
SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
public static Story from(SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider) {
|
||||
return new Story(storyMessage.getAllowsReplies().orElse(false),
|
||||
storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
|
||||
storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
|
||||
|
@ -852,7 +850,8 @@ public record MessageEnvelope(
|
|||
) {
|
||||
|
||||
static TextAttachment from(
|
||||
SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider
|
||||
SignalServiceTextAttachment textAttachment,
|
||||
final AttachmentFileProvider fileProvider
|
||||
) {
|
||||
return new TextAttachment(textAttachment.getText(),
|
||||
textAttachment.getStyle().map(Style::from),
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package org.asamk.signal.manager.api;
|
||||
|
||||
public class PinLockMissingException extends Exception {}
|
|
@ -161,7 +161,8 @@ public class Profile {
|
|||
}
|
||||
|
||||
public enum Capability {
|
||||
storage;
|
||||
storage,
|
||||
storageServiceEncryptionV2Capability;
|
||||
|
||||
public static Capability valueOfOrNull(String value) {
|
||||
try {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package org.asamk.signal.manager.api;
|
||||
|
||||
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
@ -24,24 +24,28 @@ public sealed interface RecipientIdentifier {
|
|||
sealed interface Single extends RecipientIdentifier {
|
||||
|
||||
static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
|
||||
try {
|
||||
if (UuidUtil.isUuid(identifier)) {
|
||||
return new Uuid(UUID.fromString(identifier));
|
||||
}
|
||||
|
||||
if (identifier.startsWith("u:")) {
|
||||
return new Username(identifier.substring(2));
|
||||
}
|
||||
|
||||
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
||||
if (!normalizedNumber.equals(identifier)) {
|
||||
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
||||
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
|
||||
}
|
||||
return new Number(normalizedNumber);
|
||||
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
|
||||
throw new InvalidNumberException(e.getMessage(), e);
|
||||
if (UuidUtil.isUuid(identifier)) {
|
||||
return new Uuid(UUID.fromString(identifier));
|
||||
}
|
||||
|
||||
if (identifier.startsWith("PNI:")) {
|
||||
final var pni = identifier.substring(4);
|
||||
if (!UuidUtil.isUuid(pni)) {
|
||||
throw new InvalidNumberException("Invalid PNI");
|
||||
}
|
||||
return new Pni(UUID.fromString(pni));
|
||||
}
|
||||
|
||||
if (identifier.startsWith("u:")) {
|
||||
return new Username(identifier.substring(2));
|
||||
}
|
||||
|
||||
final var normalizedNumber = PhoneNumberFormatter.formatNumber(identifier, localNumber);
|
||||
if (!normalizedNumber.equals(identifier)) {
|
||||
final Logger logger = LoggerFactory.getLogger(RecipientIdentifier.class);
|
||||
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
|
||||
}
|
||||
return new Number(normalizedNumber);
|
||||
}
|
||||
|
||||
static Single fromAddress(RecipientAddress address) {
|
||||
|
@ -50,7 +54,7 @@ public sealed interface RecipientIdentifier {
|
|||
} else if (address.aci().isPresent()) {
|
||||
return new Uuid(UUID.fromString(address.aci().get()));
|
||||
} else if (address.pni().isPresent()) {
|
||||
return new Pni(address.pni().get());
|
||||
return new Pni(UUID.fromString(address.pni().get().substring(4)));
|
||||
} else if (address.username().isPresent()) {
|
||||
return new Username(address.username().get());
|
||||
}
|
||||
|
@ -73,16 +77,16 @@ public sealed interface RecipientIdentifier {
|
|||
}
|
||||
}
|
||||
|
||||
record Pni(String pni) implements Single {
|
||||
record Pni(UUID pni) implements Single {
|
||||
|
||||
@Override
|
||||
public String getIdentifier() {
|
||||
return pni;
|
||||
return "PNI:" + pni.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecipientAddress toPartialRecipientAddress() {
|
||||
return new RecipientAddress(null, pni, null, null);
|
||||
return new RecipientAddress(null, getIdentifier(), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ package org.asamk.signal.manager.config;
|
|||
|
||||
import org.signal.libsignal.net.Network.Environment;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||
|
@ -28,8 +28,9 @@ class LiveConfig {
|
|||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
|
||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||
private static final String SVR2_MRENCLAVE = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
|
||||
private static final String SVR2_LEGACY_MRENCLAVE = "a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97";
|
||||
private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570";
|
||||
private static final String SVR2_MRENCLAVE_LEGACY = "093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6";
|
||||
private static final String SVR2_MRENCLAVE = "29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb";
|
||||
|
||||
private static final String URL = "https://chat.signal.org";
|
||||
private static final String CDN_URL = "https://cdn.signal.org";
|
||||
|
@ -42,6 +43,7 @@ class LiveConfig {
|
|||
|
||||
private static final Optional<Dns> dns = Optional.empty();
|
||||
private static final Optional<SignalProxy> proxy = Optional.empty();
|
||||
private static final Optional<HttpProxy> systemProxy = Optional.empty();
|
||||
|
||||
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==");
|
||||
|
@ -51,7 +53,7 @@ class LiveConfig {
|
|||
private static final byte[] backupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
|
||||
|
||||
private static Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
|
||||
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
|
||||
|
||||
static SignalServiceConfiguration createDefaultServiceConfiguration(
|
||||
final List<Interceptor> interceptors
|
||||
|
@ -69,14 +71,16 @@ class LiveConfig {
|
|||
interceptors,
|
||||
dns,
|
||||
proxy,
|
||||
systemProxy,
|
||||
zkGroupServerPublicParams,
|
||||
genericServerPublicParams,
|
||||
backupServerPublicParams);
|
||||
backupServerPublicParams,
|
||||
false);
|
||||
}
|
||||
|
||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
||||
try {
|
||||
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
|
||||
return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
@ -88,7 +92,7 @@ class LiveConfig {
|
|||
createDefaultServiceConfiguration(interceptors),
|
||||
getUnidentifiedSenderTrustRoot(),
|
||||
CDSI_MRENCLAVE,
|
||||
List.of(SVR2_MRENCLAVE, SVR2_LEGACY_MRENCLAVE));
|
||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
|
||||
}
|
||||
|
||||
private LiveConfig() {
|
||||
|
|
|
@ -20,7 +20,7 @@ public class ServiceConfig {
|
|||
|
||||
public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
|
||||
public static final long MAX_ENVELOPE_SIZE = 0;
|
||||
public static final int MAX_MESSAGE_BODY_SIZE = 2000;
|
||||
public static final int MAX_MESSAGE_SIZE_BYTES = 2000;
|
||||
public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
|
||||
public static final boolean AUTOMATIC_NETWORK_RETRY = true;
|
||||
public static final int GROUP_MAX_SIZE = 1001;
|
||||
|
@ -29,11 +29,14 @@ public class ServiceConfig {
|
|||
|
||||
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
|
||||
final var deleteSync = !isPrimaryDevice;
|
||||
return new AccountAttributes.Capabilities(true, deleteSync, true);
|
||||
final var storageEncryptionV2 = !isPrimaryDevice;
|
||||
final var attachmentBackfill = !isPrimaryDevice;
|
||||
return new AccountAttributes.Capabilities(true, deleteSync, true, storageEncryptionV2, attachmentBackfill);
|
||||
}
|
||||
|
||||
public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
|
||||
ServiceEnvironment serviceEnvironment, String userAgent
|
||||
ServiceEnvironment serviceEnvironment,
|
||||
String userAgent
|
||||
) {
|
||||
final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
|
||||
.newBuilder()
|
||||
|
|
|
@ -2,9 +2,9 @@ package org.asamk.signal.manager.config;
|
|||
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.internal.configuration.HttpProxy;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
|
||||
|
@ -28,8 +28,9 @@ class StagingConfig {
|
|||
private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
|
||||
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
|
||||
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
|
||||
private static final String SVR2_MRENCLAVE = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
|
||||
private static final String SVR2_LEGACY_MRENCLAVE = "acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482";
|
||||
private static final String SVR2_MRENCLAVE_LEGACY_LEGACY = "38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3";
|
||||
private static final String SVR2_MRENCLAVE_LEGACY = "2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91";
|
||||
private static final String SVR2_MRENCLAVE = "a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036";
|
||||
|
||||
private static final String URL = "https://chat.staging.signal.org";
|
||||
private static final String CDN_URL = "https://cdn-staging.signal.org";
|
||||
|
@ -42,6 +43,7 @@ class StagingConfig {
|
|||
|
||||
private static final Optional<Dns> dns = Optional.empty();
|
||||
private static final Optional<SignalProxy> proxy = Optional.empty();
|
||||
private static final Optional<HttpProxy> systemProxy = Optional.empty();
|
||||
|
||||
private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
|
||||
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==");
|
||||
|
@ -51,7 +53,7 @@ class StagingConfig {
|
|||
private static final byte[] backupServerPublicParams = Base64.getDecoder()
|
||||
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
|
||||
|
||||
private static Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
|
||||
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
|
||||
|
||||
static SignalServiceConfiguration createDefaultServiceConfiguration(
|
||||
final List<Interceptor> interceptors
|
||||
|
@ -69,14 +71,16 @@ class StagingConfig {
|
|||
interceptors,
|
||||
dns,
|
||||
proxy,
|
||||
systemProxy,
|
||||
zkGroupServerPublicParams,
|
||||
genericServerPublicParams,
|
||||
backupServerPublicParams);
|
||||
backupServerPublicParams,
|
||||
false);
|
||||
}
|
||||
|
||||
static ECPublicKey getUnidentifiedSenderTrustRoot() {
|
||||
try {
|
||||
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0);
|
||||
return new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
@ -88,7 +92,7 @@ class StagingConfig {
|
|||
createDefaultServiceConfiguration(interceptors),
|
||||
getUnidentifiedSenderTrustRoot(),
|
||||
CDSI_MRENCLAVE,
|
||||
List.of(SVR2_MRENCLAVE, SVR2_LEGACY_MRENCLAVE));
|
||||
List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY, SVR2_MRENCLAVE_LEGACY_LEGACY));
|
||||
}
|
||||
|
||||
private StagingConfig() {
|
||||
|
|
|
@ -18,7 +18,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
|||
public class GroupUtils {
|
||||
|
||||
public static void setGroupContext(
|
||||
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final GroupInfo groupInfo
|
||||
) {
|
||||
if (groupInfo instanceof GroupInfoV1) {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
|
||||
|
|
|
@ -3,8 +3,8 @@ package org.asamk.signal.manager.helper;
|
|||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
|
@ -27,11 +27,13 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||
|
@ -50,12 +52,13 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
|
||||
|
||||
public class AccountHelper {
|
||||
|
@ -101,9 +104,9 @@ public class AccountHelper {
|
|||
checkWhoAmiI();
|
||||
}
|
||||
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
|
||||
context.getSyncHelper().requestSyncPniIdentity();
|
||||
throw new IOException("Missing PNI identity key, relinking required");
|
||||
}
|
||||
if (account.getPreviousStorageVersion() < 4
|
||||
if (account.getPreviousStorageVersion() < 10
|
||||
&& account.isPrimaryDevice()
|
||||
&& account.getRegistrationLockPin() != null) {
|
||||
migrateRegistrationPin();
|
||||
|
@ -165,7 +168,9 @@ public class AccountHelper {
|
|||
}
|
||||
|
||||
public void startChangeNumber(
|
||||
String newNumber, boolean voiceVerification, String captcha
|
||||
String newNumber,
|
||||
boolean voiceVerification,
|
||||
String captcha
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
|
||||
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
|
||||
final var registrationApi = accountManager.getRegistrationApi();
|
||||
|
@ -178,8 +183,10 @@ public class AccountHelper {
|
|||
}
|
||||
|
||||
public void finishChangeNumber(
|
||||
String newNumber, String verificationCode, String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException {
|
||||
String newNumber,
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
|
||||
for (var attempts = 0; attempts < 5; attempts++) {
|
||||
try {
|
||||
finishChangeNumberInternal(newNumber, verificationCode, pin);
|
||||
|
@ -196,8 +203,10 @@ public class AccountHelper {
|
|||
}
|
||||
|
||||
private void finishChangeNumberInternal(
|
||||
String newNumber, String verificationCode, String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException {
|
||||
String newNumber,
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
|
||||
final var pniIdentity = KeyUtils.generateIdentityKeyPair();
|
||||
final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
|
||||
final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
|
||||
|
@ -282,13 +291,13 @@ public class AccountHelper {
|
|||
context.getPinHelper(),
|
||||
(sessionId1, verificationCode1, registrationLock) -> {
|
||||
final var registrationApi = dependencies.getRegistrationApi();
|
||||
final var accountApi = dependencies.getAccountApi();
|
||||
try {
|
||||
Utils.handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
|
||||
handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
|
||||
} catch (AlreadyVerifiedException e) {
|
||||
// Already verified so can continue changing number
|
||||
}
|
||||
return Utils.handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(
|
||||
sessionId1,
|
||||
return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
|
||||
null,
|
||||
newNumber,
|
||||
registrationLock,
|
||||
|
@ -308,9 +317,7 @@ public class AccountHelper {
|
|||
handlePniChangeNumberMessage(selfChangeNumber, updatePni);
|
||||
}
|
||||
|
||||
public void handlePniChangeNumberMessage(
|
||||
final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
|
||||
) {
|
||||
public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
|
||||
if (pniChangeNumber.identityKeyPair != null
|
||||
&& pniChangeNumber.registrationId != null
|
||||
&& pniChangeNumber.signedPreKey != null) {
|
||||
|
@ -374,7 +381,7 @@ public class AccountHelper {
|
|||
candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
|
||||
}
|
||||
|
||||
final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
|
||||
final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
|
||||
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
|
||||
if (hashIndex == -1) {
|
||||
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
|
||||
|
@ -384,7 +391,7 @@ public class AccountHelper {
|
|||
logger.debug("[reserveUsername] Successfully reserved username.");
|
||||
final var username = candidates.get(hashIndex);
|
||||
|
||||
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
|
||||
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||
account.setUsername(username.getUsername());
|
||||
account.setUsernameLink(linkComponents);
|
||||
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
|
||||
|
@ -392,6 +399,40 @@ public class AccountHelper {
|
|||
logger.debug("[confirmUsername] Successfully confirmed username.");
|
||||
}
|
||||
|
||||
public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
|
||||
try {
|
||||
Username.UsernameLink link = username.generateLink();
|
||||
return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
|
||||
} catch (BaseUsernameException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
|
||||
try {
|
||||
Username.UsernameLink link = username.generateLink();
|
||||
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
|
||||
|
||||
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||
} catch (BaseUsernameException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private UsernameLinkComponents reclaimUsernameAndLink(
|
||||
Username username,
|
||||
UsernameLinkComponents linkComponents
|
||||
) throws IOException {
|
||||
try {
|
||||
Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
|
||||
UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
|
||||
|
||||
return new UsernameLinkComponents(link.getEntropy(), serverId);
|
||||
} catch (BaseUsernameException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshCurrentUsername() throws IOException, BaseUsernameException {
|
||||
final var localUsername = account.getUsername();
|
||||
if (localUsername == null) {
|
||||
|
@ -434,14 +475,14 @@ public class AccountHelper {
|
|||
final var usernameLink = account.getUsernameLink();
|
||||
|
||||
if (usernameLink == null) {
|
||||
dependencies.getAccountManager()
|
||||
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
.reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
|
||||
logger.debug("[reserveUsername] Successfully reserved existing username.");
|
||||
final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
|
||||
final var linkComponents = confirmUsernameAndCreateNewLink(username);
|
||||
account.setUsernameLink(linkComponents);
|
||||
logger.debug("[confirmUsername] Successfully confirmed existing username.");
|
||||
} else {
|
||||
final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink);
|
||||
final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
|
||||
account.setUsernameLink(linkComponents);
|
||||
logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
|
||||
}
|
||||
|
@ -451,7 +492,7 @@ public class AccountHelper {
|
|||
private void tryToSetUsernameLink(Username username) {
|
||||
for (var i = 1; i < 4; i++) {
|
||||
try {
|
||||
final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
|
||||
final var linkComponents = createUsernameLink(username);
|
||||
account.setUsernameLink(linkComponents);
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
|
@ -461,9 +502,8 @@ public class AccountHelper {
|
|||
}
|
||||
|
||||
public void deleteUsername() throws IOException {
|
||||
dependencies.getAccountManager().deleteUsernameLink();
|
||||
handleResponseException(dependencies.getAccountApi().deleteUsername());
|
||||
account.setUsernameLink(null);
|
||||
dependencies.getAccountManager().deleteUsername();
|
||||
account.setUsername(null);
|
||||
logger.debug("[deleteUsername] Successfully deleted the username.");
|
||||
}
|
||||
|
@ -475,36 +515,39 @@ public class AccountHelper {
|
|||
}
|
||||
|
||||
public void updateAccountAttributes() throws IOException {
|
||||
dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
|
||||
handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
|
||||
}
|
||||
|
||||
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException, org.asamk.signal.manager.api.DeviceLimitExceededException {
|
||||
String verificationCode;
|
||||
public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
|
||||
final var linkDeviceApi = dependencies.getLinkDeviceApi();
|
||||
final LinkedDeviceVerificationCodeResponse verificationCode;
|
||||
try {
|
||||
verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
|
||||
verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
|
||||
} catch (DeviceLimitExceededException e) {
|
||||
throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
|
||||
}
|
||||
|
||||
try {
|
||||
dependencies.getAccountManager()
|
||||
.addDevice(deviceLinkInfo.deviceIdentifier(),
|
||||
deviceLinkInfo.deviceKey(),
|
||||
account.getAciIdentityKeyPair(),
|
||||
account.getPniIdentityKeyPair(),
|
||||
account.getProfileKey(),
|
||||
account.getOrCreatePinMasterKey(),
|
||||
verificationCode);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidDeviceLinkException("Invalid device link", e);
|
||||
}
|
||||
handleResponseException(dependencies.getLinkDeviceApi()
|
||||
.linkDevice(account.getNumber(),
|
||||
account.getAci(),
|
||||
account.getPni(),
|
||||
deviceLinkInfo.deviceIdentifier(),
|
||||
deviceLinkInfo.deviceKey(),
|
||||
account.getAciIdentityKeyPair(),
|
||||
account.getPniIdentityKeyPair(),
|
||||
account.getProfileKey(),
|
||||
account.getOrCreateAccountEntropyPool(),
|
||||
account.getOrCreatePinMasterKey(),
|
||||
account.getOrCreateMediaRootBackupKey(),
|
||||
verificationCode.getVerificationCode(),
|
||||
null));
|
||||
account.setMultiDevice(true);
|
||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||
}
|
||||
|
||||
public void removeLinkedDevices(int deviceId) throws IOException {
|
||||
dependencies.getAccountManager().removeDevice(deviceId);
|
||||
var devices = dependencies.getAccountManager().getDevices();
|
||||
handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
|
||||
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||
account.setMultiDevice(devices.size() > 1);
|
||||
}
|
||||
|
||||
|
@ -512,14 +555,16 @@ public class AccountHelper {
|
|||
var masterKey = account.getOrCreatePinMasterKey();
|
||||
|
||||
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
|
||||
dependencies.getAccountManager().enableRegistrationLock(masterKey);
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||
}
|
||||
|
||||
public void setRegistrationPin(String pin) throws IOException {
|
||||
var masterKey = account.getOrCreatePinMasterKey();
|
||||
|
||||
context.getPinHelper().setRegistrationLockPin(pin, masterKey);
|
||||
dependencies.getAccountManager().enableRegistrationLock(masterKey);
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
.enableRegistrationLock(masterKey.deriveRegistrationLock()));
|
||||
|
||||
account.setRegistrationLockPin(pin);
|
||||
updateAccountAttributes();
|
||||
|
@ -528,7 +573,7 @@ public class AccountHelper {
|
|||
public void removeRegistrationPin() throws IOException {
|
||||
// Remove KBS Pin
|
||||
context.getPinHelper().removeRegistrationLockPin();
|
||||
dependencies.getAccountManager().disableRegistrationLock();
|
||||
handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
|
||||
|
||||
account.setRegistrationLockPin(null);
|
||||
}
|
||||
|
@ -537,7 +582,7 @@ public class AccountHelper {
|
|||
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
|
||||
// If this is the primary device, other users can't send messages to this number anymore.
|
||||
// If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
|
||||
dependencies.getAccountManager().setGcmId(Optional.empty());
|
||||
handleResponseException(dependencies.getAccountApi().clearFcmToken());
|
||||
|
||||
account.setRegistered(false);
|
||||
unregisteredListener.call();
|
||||
|
@ -551,7 +596,7 @@ public class AccountHelper {
|
|||
}
|
||||
account.setRegistrationLockPin(null);
|
||||
|
||||
dependencies.getAccountManager().deleteAccount();
|
||||
handleResponseException(dependencies.getAccountApi().deleteAccount());
|
||||
|
||||
account.setRegistered(false);
|
||||
unregisteredListener.call();
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.asamk.signal.manager.util.IOUtils;
|
|||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
|
@ -44,14 +45,20 @@ public class AttachmentHelper {
|
|||
}
|
||||
|
||||
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
|
||||
var attachmentStreams = createAttachmentStreams(attachments);
|
||||
final var attachmentStreams = createAttachmentStreams(attachments);
|
||||
|
||||
// Upload attachments here, so we only upload once even for multiple recipients
|
||||
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
|
||||
for (var attachmentStream : attachmentStreams) {
|
||||
attachmentPointers.add(uploadAttachment(attachmentStream));
|
||||
try {
|
||||
// Upload attachments here, so we only upload once even for multiple recipients
|
||||
final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
|
||||
for (final var attachmentStream : attachmentStreams) {
|
||||
attachmentPointers.add(uploadAttachment(attachmentStream));
|
||||
}
|
||||
return attachmentPointers;
|
||||
} finally {
|
||||
for (final var attachmentStream : attachmentStreams) {
|
||||
attachmentStream.close();
|
||||
}
|
||||
}
|
||||
return attachmentPointers;
|
||||
}
|
||||
|
||||
private List<SignalServiceAttachmentStream> createAttachmentStreams(List<String> attachments) throws AttachmentInvalidException, IOException {
|
||||
|
@ -104,9 +111,7 @@ public class AttachmentHelper {
|
|||
retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
|
||||
}
|
||||
|
||||
public void retrieveAttachment(
|
||||
SignalServiceAttachment attachment, AttachmentHandler consumer
|
||||
) throws IOException {
|
||||
public void retrieveAttachment(SignalServiceAttachment attachment, AttachmentHandler consumer) throws IOException {
|
||||
if (attachment.isStream()) {
|
||||
var input = attachment.asStream().getInputStream();
|
||||
// don't close input stream here, it might be reused later (e.g. with contact sync messages ...)
|
||||
|
@ -131,11 +136,18 @@ public class AttachmentHelper {
|
|||
}
|
||||
|
||||
private InputStream retrieveAttachmentAsStream(
|
||||
SignalServiceAttachmentPointer pointer, File tmpFile
|
||||
SignalServiceAttachmentPointer pointer,
|
||||
File tmpFile
|
||||
) throws IOException {
|
||||
if (pointer.getDigest().isEmpty()) {
|
||||
throw new IOException("Attachment pointer has no digest.");
|
||||
}
|
||||
try {
|
||||
return dependencies.getMessageReceiver()
|
||||
.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE);
|
||||
.retrieveAttachment(pointer,
|
||||
tmpFile,
|
||||
ServiceConfig.MAX_ATTACHMENT_SIZE,
|
||||
AttachmentCipherInputStream.IntegrityCheck.forEncryptedDigest(pointer.getDigest().get()));
|
||||
} catch (MissingConfigurationException | InvalidMessageException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,14 @@ public class ContactHelper {
|
|||
return sourceContact != null && sourceContact.isBlocked();
|
||||
}
|
||||
|
||||
public void setContactName(final RecipientId recipientId, final String givenName, final String familyName) {
|
||||
public void setContactName(
|
||||
final RecipientId recipientId,
|
||||
final String givenName,
|
||||
final String familyName,
|
||||
final String nickGivenName,
|
||||
final String nickFamilyName,
|
||||
final String note
|
||||
) {
|
||||
var contact = account.getContactStore().getContact(recipientId);
|
||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||
builder.withIsHidden(false);
|
||||
|
@ -27,6 +34,15 @@ public class ContactHelper {
|
|||
if (familyName != null) {
|
||||
builder.withFamilyName(familyName);
|
||||
}
|
||||
if (nickGivenName != null) {
|
||||
builder.withNickNameGivenName(nickGivenName);
|
||||
}
|
||||
if (nickFamilyName != null) {
|
||||
builder.withNickNameFamilyName(nickFamilyName);
|
||||
}
|
||||
if (note != null) {
|
||||
builder.withNote(note);
|
||||
}
|
||||
account.getContactStore().storeContact(recipientId, builder.build());
|
||||
}
|
||||
|
||||
|
@ -49,7 +65,9 @@ public class ContactHelper {
|
|||
}
|
||||
|
||||
public void setExpirationTimer(
|
||||
RecipientId recipientId, int messageExpirationTimer, int messageExpirationTimerVersion
|
||||
RecipientId recipientId,
|
||||
int messageExpirationTimer,
|
||||
int messageExpirationTimerVersion
|
||||
) {
|
||||
var contact = account.getContactStore().getContact(recipientId);
|
||||
if (contact != null && (
|
||||
|
|
|
@ -118,7 +118,9 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
public GroupInfoV2 getOrMigrateGroup(
|
||||
final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange
|
||||
final GroupMasterKey groupMasterKey,
|
||||
final int revision,
|
||||
final byte[] signedGroupChange
|
||||
) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
|
||||
|
@ -166,7 +168,8 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
private DecryptedGroup handleDecryptedGroupResponse(
|
||||
GroupInfoV2 groupInfoV2, final DecryptedGroupResponse decryptedGroupResponse
|
||||
GroupInfoV2 groupInfoV2,
|
||||
final DecryptedGroupResponse decryptedGroupResponse
|
||||
) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
||||
ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations()
|
||||
|
@ -181,7 +184,8 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
private GroupChange handleGroupChangeResponse(
|
||||
final GroupInfoV2 groupInfoV2, final GroupChangeResponse groupChangeResponse
|
||||
final GroupInfoV2 groupInfoV2,
|
||||
final GroupChangeResponse groupChangeResponse
|
||||
) {
|
||||
ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations()
|
||||
.forGroup(GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()))
|
||||
|
@ -195,7 +199,9 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String name, Set<RecipientId> members, String avatarFile
|
||||
String name,
|
||||
Set<RecipientId> members,
|
||||
String avatarFile
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
if (members != null && members.contains(selfRecipientId)) {
|
||||
|
@ -363,7 +369,8 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
public SendGroupMessageResults quitGroup(
|
||||
final GroupId groupId, final Set<RecipientId> newAdmins
|
||||
final GroupId groupId,
|
||||
final Set<RecipientId> newAdmins
|
||||
) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException {
|
||||
var group = getGroupForUpdating(groupId);
|
||||
if (group instanceof GroupInfoV1) {
|
||||
|
@ -396,9 +403,7 @@ public class GroupHelper {
|
|||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||
}
|
||||
|
||||
public SendGroupMessageResults sendGroupInfoRequest(
|
||||
GroupIdV1 groupId, RecipientId recipientId
|
||||
) throws IOException {
|
||||
public SendGroupMessageResults sendGroupInfoRequest(GroupIdV1 groupId, RecipientId recipientId) throws IOException {
|
||||
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize());
|
||||
|
||||
var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build());
|
||||
|
@ -408,7 +413,8 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
public SendGroupMessageResults sendGroupInfoMessage(
|
||||
GroupIdV1 groupId, RecipientId recipientId
|
||||
GroupIdV1 groupId,
|
||||
RecipientId recipientId
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
|
||||
GroupInfoV1 g;
|
||||
var group = getGroupForUpdating(groupId);
|
||||
|
@ -480,7 +486,9 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
private void retrieveGroupV2Avatar(
|
||||
GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream
|
||||
GroupSecretParams groupSecretParams,
|
||||
String cdnKey,
|
||||
OutputStream outputStream
|
||||
) throws IOException {
|
||||
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
|
||||
|
||||
|
@ -543,6 +551,9 @@ public class GroupHelper {
|
|||
while (true) {
|
||||
final var page = context.getGroupV2Helper()
|
||||
.getDecryptedGroupHistoryPage(groupSecretParams, fromRevision, sendEndorsementsExpirationMs);
|
||||
if (page == null) {
|
||||
break;
|
||||
}
|
||||
page.getChangeLogs()
|
||||
.stream()
|
||||
.map(DecryptedGroupChangeLog::getChange)
|
||||
|
@ -583,7 +594,10 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
private SendGroupMessageResults updateGroupV1(
|
||||
final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final byte[] avatarFile
|
||||
final GroupInfoV1 gv1,
|
||||
final String name,
|
||||
final Set<RecipientId> members,
|
||||
final byte[] avatarFile
|
||||
) throws IOException, AttachmentInvalidException {
|
||||
updateGroupV1Details(gv1, name, members, avatarFile);
|
||||
|
||||
|
@ -596,7 +610,10 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
private void updateGroupV1Details(
|
||||
final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final byte[] avatarFile
|
||||
final GroupInfoV1 g,
|
||||
final String name,
|
||||
final Collection<RecipientId> members,
|
||||
final byte[] avatarFile
|
||||
) throws IOException {
|
||||
if (name != null) {
|
||||
g.name = name;
|
||||
|
@ -615,7 +632,8 @@ public class GroupHelper {
|
|||
* Change the expiration timer for a group
|
||||
*/
|
||||
private void setExpirationTimer(
|
||||
GroupInfoV1 groupInfoV1, int messageExpirationTimer
|
||||
GroupInfoV1 groupInfoV1,
|
||||
int messageExpirationTimer
|
||||
) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException {
|
||||
groupInfoV1.messageExpirationTime = messageExpirationTimer;
|
||||
account.getGroupStore().updateGroup(groupInfoV1);
|
||||
|
@ -828,7 +846,8 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
private SendGroupMessageResults quitGroupV2(
|
||||
final GroupInfoV2 groupInfoV2, final Set<RecipientId> newAdmins
|
||||
final GroupInfoV2 groupInfoV2,
|
||||
final Set<RecipientId> newAdmins
|
||||
) throws LastGroupAdminException, IOException {
|
||||
final var currentAdmins = groupInfoV2.getAdminMembers();
|
||||
newAdmins.removeAll(currentAdmins);
|
||||
|
@ -882,7 +901,9 @@ public class GroupHelper {
|
|||
}
|
||||
|
||||
private SendGroupMessageResults sendUpdateGroupV2Message(
|
||||
GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
|
||||
GroupInfoV2 group,
|
||||
DecryptedGroup newDecryptedGroup,
|
||||
GroupChange groupChange
|
||||
) throws IOException {
|
||||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
final var members = group.getMembersIncludingPendingWithout(selfRecipientId);
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
|||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptChangeVerificationMode;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
|
||||
|
@ -43,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
|||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -82,7 +84,7 @@ class GroupV2Helper {
|
|||
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
|
||||
return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.getCode() == 403) {
|
||||
if (e.code == 403) {
|
||||
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
|
||||
}
|
||||
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
|
||||
|
@ -94,7 +96,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
|
||||
GroupMasterKey groupMasterKey, GroupLinkPassword password
|
||||
GroupMasterKey groupMasterKey,
|
||||
GroupLinkPassword password
|
||||
) throws IOException, GroupLinkNotActiveException {
|
||||
var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
|
||||
|
@ -105,7 +108,9 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
GroupHistoryPage getDecryptedGroupHistoryPage(
|
||||
final GroupSecretParams groupSecretParams, int fromRevision, long sendEndorsementsExpirationMs
|
||||
final GroupSecretParams groupSecretParams,
|
||||
int fromRevision,
|
||||
long sendEndorsementsExpirationMs
|
||||
) throws NotAGroupMemberException {
|
||||
try {
|
||||
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
|
||||
|
@ -115,8 +120,10 @@ class GroupV2Helper {
|
|||
groupsV2AuthorizationString,
|
||||
false,
|
||||
sendEndorsementsExpirationMs);
|
||||
} catch (NotInGroupException e) {
|
||||
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.getCode() == 403) {
|
||||
if (e.code == 403) {
|
||||
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
|
||||
}
|
||||
logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
|
||||
|
@ -138,9 +145,7 @@ class GroupV2Helper {
|
|||
return partialDecryptedGroup.revision;
|
||||
}
|
||||
|
||||
Pair<GroupInfoV2, DecryptedGroupResponse> createGroup(
|
||||
String name, Set<RecipientId> members, byte[] avatarFile
|
||||
) {
|
||||
Pair<GroupInfoV2, DecryptedGroupResponse> createGroup(String name, Set<RecipientId> members, byte[] avatarFile) {
|
||||
final var newGroup = buildNewGroup(name, members, avatarFile);
|
||||
if (newGroup == null) {
|
||||
return null;
|
||||
|
@ -170,9 +175,7 @@ class GroupV2Helper {
|
|||
return new Pair<>(g, response);
|
||||
}
|
||||
|
||||
private GroupsV2Operations.NewGroup buildNewGroup(
|
||||
String name, Set<RecipientId> members, byte[] avatar
|
||||
) {
|
||||
private GroupsV2Operations.NewGroup buildNewGroup(String name, Set<RecipientId> members, byte[] avatar) {
|
||||
final var profileKeyCredential = context.getProfileHelper()
|
||||
.getExpiringProfileKeyCredential(context.getAccount().getSelfRecipientId());
|
||||
if (profileKeyCredential == null) {
|
||||
|
@ -202,7 +205,10 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> updateGroup(
|
||||
GroupInfoV2 groupInfoV2, String name, String description, byte[] avatarFile
|
||||
GroupInfoV2 groupInfoV2,
|
||||
String name,
|
||||
String description,
|
||||
byte[] avatarFile
|
||||
) throws IOException {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
||||
var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
|
||||
|
@ -225,7 +231,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> addMembers(
|
||||
GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> newMembers
|
||||
) throws IOException {
|
||||
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
|
||||
|
@ -251,7 +258,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> leaveGroup(
|
||||
GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> membersToMakeAdmin
|
||||
) throws IOException {
|
||||
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
|
||||
final var selfAci = getSelfAci();
|
||||
|
@ -271,7 +279,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> removeMembers(
|
||||
GroupInfoV2 groupInfoV2, Set<RecipientId> members
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> members
|
||||
) throws IOException {
|
||||
final var memberUuids = members.stream()
|
||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||
|
@ -283,7 +292,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> approveJoinRequestMembers(
|
||||
GroupInfoV2 groupInfoV2, Set<RecipientId> members
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> members
|
||||
) throws IOException {
|
||||
final var memberUuids = members.stream()
|
||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||
|
@ -294,7 +304,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> refuseJoinRequestMembers(
|
||||
GroupInfoV2 groupInfoV2, Set<RecipientId> members
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> members
|
||||
) throws IOException {
|
||||
final var memberUuids = members.stream()
|
||||
.map(context.getRecipientHelper()::resolveSignalServiceAddress)
|
||||
|
@ -304,7 +315,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> revokeInvitedMembers(
|
||||
GroupInfoV2 groupInfoV2, Set<RecipientId> members
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> members
|
||||
) throws IOException {
|
||||
var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
|
||||
final var memberUuids = members.stream()
|
||||
|
@ -318,7 +330,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> banMembers(
|
||||
GroupInfoV2 groupInfoV2, Set<RecipientId> block
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> block
|
||||
) throws IOException {
|
||||
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
|
||||
|
@ -336,7 +349,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> unbanMembers(
|
||||
GroupInfoV2 groupInfoV2, Set<RecipientId> block
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<RecipientId> block
|
||||
) throws IOException {
|
||||
GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
|
||||
|
@ -359,7 +373,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setGroupLinkState(
|
||||
GroupInfoV2 groupInfoV2, GroupLinkState state
|
||||
GroupInfoV2 groupInfoV2,
|
||||
GroupLinkState state
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
|
||||
|
@ -374,7 +389,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setEditDetailsPermission(
|
||||
GroupInfoV2 groupInfoV2, GroupPermission permission
|
||||
GroupInfoV2 groupInfoV2,
|
||||
GroupPermission permission
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
|
||||
|
@ -384,7 +400,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setAddMemberPermission(
|
||||
GroupInfoV2 groupInfoV2, GroupPermission permission
|
||||
GroupInfoV2 groupInfoV2,
|
||||
GroupPermission permission
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
|
||||
|
@ -468,7 +485,9 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
|
||||
GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
|
||||
GroupInfoV2 groupInfoV2,
|
||||
RecipientId recipientId,
|
||||
boolean admin
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
|
||||
|
@ -482,7 +501,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setMessageExpirationTimer(
|
||||
GroupInfoV2 groupInfoV2, int messageExpirationTimer
|
||||
GroupInfoV2 groupInfoV2,
|
||||
int messageExpirationTimer
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
|
||||
|
@ -490,7 +510,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
Pair<DecryptedGroup, GroupChangeResponse> setIsAnnouncementGroup(
|
||||
GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup
|
||||
GroupInfoV2 groupInfoV2,
|
||||
boolean isAnnouncementGroup
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup);
|
||||
|
@ -518,7 +539,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
private Pair<DecryptedGroup, GroupChangeResponse> revokeInvites(
|
||||
GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<DecryptedPendingMember> pendingMembers
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
final var uuidCipherTexts = pendingMembers.stream().map(member -> {
|
||||
|
@ -532,28 +554,32 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
private Pair<DecryptedGroup, GroupChangeResponse> approveJoinRequest(
|
||||
GroupInfoV2 groupInfoV2, Set<UUID> uuids
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<UUID> uuids
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
return commitChange(groupInfoV2, groupOperations.createApproveGroupJoinRequest(uuids));
|
||||
}
|
||||
|
||||
private Pair<DecryptedGroup, GroupChangeResponse> refuseJoinRequest(
|
||||
GroupInfoV2 groupInfoV2, Set<ServiceId> serviceIds
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<ServiceId> serviceIds
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
return commitChange(groupInfoV2, groupOperations.createRefuseGroupJoinRequest(serviceIds, false, List.of()));
|
||||
}
|
||||
|
||||
private Pair<DecryptedGroup, GroupChangeResponse> ejectMembers(
|
||||
GroupInfoV2 groupInfoV2, Set<ACI> members
|
||||
GroupInfoV2 groupInfoV2,
|
||||
Set<ACI> members
|
||||
) throws IOException {
|
||||
final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
|
||||
return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(members, false, List.of()));
|
||||
}
|
||||
|
||||
private Pair<DecryptedGroup, GroupChangeResponse> commitChange(
|
||||
GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
|
||||
GroupInfoV2 groupInfoV2,
|
||||
GroupChange.Actions.Builder change
|
||||
) throws IOException {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
||||
final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
|
||||
|
@ -630,11 +656,13 @@ class GroupV2Helper {
|
|||
|
||||
DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
|
||||
if (signedGroupChange != null) {
|
||||
var groupOperations = dependencies.getGroupsV2Operations()
|
||||
.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
|
||||
final var groupId = groupSecretParams.getPublicParams().getGroupIdentifier();
|
||||
|
||||
try {
|
||||
return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange), true).orElse(null);
|
||||
return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange),
|
||||
DecryptChangeVerificationMode.verify(groupId)).orElse(null);
|
||||
} catch (VerificationFailedException | InvalidGroupStateException | IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
@ -676,7 +704,8 @@ class GroupV2Helper {
|
|||
}
|
||||
|
||||
private GroupsV2AuthorizationString getAuthorizationString(
|
||||
final GroupSecretParams groupSecretParams, final long todaySeconds
|
||||
final GroupSecretParams groupSecretParams,
|
||||
final long todaySeconds
|
||||
) throws VerificationFailedException {
|
||||
var authCredentialResponse = groupApiCredentials.get(todaySeconds);
|
||||
final var aci = getSelfAci();
|
||||
|
|
|
@ -66,9 +66,7 @@ public class IdentityHelper {
|
|||
return fingerprint == null ? null : fingerprint.getScannableFingerprint();
|
||||
}
|
||||
|
||||
private Fingerprint computeSafetyNumberFingerprint(
|
||||
final ServiceId serviceId, final IdentityKey theirIdentityKey
|
||||
) {
|
||||
private Fingerprint computeSafetyNumberFingerprint(final ServiceId serviceId, final IdentityKey theirIdentityKey) {
|
||||
if (!serviceId.isUnknown()) {
|
||||
return Utils.computeSafetyNumberForUuid(account.getAci(),
|
||||
account.getAciIdentityKeyPair().getPublicKey(),
|
||||
|
@ -89,7 +87,9 @@ public class IdentityHelper {
|
|||
}
|
||||
|
||||
private boolean trustIdentity(
|
||||
RecipientId recipientId, BiFunction<ServiceId, IdentityKey, Boolean> verifier, TrustLevel trustLevel
|
||||
RecipientId recipientId,
|
||||
BiFunction<ServiceId, IdentityKey, Boolean> verifier,
|
||||
TrustLevel trustLevel
|
||||
) {
|
||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
|
||||
final var serviceId = address.serviceId().orElse(null);
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.asamk.signal.manager.internal.SignalDependencies;
|
|||
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.stickers.StickerPack;
|
||||
import org.signal.libsignal.metadata.ProtocolInvalidKeyException;
|
||||
|
@ -40,6 +41,7 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException;
|
|||
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
|
||||
import org.signal.libsignal.metadata.SelfSendException;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.UsePqRatchet;
|
||||
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
|
@ -104,7 +106,7 @@ public final class IncomingMessageHandler {
|
|||
try {
|
||||
final var cipherResult = dependencies.getCipher(destination == null
|
||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
|
||||
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
||||
if (content == null) {
|
||||
return new Pair<>(List.of(), null);
|
||||
|
@ -142,7 +144,7 @@ public final class IncomingMessageHandler {
|
|||
try {
|
||||
final var cipherResult = dependencies.getCipher(destination == null
|
||||
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
|
||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
|
||||
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp(), UsePqRatchet.NO);
|
||||
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
|
||||
if (content == null) {
|
||||
return new Pair<>(List.of(), null);
|
||||
|
@ -156,6 +158,9 @@ public final class IncomingMessageHandler {
|
|||
} catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException |
|
||||
ProtocolInvalidMessageException e) {
|
||||
logger.debug("Failed to decrypt incoming message", e);
|
||||
if (e instanceof ProtocolInvalidKeyIdException) {
|
||||
actions.add(RefreshPreKeysAction.create());
|
||||
}
|
||||
final var sender = account.getRecipientResolver().resolveRecipient(e.getSender());
|
||||
if (context.getContactHelper().isContactBlocked(sender)) {
|
||||
logger.debug("Received invalid message from blocked contact, ignoring.");
|
||||
|
@ -164,12 +169,11 @@ public final class IncomingMessageHandler {
|
|||
if (serviceId != null) {
|
||||
final var isSelf = sender.equals(account.getSelfRecipientId())
|
||||
&& e.getSenderDevice() == account.getDeviceId();
|
||||
logger.debug("Received invalid message, queuing renew session action.");
|
||||
actions.add(new RenewSessionAction(sender, serviceId, destination));
|
||||
if (!isSelf) {
|
||||
logger.debug("Received invalid message, requesting message resend.");
|
||||
actions.add(new SendRetryMessageRequestAction(sender, serviceId, e, envelope, destination));
|
||||
} else {
|
||||
logger.debug("Received invalid message, queuing renew session action.");
|
||||
actions.add(new RenewSessionAction(sender, serviceId, destination));
|
||||
actions.add(new SendRetryMessageRequestAction(sender, e, envelope));
|
||||
}
|
||||
} else {
|
||||
logger.debug("Received invalid message from invalid sender: {}", e.getSender());
|
||||
|
@ -190,7 +194,9 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
|
||||
private SignalServiceContent validate(
|
||||
Envelope envelope, SignalServiceCipherResult cipherResult, long serverDeliveredTimestamp
|
||||
Envelope envelope,
|
||||
SignalServiceCipherResult cipherResult,
|
||||
long serverDeliveredTimestamp
|
||||
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException {
|
||||
final var content = cipherResult.getContent();
|
||||
final var envelopeMetadata = cipherResult.getMetadata();
|
||||
|
@ -280,7 +286,9 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
|
||||
public List<HandleAction> handleMessage(
|
||||
SignalServiceEnvelope envelope, SignalServiceContent content, ReceiveConfig receiveConfig
|
||||
SignalServiceEnvelope envelope,
|
||||
SignalServiceContent content,
|
||||
ReceiveConfig receiveConfig
|
||||
) {
|
||||
var actions = new ArrayList<HandleAction>();
|
||||
final var senderDeviceAddress = getSender(envelope, content);
|
||||
|
@ -381,7 +389,8 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
|
||||
private boolean handlePniSignatureMessage(
|
||||
final SignalServicePniSignatureMessage message, final SignalServiceAddress senderAddress
|
||||
final SignalServicePniSignatureMessage message,
|
||||
final SignalServiceAddress senderAddress
|
||||
) {
|
||||
final var aci = senderAddress.getServiceId();
|
||||
final var aciIdentity = account.getIdentityKeyStore().getIdentityInfo(aci);
|
||||
|
@ -520,12 +529,12 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
if (syncMessage.getBlockedList().isPresent()) {
|
||||
final var blockedListMessage = syncMessage.getBlockedList().get();
|
||||
for (var address : blockedListMessage.getAddresses()) {
|
||||
context.getContactHelper()
|
||||
.setContactBlocked(account.getRecipientResolver().resolveRecipient(address), true);
|
||||
for (var individual : blockedListMessage.individuals) {
|
||||
final var address = new RecipientAddress(individual.getAci(), individual.getE164());
|
||||
final var recipientId = account.getRecipientResolver().resolveRecipient(address);
|
||||
context.getContactHelper().setContactBlocked(recipientId, true);
|
||||
}
|
||||
for (var groupId : blockedListMessage.getGroupIds()
|
||||
.stream()
|
||||
for (var groupId : blockedListMessage.groupIds.stream()
|
||||
.map(GroupId::unknownVersion)
|
||||
.collect(Collectors.toSet())) {
|
||||
try {
|
||||
|
@ -580,14 +589,22 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
if (syncMessage.getKeys().isPresent()) {
|
||||
final var keysMessage = syncMessage.getKeys().get();
|
||||
if (keysMessage.getStorageService().isPresent()) {
|
||||
final var storageKey = keysMessage.getStorageService().get();
|
||||
if (keysMessage.getAccountEntropyPool() != null) {
|
||||
final var aep = keysMessage.getAccountEntropyPool();
|
||||
account.setAccountEntropyPool(aep);
|
||||
actions.add(SyncStorageDataAction.create());
|
||||
} else if (keysMessage.getMaster() != null) {
|
||||
final var masterKey = keysMessage.getMaster();
|
||||
account.setMasterKey(masterKey);
|
||||
actions.add(SyncStorageDataAction.create());
|
||||
} else if (keysMessage.getStorageService() != null) {
|
||||
final var storageKey = keysMessage.getStorageService();
|
||||
account.setStorageKey(storageKey);
|
||||
actions.add(SyncStorageDataAction.create());
|
||||
}
|
||||
if (keysMessage.getMaster().isPresent()) {
|
||||
final var masterKey = keysMessage.getMaster().get();
|
||||
account.setMasterKey(masterKey);
|
||||
if (keysMessage.getMediaRootBackupKey() != null) {
|
||||
final var mrb = keysMessage.getMediaRootBackupKey();
|
||||
account.setMediaRootBackupKey(mrb);
|
||||
actions.add(SyncStorageDataAction.create());
|
||||
}
|
||||
}
|
||||
|
@ -865,7 +882,9 @@ public final class IncomingMessageHandler {
|
|||
}
|
||||
|
||||
private List<HandleAction> handleSignalServiceStoryMessage(
|
||||
SignalServiceStoryMessage message, RecipientId source, boolean ignoreAttachments
|
||||
SignalServiceStoryMessage message,
|
||||
RecipientId source,
|
||||
boolean ignoreAttachments
|
||||
) {
|
||||
var actions = new ArrayList<HandleAction>();
|
||||
if (message.getGroupContext().isPresent()) {
|
||||
|
@ -946,7 +965,7 @@ public final class IncomingMessageHandler {
|
|||
|
||||
private DeviceAddress getDestination(SignalServiceEnvelope envelope) {
|
||||
final var destination = envelope.getDestinationServiceId();
|
||||
if (destination == null) {
|
||||
if (destination == null || destination.isUnknown()) {
|
||||
return new DeviceAddress(account.getSelfRecipientId(), account.getAci(), account.getDeviceId());
|
||||
}
|
||||
return new DeviceAddress(account.getRecipientResolver().resolveRecipient(destination),
|
||||
|
|
|
@ -21,9 +21,7 @@ public class PinHelper {
|
|||
this.secureValueRecoveries = secureValueRecoveries;
|
||||
}
|
||||
|
||||
public void setRegistrationLockPin(
|
||||
String pin, MasterKey masterKey
|
||||
) throws IOException {
|
||||
public void setRegistrationLockPin(String pin, MasterKey masterKey) throws IOException {
|
||||
IOException exception = null;
|
||||
for (final var secureValueRecovery : secureValueRecoveries) {
|
||||
try {
|
||||
|
@ -82,14 +80,19 @@ public class PinHelper {
|
|||
}
|
||||
|
||||
public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData(
|
||||
String pin, LockedException lockedException
|
||||
String pin,
|
||||
LockedException lockedException
|
||||
) throws IOException, IncorrectPinException {
|
||||
var svr2Credentials = lockedException.getSvr2Credentials();
|
||||
if (svr2Credentials != null) {
|
||||
IOException exception = null;
|
||||
for (final var secureValueRecovery : secureValueRecoveries) {
|
||||
try {
|
||||
return getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
|
||||
final var lockData = getRegistrationLockData(secureValueRecovery, svr2Credentials, pin);
|
||||
if (lockData == null) {
|
||||
continue;
|
||||
}
|
||||
return lockData;
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
}
|
||||
|
@ -103,7 +106,9 @@ public class PinHelper {
|
|||
}
|
||||
|
||||
public SecureValueRecovery.RestoreResponse.Success getRegistrationLockData(
|
||||
SecureValueRecovery secureValueRecovery, AuthCredentials authCredentials, String pin
|
||||
SecureValueRecovery secureValueRecovery,
|
||||
AuthCredentials authCredentials,
|
||||
String pin
|
||||
) throws IOException, IncorrectPinException {
|
||||
final var restoreResponse = secureValueRecovery.restoreDataPreRegistration(authCredentials, null, pin);
|
||||
|
||||
|
|
|
@ -11,17 +11,19 @@ import org.signal.libsignal.protocol.state.PreKeyRecord;
|
|||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload;
|
||||
import org.whispersystems.signalservice.api.keys.OneTimePreKeyCounts;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_STALE_AGE;
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.SIGNED_PREKEY_ROTATE_AGE;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
|
||||
public class PreKeyHelper {
|
||||
|
||||
|
@ -30,9 +32,7 @@ public class PreKeyHelper {
|
|||
private final SignalAccount account;
|
||||
private final SignalDependencies dependencies;
|
||||
|
||||
public PreKeyHelper(
|
||||
final SignalAccount account, final SignalDependencies dependencies
|
||||
) {
|
||||
public PreKeyHelper(final SignalAccount account, final SignalDependencies dependencies) {
|
||||
this.account = account;
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
@ -79,11 +79,12 @@ public class PreKeyHelper {
|
|||
}
|
||||
|
||||
private boolean refreshPreKeysIfNecessary(
|
||||
final ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair
|
||||
final ServiceIdType serviceIdType,
|
||||
final IdentityKeyPair identityKeyPair
|
||||
) throws IOException {
|
||||
OneTimePreKeyCounts preKeyCounts;
|
||||
try {
|
||||
preKeyCounts = dependencies.getAccountManager().getPreKeyCounts(serviceIdType);
|
||||
preKeyCounts = handleResponseException(dependencies.getKeysApi().getAvailablePreKeyCounts(serviceIdType));
|
||||
} catch (AuthorizationFailedException e) {
|
||||
logger.debug("Failed to get pre key count, ignoring: " + e.getClass().getSimpleName());
|
||||
preKeyCounts = new OneTimePreKeyCounts(0, 0);
|
||||
|
@ -144,7 +145,7 @@ public class PreKeyHelper {
|
|||
kyberPreKeyRecords);
|
||||
var needsReset = false;
|
||||
try {
|
||||
dependencies.getAccountManager().setPreKeys(preKeyUpload);
|
||||
NetworkResultUtil.toPreKeysLegacy(dependencies.getKeysApi().setPreKeys(preKeyUpload));
|
||||
try {
|
||||
if (preKeyRecords != null) {
|
||||
account.addPreKeys(serviceIdType, preKeyRecords);
|
||||
|
@ -173,7 +174,7 @@ public class PreKeyHelper {
|
|||
// This can happen when the primary device has changed phone number
|
||||
logger.warn("Failed to updated pre keys: {}", e.getMessage());
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (serviceIdType != ServiceIdType.PNI || e.getCode() != 422) {
|
||||
if (serviceIdType != ServiceIdType.PNI || e.code != 422) {
|
||||
throw e;
|
||||
}
|
||||
logger.warn("Failed to set PNI pre keys, ignoring for now. Account needs to be reregistered to fix this.");
|
||||
|
@ -221,7 +222,8 @@ public class PreKeyHelper {
|
|||
}
|
||||
|
||||
private List<KyberPreKeyRecord> generateKyberPreKeys(
|
||||
ServiceIdType serviceIdType, final IdentityKeyPair identityKeyPair
|
||||
ServiceIdType serviceIdType,
|
||||
final IdentityKeyPair identityKeyPair
|
||||
) {
|
||||
final var accountData = account.getAccountData(serviceIdType);
|
||||
final var offset = accountData.getPreKeyMetadata().getNextKyberPreKeyId();
|
||||
|
@ -246,7 +248,9 @@ public class PreKeyHelper {
|
|||
}
|
||||
|
||||
private KyberPreKeyRecord generateLastResortKyberPreKey(
|
||||
ServiceIdType serviceIdType, IdentityKeyPair identityKeyPair, final int offset
|
||||
ServiceIdType serviceIdType,
|
||||
IdentityKeyPair identityKeyPair,
|
||||
final int offset
|
||||
) {
|
||||
final var accountData = account.getAccountData(serviceIdType);
|
||||
final var signedPreKeyId = accountData.getPreKeyMetadata().getNextKyberPreKeyId() + offset;
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
|
|||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.NetworkResultUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
|
||||
import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
|
||||
|
@ -196,9 +197,10 @@ public final class ProfileHelper {
|
|||
: avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
|
||||
final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress())
|
||||
.map(address -> PaymentUtils.signPaymentsAddress(address,
|
||||
account.getAciIdentityKeyPair().getPrivateKey()));
|
||||
account.getAciIdentityKeyPair().getPrivateKey()))
|
||||
.orElse(null);
|
||||
logger.debug("Uploading new profile");
|
||||
final var avatarPath = dependencies.getAccountManager()
|
||||
final var avatarPath = NetworkResultUtil.toSetProfileLegacy(dependencies.getProfileApi()
|
||||
.setVersionedProfile(account.getAci(),
|
||||
account.getProfileKey(),
|
||||
newProfile.getInternalServiceName(),
|
||||
|
@ -208,9 +210,9 @@ public final class ProfileHelper {
|
|||
avatarUploadParams,
|
||||
List.of(/* TODO implement support for badges */),
|
||||
account.getConfigurationStore().getPhoneNumberSharingMode()
|
||||
== PhoneNumberSharingMode.EVERYBODY);
|
||||
== PhoneNumberSharingMode.EVERYBODY));
|
||||
if (!avatarUploadParams.keepTheSame) {
|
||||
builder.withAvatarUrlPath(avatarPath.orElse(null));
|
||||
builder.withAvatarUrlPath(avatarPath);
|
||||
}
|
||||
newProfile = builder.build();
|
||||
}
|
||||
|
@ -271,7 +273,9 @@ public final class ProfileHelper {
|
|||
}
|
||||
|
||||
private Profile decryptProfileAndDownloadAvatar(
|
||||
final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
|
||||
final RecipientId recipientId,
|
||||
final ProfileKey profileKey,
|
||||
final SignalServiceProfile encryptedProfile
|
||||
) {
|
||||
final var avatarPath = encryptedProfile.getAvatar();
|
||||
downloadProfileAvatar(recipientId, avatarPath, profileKey);
|
||||
|
@ -280,7 +284,9 @@ public final class ProfileHelper {
|
|||
}
|
||||
|
||||
public void downloadProfileAvatar(
|
||||
final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey
|
||||
final RecipientId recipientId,
|
||||
final String avatarPath,
|
||||
final ProfileKey profileKey
|
||||
) {
|
||||
var profile = account.getProfileStore().getProfile(recipientId);
|
||||
if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) {
|
||||
|
@ -308,7 +314,8 @@ public final class ProfileHelper {
|
|||
}
|
||||
|
||||
private Single<ProfileAndCredential> retrieveProfile(
|
||||
RecipientId recipientId, SignalServiceProfile.RequestType requestType
|
||||
RecipientId recipientId,
|
||||
SignalServiceProfile.RequestType requestType
|
||||
) {
|
||||
var unidentifiedAccess = getUnidentifiedAccess(recipientId);
|
||||
var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId));
|
||||
|
@ -331,13 +338,6 @@ public final class ProfileHelper {
|
|||
|
||||
final var profile = account.getProfileStore().getProfile(recipientId);
|
||||
|
||||
if (recipientId.equals(account.getSelfRecipientId())) {
|
||||
final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
|
||||
if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
|
||||
account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
|
||||
}
|
||||
}
|
||||
|
||||
Profile newProfile = null;
|
||||
if (profileKey.isPresent()) {
|
||||
logger.trace("Decrypting profile");
|
||||
|
@ -353,6 +353,18 @@ public final class ProfileHelper {
|
|||
.build();
|
||||
}
|
||||
|
||||
if (recipientId.equals(account.getSelfRecipientId())) {
|
||||
final var isUnrestricted = encryptedProfile.isUnrestrictedUnidentifiedAccess();
|
||||
if (account.isUnrestrictedUnidentifiedAccess() != isUnrestricted) {
|
||||
account.setUnrestrictedUnidentifiedAccess(isUnrestricted);
|
||||
}
|
||||
if (account.isPrimaryDevice() && profile != null && newProfile.getCapabilities()
|
||||
.contains(Profile.Capability.storageServiceEncryptionV2Capability) && !profile.getCapabilities()
|
||||
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
|
||||
context.getJobExecutor().enqueueJob(new SyncStorageJob(true));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
logger.trace("Storing identity");
|
||||
final var identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()));
|
||||
|
@ -408,9 +420,7 @@ public final class ProfileHelper {
|
|||
});
|
||||
}
|
||||
|
||||
private void downloadProfileAvatar(
|
||||
RecipientAddress address, String avatarPath, ProfileKey profileKey
|
||||
) {
|
||||
private void downloadProfileAvatar(RecipientAddress address, String avatarPath, ProfileKey profileKey) {
|
||||
if (avatarPath == null) {
|
||||
try {
|
||||
context.getAvatarStore().deleteProfileAvatar(address);
|
||||
|
@ -430,7 +440,9 @@ public final class ProfileHelper {
|
|||
}
|
||||
|
||||
private void retrieveProfileAvatar(
|
||||
String avatarPath, ProfileKey profileKey, OutputStream outputStream
|
||||
String avatarPath,
|
||||
ProfileKey profileKey,
|
||||
OutputStream outputStream
|
||||
) throws IOException {
|
||||
var tmpFile = IOUtils.createTempFile();
|
||||
try (var input = dependencies.getMessageReceiver()
|
||||
|
|
|
@ -11,10 +11,10 @@ import org.asamk.signal.manager.storage.messageCache.CachedMessage;
|
|||
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException;
|
||||
|
||||
|
@ -28,7 +28,6 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class ReceiveHelper {
|
||||
|
@ -83,7 +82,10 @@ public class ReceiveHelper {
|
|||
}
|
||||
|
||||
public void receiveMessages(
|
||||
Duration timeout, boolean returnOnTimeout, Integer maxMessages, Manager.ReceiveMessageHandler handler
|
||||
Duration timeout,
|
||||
boolean returnOnTimeout,
|
||||
Integer maxMessages,
|
||||
Manager.ReceiveMessageHandler handler
|
||||
) throws IOException {
|
||||
account.setNeedsToRetryFailedMessages(true);
|
||||
hasCaughtUpWithOldMessages = false;
|
||||
|
@ -91,14 +93,14 @@ public class ReceiveHelper {
|
|||
// Use a Map here because java Set doesn't have a get method ...
|
||||
Map<HandleAction, HandleAction> queuedActions = new HashMap<>();
|
||||
|
||||
final var signalWebSocket = dependencies.getSignalWebSocket();
|
||||
final var webSocketStateDisposable = Observable.merge(signalWebSocket.getUnidentifiedWebSocketState(),
|
||||
signalWebSocket.getWebSocketState())
|
||||
final var signalWebSocket = dependencies.getAuthenticatedSignalWebSocket();
|
||||
final var webSocketStateDisposable = signalWebSocket.getState()
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(Schedulers.computation())
|
||||
.distinctUntilChanged()
|
||||
.subscribe(this::onWebSocketStateChange);
|
||||
signalWebSocket.connect();
|
||||
signalWebSocket.registerKeepAliveToken("receive");
|
||||
|
||||
try {
|
||||
receiveMessagesInternal(signalWebSocket, timeout, returnOnTimeout, maxMessages, handler, queuedActions);
|
||||
|
@ -106,6 +108,7 @@ public class ReceiveHelper {
|
|||
hasCaughtUpWithOldMessages = false;
|
||||
handleQueuedActions(queuedActions.keySet());
|
||||
queuedActions.clear();
|
||||
signalWebSocket.removeKeepAliveToken("receive");
|
||||
signalWebSocket.disconnect();
|
||||
webSocketStateDisposable.dispose();
|
||||
shouldStop = false;
|
||||
|
@ -113,7 +116,7 @@ public class ReceiveHelper {
|
|||
}
|
||||
|
||||
private void receiveMessagesInternal(
|
||||
final SignalWebSocket signalWebSocket,
|
||||
final SignalWebSocket.AuthenticatedWebSocket signalWebSocket,
|
||||
Duration timeout,
|
||||
boolean returnOnTimeout,
|
||||
Integer maxMessages,
|
||||
|
@ -264,7 +267,8 @@ public class ReceiveHelper {
|
|||
}
|
||||
|
||||
private List<HandleAction> retryFailedReceivedMessage(
|
||||
final Manager.ReceiveMessageHandler handler, final CachedMessage cachedMessage
|
||||
final Manager.ReceiveMessageHandler handler,
|
||||
final CachedMessage cachedMessage
|
||||
) {
|
||||
var envelope = cachedMessage.loadEnvelope();
|
||||
if (envelope == null) {
|
||||
|
|
|
@ -10,13 +10,14 @@ import org.signal.libsignal.usernames.BaseUsernameException;
|
|||
import org.signal.libsignal.usernames.Username;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.cds.CdsiV2Service;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
|
||||
import org.whispersystems.signalservice.api.services.CdsiV2Service;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
@ -25,8 +26,10 @@ import java.util.HashSet;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
|
||||
public class RecipientHelper {
|
||||
|
||||
|
@ -66,7 +69,7 @@ public class RecipientHelper {
|
|||
.toSignalServiceAddress();
|
||||
}
|
||||
|
||||
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
|
||||
public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException, IOException {
|
||||
final var recipientIds = new HashSet<RecipientId>(recipients.size());
|
||||
for (var number : recipients) {
|
||||
final var recipientId = resolveRecipient(number);
|
||||
|
@ -76,12 +79,11 @@ public class RecipientHelper {
|
|||
}
|
||||
|
||||
public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
|
||||
if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
|
||||
return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid()));
|
||||
} else if (recipient instanceof RecipientIdentifier.Pni pniRecipient) {
|
||||
return account.getRecipientResolver().resolveRecipient(PNI.parseOrThrow(pniRecipient.pni()));
|
||||
} else if (recipient instanceof RecipientIdentifier.Number numberRecipient) {
|
||||
final var number = numberRecipient.number();
|
||||
if (recipient instanceof RecipientIdentifier.Uuid(UUID uuid)) {
|
||||
return account.getRecipientResolver().resolveRecipient(ACI.from(uuid));
|
||||
} else if (recipient instanceof RecipientIdentifier.Pni(UUID pni)) {
|
||||
return account.getRecipientResolver().resolveRecipient(PNI.from(pni));
|
||||
} else if (recipient instanceof RecipientIdentifier.Number(String number)) {
|
||||
return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
|
||||
try {
|
||||
return getRegisteredUserByNumber(number);
|
||||
|
@ -89,16 +91,20 @@ public class RecipientHelper {
|
|||
return null;
|
||||
}
|
||||
});
|
||||
} else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
|
||||
var username = usernameRecipient.username();
|
||||
return resolveRecipientByUsernameOrLink(username, false);
|
||||
} else if (recipient instanceof RecipientIdentifier.Username(String username)) {
|
||||
try {
|
||||
return resolveRecipientByUsernameOrLink(username, false);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
|
||||
}
|
||||
|
||||
public RecipientId resolveRecipientByUsernameOrLink(
|
||||
String username, boolean forceRefresh
|
||||
) throws UnregisteredRecipientException {
|
||||
String username,
|
||||
boolean forceRefresh
|
||||
) throws UnregisteredRecipientException, IOException {
|
||||
final Username finalUsername;
|
||||
try {
|
||||
finalUsername = getUsernameFromUsernameOrLink(username);
|
||||
|
@ -107,18 +113,22 @@ public class RecipientHelper {
|
|||
}
|
||||
if (forceRefresh) {
|
||||
try {
|
||||
final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername);
|
||||
final var aci = handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
|
||||
return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
|
||||
} catch (IOException e) {
|
||||
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
||||
null,
|
||||
null,
|
||||
username));
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (e.code == 404) {
|
||||
throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
|
||||
null,
|
||||
null,
|
||||
username));
|
||||
}
|
||||
logger.debug("Failed to get uuid for username: {}", username, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
|
||||
try {
|
||||
return dependencies.getAccountManager().getAciByUsername(finalUsername);
|
||||
return handleResponseException(dependencies.getUsernameApi().getAciByUsername(finalUsername));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
|
@ -129,8 +139,8 @@ public class RecipientHelper {
|
|||
try {
|
||||
final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
|
||||
final var components = usernameLinkUrl.getComponents();
|
||||
final var encryptedUsername = dependencies.getAccountManager()
|
||||
.getEncryptedUsernameFromLinkServerId(components.getServerId());
|
||||
final var encryptedUsername = handleResponseException(dependencies.getUsernameApi()
|
||||
.getEncryptedUsernameFromLinkServerId(components.getServerId()));
|
||||
final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
|
||||
|
||||
return Username.fromLink(link);
|
||||
|
@ -143,8 +153,8 @@ public class RecipientHelper {
|
|||
try {
|
||||
return Optional.of(resolveRecipient(recipient));
|
||||
} catch (UnregisteredRecipientException e) {
|
||||
if (recipient instanceof RecipientIdentifier.Number r) {
|
||||
return account.getRecipientStore().resolveRecipientByNumberOptional(r.number());
|
||||
if (recipient instanceof RecipientIdentifier.Number(String number)) {
|
||||
return account.getRecipientStore().resolveRecipientByNumberOptional(number);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
@ -180,7 +190,8 @@ public class RecipientHelper {
|
|||
}
|
||||
|
||||
private Map<String, RegisteredUser> getRegisteredUsers(
|
||||
final Set<String> numbers, final boolean isPartialRefresh
|
||||
final Set<String> numbers,
|
||||
final boolean isPartialRefresh
|
||||
) throws IOException {
|
||||
Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh);
|
||||
|
||||
|
@ -211,7 +222,8 @@ public class RecipientHelper {
|
|||
}
|
||||
|
||||
private Map<String, RegisteredUser> getRegisteredUsersV2(
|
||||
final Set<String> numbers, boolean isPartialRefresh
|
||||
final Set<String> numbers,
|
||||
boolean isPartialRefresh
|
||||
) throws IOException {
|
||||
final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
|
||||
final var newNumbers = new HashSet<>(numbers) {{
|
||||
|
@ -231,12 +243,11 @@ public class RecipientHelper {
|
|||
|
||||
final CdsiV2Service.Response response;
|
||||
try {
|
||||
response = dependencies.getAccountManager()
|
||||
.getRegisteredUsersWithCdsi(token.isEmpty() ? Set.of() : previousNumbers,
|
||||
response = handleResponseException(dependencies.getCdsApi()
|
||||
.getRegisteredUsers(token.isEmpty() ? Set.of() : previousNumbers,
|
||||
newNumbers,
|
||||
account.getRecipientStore().getServiceIdToProfileKeyMap(),
|
||||
token,
|
||||
dependencies.getServiceEnvironmentConfig().cdsiMrenclave(),
|
||||
null,
|
||||
dependencies.getLibSignalNetwork(),
|
||||
newToken -> {
|
||||
|
@ -254,7 +265,7 @@ public class RecipientHelper {
|
|||
account.setCdsiToken(newToken);
|
||||
account.setLastRecipientsRefresh(System.currentTimeMillis());
|
||||
}
|
||||
});
|
||||
}));
|
||||
} catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) {
|
||||
account.setCdsiToken(null);
|
||||
account.getCdsiStore().clearAll();
|
||||
|
|
|
@ -125,7 +125,8 @@ public class SendHelper {
|
|||
}
|
||||
|
||||
public SendMessageResult sendReceiptMessage(
|
||||
final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId
|
||||
final SignalServiceReceiptMessage receiptMessage,
|
||||
final RecipientId recipientId
|
||||
) {
|
||||
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||
final var result = handleSendMessage(recipientId,
|
||||
|
@ -157,7 +158,9 @@ public class SendHelper {
|
|||
}
|
||||
|
||||
public SendMessageResult sendRetryReceipt(
|
||||
DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional<GroupId> groupId
|
||||
DecryptionErrorMessage errorMessage,
|
||||
RecipientId recipientId,
|
||||
Optional<GroupId> groupId
|
||||
) {
|
||||
logger.debug("Sending retry receipt for {} to {}, device: {}",
|
||||
errorMessage.getTimestamp(),
|
||||
|
@ -183,7 +186,8 @@ public class SendHelper {
|
|||
}
|
||||
|
||||
public SendMessageResult sendSelfMessage(
|
||||
SignalServiceDataMessage.Builder messageBuilder, Optional<Long> editTargetTimestamp
|
||||
SignalServiceDataMessage.Builder messageBuilder,
|
||||
Optional<Long> editTargetTimestamp
|
||||
) {
|
||||
final var recipientId = account.getSelfRecipientId();
|
||||
final var contact = account.getContactStore().getContact(recipientId);
|
||||
|
@ -214,9 +218,7 @@ public class SendHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public SendMessageResult sendTypingMessage(
|
||||
SignalServiceTypingMessage message, RecipientId recipientId
|
||||
) {
|
||||
public SendMessageResult sendTypingMessage(SignalServiceTypingMessage message, RecipientId recipientId) {
|
||||
final var result = handleSendMessage(recipientId,
|
||||
(messageSender, address, unidentifiedAccess, includePniSignature) -> messageSender.sendTyping(List.of(
|
||||
address), List.of(unidentifiedAccess), message, null).getFirst());
|
||||
|
@ -225,7 +227,8 @@ public class SendHelper {
|
|||
}
|
||||
|
||||
public List<SendMessageResult> sendGroupTypingMessage(
|
||||
SignalServiceTypingMessage message, GroupId groupId
|
||||
SignalServiceTypingMessage message,
|
||||
GroupId groupId
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
final var g = getGroupForSending(groupId);
|
||||
if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) {
|
||||
|
@ -238,7 +241,9 @@ public class SendHelper {
|
|||
}
|
||||
|
||||
public SendMessageResult resendMessage(
|
||||
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry
|
||||
final RecipientId recipientId,
|
||||
final long timestamp,
|
||||
final MessageSendLogEntry messageSendLogEntry
|
||||
) {
|
||||
logger.trace("Resending message {} to {}", timestamp, recipientId);
|
||||
if (messageSendLogEntry.groupId().isEmpty()) {
|
||||
|
@ -552,7 +557,9 @@ public class SendHelper {
|
|||
}
|
||||
|
||||
private List<SendMessageResult> sendGroupMessageInternalWithLegacy(
|
||||
final LegacySenderHandler sender, final Set<RecipientId> recipientIds, final boolean isRecipientUpdate
|
||||
final LegacySenderHandler sender,
|
||||
final Set<RecipientId> recipientIds,
|
||||
final boolean isRecipientUpdate
|
||||
) throws IOException {
|
||||
final var recipientIdList = new ArrayList<>(recipientIds);
|
||||
final var addresses = recipientIdList.stream()
|
||||
|
@ -644,7 +651,9 @@ public class SendHelper {
|
|||
}
|
||||
|
||||
private SendMessageResult sendMessage(
|
||||
SignalServiceDataMessage message, RecipientId recipientId, Optional<Long> editTargetTimestamp
|
||||
SignalServiceDataMessage message,
|
||||
RecipientId recipientId,
|
||||
Optional<Long> editTargetTimestamp
|
||||
) {
|
||||
final var messageSendLogStore = account.getMessageSendLogStore();
|
||||
final var urgent = true;
|
||||
|
|
|
@ -30,7 +30,9 @@ public class StickerHelper {
|
|||
}
|
||||
|
||||
public StickerPack addOrUpdateStickerPack(
|
||||
final StickerPackId stickerPackId, final byte[] stickerPackKey, final boolean installed
|
||||
final StickerPackId stickerPackId,
|
||||
final byte[] stickerPackKey,
|
||||
final boolean installed
|
||||
) {
|
||||
final var sticker = account.getStickerStore().getStickerPack(stickerPackId);
|
||||
if (sticker != null) {
|
||||
|
@ -50,7 +52,8 @@ public class StickerHelper {
|
|||
}
|
||||
|
||||
public JsonStickerPack getOrRetrieveStickerPack(
|
||||
StickerPackId packId, byte[] packKey
|
||||
StickerPackId packId,
|
||||
byte[] packKey
|
||||
) throws InvalidStickerException {
|
||||
try {
|
||||
retrieveStickerPack(packId, packKey);
|
||||
|
|
|
@ -2,6 +2,7 @@ package org.asamk.signal.manager.helper;
|
|||
|
||||
import org.asamk.signal.manager.api.GroupIdV1;
|
||||
import org.asamk.signal.manager.api.GroupIdV2;
|
||||
import org.asamk.signal.manager.api.Profile;
|
||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
|
@ -17,11 +18,17 @@ import org.signal.core.util.SetUtil;
|
|||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.storage.RecordIkm;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageRecordConvertersKt;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.ManifestIfDifferentVersionResult;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository.WriteStorageRecordsResult;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
|
@ -32,9 +39,10 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
|
||||
public class StorageHelper {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class);
|
||||
|
@ -54,7 +62,7 @@ public class StorageHelper {
|
|||
}
|
||||
|
||||
public void syncDataWithStorage() throws IOException {
|
||||
final var storageKey = account.getOrCreateStorageKey();
|
||||
var storageKey = account.getOrCreateStorageKey();
|
||||
if (storageKey == null) {
|
||||
if (!account.isPrimaryDevice()) {
|
||||
logger.debug("Storage key unknown, requesting from primary device.");
|
||||
|
@ -65,52 +73,76 @@ public class StorageHelper {
|
|||
|
||||
logger.trace("Reading manifest from remote storage");
|
||||
final var localManifestVersion = account.getStorageManifestVersion();
|
||||
final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.EMPTY);
|
||||
SignalStorageManifest remoteManifest;
|
||||
try {
|
||||
remoteManifest = dependencies.getAccountManager()
|
||||
.getStorageManifestIfDifferentVersion(storageKey, localManifestVersion)
|
||||
.orElse(localManifest);
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.warn("Manifest couldn't be decrypted.");
|
||||
if (account.isPrimaryDevice()) {
|
||||
try {
|
||||
forcePushToStorage(storageKey);
|
||||
} catch (RetryLaterException rle) {
|
||||
// TODO retry later
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.getVersion());
|
||||
final var localManifest = account.getStorageManifest().orElse(SignalStorageManifest.Companion.getEMPTY());
|
||||
final var storageServiceRepository = dependencies.getStorageServiceRepository();
|
||||
final var result = storageServiceRepository.getStorageManifestIfDifferentVersion(storageKey,
|
||||
localManifestVersion);
|
||||
|
||||
var needsForcePush = false;
|
||||
if (remoteManifest.getVersion() > localManifestVersion) {
|
||||
logger.trace("Remote version was newer, reading records.");
|
||||
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest);
|
||||
} else if (remoteManifest.getVersion() < localManifest.getVersion()) {
|
||||
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
|
||||
}
|
||||
logger.trace("Done reading data from remote storage");
|
||||
final var remoteManifest = switch (result) {
|
||||
case ManifestIfDifferentVersionResult.DifferentVersion diff -> {
|
||||
final var manifest = diff.getManifest();
|
||||
storeManifestLocally(manifest);
|
||||
yield manifest;
|
||||
}
|
||||
case ManifestIfDifferentVersionResult.DecryptionError ignore -> {
|
||||
logger.warn("Manifest couldn't be decrypted.");
|
||||
if (account.isPrimaryDevice()) {
|
||||
needsForcePush = true;
|
||||
} else {
|
||||
context.getSyncHelper().requestSyncKeys();
|
||||
}
|
||||
yield null;
|
||||
}
|
||||
case ManifestIfDifferentVersionResult.SameVersion ignored -> localManifest;
|
||||
case ManifestIfDifferentVersionResult.NetworkError e -> throw e.getException();
|
||||
case ManifestIfDifferentVersionResult.StatusCodeError e -> throw e.getException();
|
||||
default -> throw new RuntimeException("Unhandled ManifestIfDifferentVersionResult type");
|
||||
};
|
||||
|
||||
if (localManifest != remoteManifest) {
|
||||
storeManifestLocally(remoteManifest);
|
||||
}
|
||||
if (remoteManifest != null) {
|
||||
logger.trace("Manifest versions: local {}, remote {}", localManifestVersion, remoteManifest.version);
|
||||
|
||||
readRecordsWithPreviouslyUnknownTypes(storageKey);
|
||||
if (remoteManifest.version > localManifestVersion) {
|
||||
logger.trace("Remote version was newer, reading records.");
|
||||
needsForcePush = readDataFromStorage(storageKey, localManifest, remoteManifest);
|
||||
} else if (remoteManifest.version < localManifest.version) {
|
||||
logger.debug("Remote storage manifest version was older. User might have switched accounts.");
|
||||
}
|
||||
logger.trace("Done reading data from remote storage");
|
||||
|
||||
readRecordsWithPreviouslyUnknownTypes(storageKey, remoteManifest);
|
||||
}
|
||||
|
||||
logger.trace("Adding missing storageIds to local data");
|
||||
account.getRecipientStore().setMissingStorageIds();
|
||||
account.getGroupStore().setMissingStorageIds();
|
||||
|
||||
var needsMultiDeviceSync = false;
|
||||
try {
|
||||
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
||||
} catch (RetryLaterException e) {
|
||||
// TODO retry later
|
||||
return;
|
||||
|
||||
if (account.needsStorageKeyMigration()) {
|
||||
logger.debug("Storage needs force push due to new account entropy pool");
|
||||
// Set new aep and reset previous master key and storage key
|
||||
account.setAccountEntropyPool(account.getOrCreateAccountEntropyPool());
|
||||
storageKey = account.getOrCreateStorageKey();
|
||||
context.getSyncHelper().sendKeysMessage();
|
||||
needsForcePush = true;
|
||||
} else if (remoteManifest == null) {
|
||||
if (account.isPrimaryDevice()) {
|
||||
needsForcePush = true;
|
||||
}
|
||||
} else if (remoteManifest.recordIkm == null && account.getSelfRecipientProfile()
|
||||
.getCapabilities()
|
||||
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
|
||||
logger.debug("The SSRE2 capability is supported, but no recordIkm is set! Force pushing.");
|
||||
needsForcePush = true;
|
||||
} else {
|
||||
try {
|
||||
needsMultiDeviceSync = writeToStorage(storageKey, remoteManifest, needsForcePush);
|
||||
} catch (RetryLaterException e) {
|
||||
// TODO retry later
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsForcePush) {
|
||||
|
@ -131,6 +163,23 @@ public class StorageHelper {
|
|||
logger.debug("Done syncing data with remote storage");
|
||||
}
|
||||
|
||||
public void forcePushToStorage() throws IOException {
|
||||
if (!account.isPrimaryDevice()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final var storageKey = account.getOrCreateStorageKey();
|
||||
if (storageKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
forcePushToStorage(storageKey);
|
||||
} catch (RetryLaterException e) {
|
||||
// TODO retry later
|
||||
}
|
||||
}
|
||||
|
||||
private boolean readDataFromStorage(
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest localManifest,
|
||||
|
@ -140,36 +189,37 @@ public class StorageHelper {
|
|||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
|
||||
var idDifference = findIdDifference(remoteManifest.getStorageIds(), localManifest.getStorageIds());
|
||||
var idDifference = findIdDifference(remoteManifest.storageIds, localManifest.storageIds);
|
||||
|
||||
if (idDifference.hasTypeMismatches() && account.isPrimaryDevice()) {
|
||||
logger.debug("Found type mismatches in the ID sets! Scheduling a force push after this sync completes.");
|
||||
needsForcePush = true;
|
||||
}
|
||||
|
||||
logger.debug("Pre-Merge ID Difference :: " + idDifference);
|
||||
|
||||
if (!idDifference.localOnlyIds().isEmpty()) {
|
||||
final var updated = account.getRecipientStore()
|
||||
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection, idDifference.localOnlyIds());
|
||||
|
||||
if (updated > 0) {
|
||||
logger.warn(
|
||||
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
|
||||
updated);
|
||||
}
|
||||
}
|
||||
logger.debug("Pre-Merge ID Difference :: {}", idDifference);
|
||||
|
||||
if (!idDifference.isEmpty()) {
|
||||
final var remoteOnlyRecords = getSignalStorageRecords(storageKey, idDifference.remoteOnlyIds());
|
||||
final var remoteOnlyRecords = getSignalStorageRecords(storageKey,
|
||||
remoteManifest,
|
||||
idDifference.remoteOnlyIds());
|
||||
|
||||
if (remoteOnlyRecords.size() != idDifference.remoteOnlyIds().size()) {
|
||||
logger.debug("Could not find all remote-only records! Requested: "
|
||||
+ idDifference.remoteOnlyIds()
|
||||
.size()
|
||||
+ ", Found: "
|
||||
+ remoteOnlyRecords.size()
|
||||
+ ". These stragglers should naturally get deleted during the sync.");
|
||||
logger.debug(
|
||||
"Could not find all remote-only records! Requested: {}, Found: {}. These stragglers should naturally get deleted during the sync.",
|
||||
idDifference.remoteOnlyIds().size(),
|
||||
remoteOnlyRecords.size());
|
||||
}
|
||||
|
||||
if (!idDifference.localOnlyIds().isEmpty()) {
|
||||
final var updated = account.getRecipientStore()
|
||||
.removeStorageIdsFromLocalOnlyUnregisteredRecipients(connection,
|
||||
idDifference.localOnlyIds());
|
||||
|
||||
if (updated > 0) {
|
||||
logger.warn(
|
||||
"Found {} records that were deleted remotely but only marked unregistered locally. Removed those from local store.",
|
||||
updated);
|
||||
}
|
||||
}
|
||||
|
||||
final var unknownInserts = processKnownRecords(connection, remoteOnlyRecords);
|
||||
|
@ -194,18 +244,21 @@ public class StorageHelper {
|
|||
return needsForcePush;
|
||||
}
|
||||
|
||||
private void readRecordsWithPreviouslyUnknownTypes(final StorageKey storageKey) throws IOException {
|
||||
private void readRecordsWithPreviouslyUnknownTypes(
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest remoteManifest
|
||||
) throws IOException {
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
final var knownUnknownIds = account.getUnknownStorageIdStore()
|
||||
.getUnknownStorageIds(connection, KNOWN_TYPES);
|
||||
|
||||
if (!knownUnknownIds.isEmpty()) {
|
||||
logger.debug("We have " + knownUnknownIds.size() + " unknown records that we can now process.");
|
||||
logger.debug("We have {} unknown records that we can now process.", knownUnknownIds.size());
|
||||
|
||||
final var remote = getSignalStorageRecords(storageKey, knownUnknownIds);
|
||||
final var remote = getSignalStorageRecords(storageKey, remoteManifest, knownUnknownIds);
|
||||
|
||||
logger.debug("Found " + remote.size() + " of the known-unknowns remotely.");
|
||||
logger.debug("Found {} of the known-unknowns remotely.", remote.size());
|
||||
|
||||
processKnownRecords(connection, remote);
|
||||
account.getUnknownStorageIdStore()
|
||||
|
@ -218,22 +271,37 @@ public class StorageHelper {
|
|||
}
|
||||
|
||||
private boolean writeToStorage(
|
||||
final StorageKey storageKey, final SignalStorageManifest remoteManifest, final boolean needsForcePush
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest remoteManifest,
|
||||
final boolean needsForcePush
|
||||
) throws IOException, RetryLaterException {
|
||||
final WriteOperationResult remoteWriteOperation;
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
|
||||
final var localStorageIds = getAllLocalStorageIds(connection);
|
||||
final var idDifference = findIdDifference(remoteManifest.getStorageIds(), localStorageIds);
|
||||
logger.debug("ID Difference :: " + idDifference);
|
||||
var localStorageIds = getAllLocalStorageIds(connection);
|
||||
var idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
|
||||
logger.debug("ID Difference :: {}", idDifference);
|
||||
|
||||
final var unknownOnlyLocal = idDifference.localOnlyIds()
|
||||
.stream()
|
||||
.filter(id -> !KNOWN_TYPES.contains(id.getType()))
|
||||
.toList();
|
||||
|
||||
if (!unknownOnlyLocal.isEmpty()) {
|
||||
logger.debug("Storage ids with unknown type: {} to delete", unknownOnlyLocal.size());
|
||||
account.getUnknownStorageIdStore().deleteUnknownStorageIds(connection, unknownOnlyLocal);
|
||||
localStorageIds = getAllLocalStorageIds(connection);
|
||||
idDifference = findIdDifference(remoteManifest.storageIds, localStorageIds);
|
||||
}
|
||||
|
||||
final var remoteDeletes = idDifference.remoteOnlyIds().stream().map(StorageId::getRaw).toList();
|
||||
final var remoteInserts = buildLocalStorageRecords(connection, idDifference.localOnlyIds());
|
||||
// TODO check if local storage record proto matches remote, then reset to remote storage_id
|
||||
|
||||
remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1,
|
||||
remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.version + 1,
|
||||
account.getDeviceId(),
|
||||
remoteManifest.recordIkm,
|
||||
localStorageIds), remoteInserts, remoteDeletes);
|
||||
|
||||
connection.commit();
|
||||
|
@ -242,39 +310,37 @@ public class StorageHelper {
|
|||
}
|
||||
|
||||
if (remoteWriteOperation.isEmpty()) {
|
||||
logger.debug("No remote writes needed. Still at version: " + remoteManifest.getVersion());
|
||||
logger.debug("No remote writes needed. Still at version: {}", remoteManifest.version);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug("We have something to write remotely.");
|
||||
logger.debug("WriteOperationResult :: " + remoteWriteOperation);
|
||||
logger.debug("WriteOperationResult :: {}", remoteWriteOperation);
|
||||
|
||||
StorageSyncValidations.validate(remoteWriteOperation,
|
||||
remoteManifest,
|
||||
needsForcePush,
|
||||
account.getSelfRecipientAddress());
|
||||
|
||||
final Optional<SignalStorageManifest> conflict;
|
||||
try {
|
||||
conflict = dependencies.getAccountManager()
|
||||
.writeStorageRecords(storageKey,
|
||||
remoteWriteOperation.manifest(),
|
||||
remoteWriteOperation.inserts(),
|
||||
remoteWriteOperation.deletes());
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.warn("Failed to decrypt conflicting storage manifest: {}", e.getMessage());
|
||||
throw new IOException(e);
|
||||
final var result = dependencies.getStorageServiceRepository()
|
||||
.writeStorageRecords(storageKey,
|
||||
remoteWriteOperation.manifest(),
|
||||
remoteWriteOperation.inserts(),
|
||||
remoteWriteOperation.deletes());
|
||||
switch (result) {
|
||||
case WriteStorageRecordsResult.ConflictError ignored -> {
|
||||
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
||||
case WriteStorageRecordsResult.Success ignored -> {
|
||||
logger.debug("Saved new manifest. Now at version: {}", remoteWriteOperation.manifest().version);
|
||||
storeManifestLocally(remoteWriteOperation.manifest());
|
||||
return true;
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||
}
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
logger.debug("Hit a conflict when trying to resolve the conflict! Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
|
||||
logger.debug("Saved new manifest. Now at version: " + remoteWriteOperation.manifest().getVersion());
|
||||
storeManifestLocally(remoteWriteOperation.manifest());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void forcePushToStorage(
|
||||
|
@ -282,7 +348,8 @@ public class StorageHelper {
|
|||
) throws IOException, RetryLaterException {
|
||||
logger.debug("Force pushing local state to remote storage");
|
||||
|
||||
final var currentVersion = dependencies.getAccountManager().getStorageManifestVersion();
|
||||
final var currentVersion = handleResponseException(dependencies.getStorageServiceRepository()
|
||||
.getManifestVersion());
|
||||
final var newVersion = currentVersion + 1;
|
||||
final var newStorageRecords = new ArrayList<SignalStorageRecord>();
|
||||
final Map<RecipientId, StorageId> newContactStorageIds;
|
||||
|
@ -298,17 +365,19 @@ public class StorageHelper {
|
|||
final var storageId = newContactStorageIds.get(recipientId);
|
||||
if (storageId.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue()) {
|
||||
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
|
||||
final var accountRecord = StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
|
||||
final var accountRecord = StorageSyncModels.localToRemoteRecord(connection,
|
||||
account.getConfigurationStore(),
|
||||
recipient,
|
||||
account.getUsernameLink(),
|
||||
storageId.getRaw());
|
||||
newStorageRecords.add(accountRecord);
|
||||
account.getUsernameLink());
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().account(accountRecord).build()));
|
||||
} else {
|
||||
final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
|
||||
final var address = recipient.getAddress().getIdentifier();
|
||||
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw());
|
||||
newStorageRecords.add(record);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity);
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().contact(record).build()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,8 +386,9 @@ public class StorageHelper {
|
|||
for (final var groupId : groupV1Ids) {
|
||||
final var storageId = newGroupV1StorageIds.get(groupId);
|
||||
final var group = account.getGroupStore().getGroup(connection, groupId);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw());
|
||||
newStorageRecords.add(record);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(group);
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().groupV1(record).build()));
|
||||
}
|
||||
|
||||
final var groupV2Ids = account.getGroupStore().getGroupV2Ids(connection);
|
||||
|
@ -326,8 +396,9 @@ public class StorageHelper {
|
|||
for (final var groupId : groupV2Ids) {
|
||||
final var storageId = newGroupV2StorageIds.get(groupId);
|
||||
final var group = account.getGroupStore().getGroup(connection, groupId);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(group, storageId.getRaw());
|
||||
newStorageRecords.add(record);
|
||||
final var record = StorageSyncModels.localToRemoteRecord(group);
|
||||
newStorageRecords.add(new SignalStorageRecord(storageId,
|
||||
new StorageRecord.Builder().groupV2(record).build()));
|
||||
}
|
||||
|
||||
connection.commit();
|
||||
|
@ -336,34 +407,46 @@ public class StorageHelper {
|
|||
}
|
||||
final var newStorageIds = newStorageRecords.stream().map(SignalStorageRecord::getId).toList();
|
||||
|
||||
final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), newStorageIds);
|
||||
final RecordIkm recordIkm;
|
||||
if (account.getSelfRecipientProfile()
|
||||
.getCapabilities()
|
||||
.contains(Profile.Capability.storageServiceEncryptionV2Capability)) {
|
||||
logger.debug("Generating and including a new recordIkm.");
|
||||
recordIkm = RecordIkm.Companion.generate();
|
||||
} else {
|
||||
logger.debug("SSRE2 not yet supported. Not including recordIkm.");
|
||||
recordIkm = null;
|
||||
}
|
||||
|
||||
final var manifest = new SignalStorageManifest(newVersion, account.getDeviceId(), recordIkm, newStorageIds);
|
||||
|
||||
StorageSyncValidations.validateForcePush(manifest, newStorageRecords, account.getSelfRecipientAddress());
|
||||
|
||||
final Optional<SignalStorageManifest> conflict;
|
||||
try {
|
||||
if (newVersion > 1) {
|
||||
logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size());
|
||||
conflict = dependencies.getAccountManager()
|
||||
.resetStorageRecords(storageServiceKey, manifest, newStorageRecords);
|
||||
} else {
|
||||
logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size());
|
||||
conflict = dependencies.getAccountManager()
|
||||
.writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList());
|
||||
final WriteStorageRecordsResult result;
|
||||
if (newVersion > 1) {
|
||||
logger.trace("Force-pushing data. Inserting {} IDs.", newStorageRecords.size());
|
||||
result = dependencies.getStorageServiceRepository()
|
||||
.resetAndWriteStorageRecords(storageServiceKey, manifest, newStorageRecords);
|
||||
} else {
|
||||
logger.trace("First version, normal push. Inserting {} IDs.", newStorageRecords.size());
|
||||
result = dependencies.getStorageServiceRepository()
|
||||
.writeStorageRecords(storageServiceKey, manifest, newStorageRecords, Collections.emptyList());
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case WriteStorageRecordsResult.ConflictError ignored -> {
|
||||
logger.debug("Hit a conflict. Trying again.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.debug("Hit an invalid key exception, which likely indicates a conflict.", e);
|
||||
throw new RetryLaterException();
|
||||
case WriteStorageRecordsResult.NetworkError networkError -> throw networkError.getException();
|
||||
case WriteStorageRecordsResult.StatusCodeError statusCodeError -> throw statusCodeError.getException();
|
||||
case WriteStorageRecordsResult.Success ignored -> {
|
||||
logger.debug("Force push succeeded. Updating local manifest version to: {}", manifest.version);
|
||||
storeManifestLocally(manifest);
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||
}
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
logger.debug("Hit a conflict. Trying again.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
|
||||
logger.debug("Force push succeeded. Updating local manifest version to: " + manifest.getVersion());
|
||||
storeManifestLocally(manifest);
|
||||
|
||||
try (final var connection = account.getAccountDatabase().getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
account.getRecipientStore().updateStorageIds(connection, newContactStorageIds);
|
||||
|
@ -403,21 +486,35 @@ public class StorageHelper {
|
|||
private void storeManifestLocally(
|
||||
final SignalStorageManifest remoteManifest
|
||||
) {
|
||||
account.setStorageManifestVersion(remoteManifest.getVersion());
|
||||
account.setStorageManifestVersion(remoteManifest.version);
|
||||
account.setStorageManifest(remoteManifest);
|
||||
}
|
||||
|
||||
private List<SignalStorageRecord> getSignalStorageRecords(
|
||||
final StorageKey storageKey, final List<StorageId> storageIds
|
||||
final StorageKey storageKey,
|
||||
final SignalStorageManifest manifest,
|
||||
final List<StorageId> storageIds
|
||||
) throws IOException {
|
||||
List<SignalStorageRecord> records;
|
||||
try {
|
||||
records = dependencies.getAccountManager().readStorageRecords(storageKey, storageIds);
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.warn("Failed to read storage records, ignoring.");
|
||||
return List.of();
|
||||
}
|
||||
return records;
|
||||
final var result = dependencies.getStorageServiceRepository()
|
||||
.readStorageRecords(storageKey, manifest.recordIkm, storageIds);
|
||||
return switch (result) {
|
||||
case StorageServiceRepository.StorageRecordResult.DecryptionError decryptionError -> {
|
||||
if (decryptionError.getException() instanceof InvalidKeyException) {
|
||||
logger.warn("Failed to read storage records, ignoring.");
|
||||
yield List.of();
|
||||
} else if (decryptionError.getException() instanceof IOException ioe) {
|
||||
throw ioe;
|
||||
} else {
|
||||
throw new IOException(decryptionError.getException());
|
||||
}
|
||||
}
|
||||
case StorageServiceRepository.StorageRecordResult.NetworkError networkError ->
|
||||
throw networkError.getException();
|
||||
case StorageServiceRepository.StorageRecordResult.StatusCodeError statusCodeError ->
|
||||
throw statusCodeError.getException();
|
||||
case StorageServiceRepository.StorageRecordResult.Success success -> success.getRecords();
|
||||
default -> throw new IllegalStateException("Unexpected value: " + result);
|
||||
};
|
||||
}
|
||||
|
||||
private List<StorageId> getAllLocalStorageIds(final Connection connection) throws SQLException {
|
||||
|
@ -430,45 +527,52 @@ public class StorageHelper {
|
|||
}
|
||||
|
||||
private List<SignalStorageRecord> buildLocalStorageRecords(
|
||||
final Connection connection, final List<StorageId> storageIds
|
||||
final Connection connection,
|
||||
final List<StorageId> storageIds
|
||||
) throws SQLException {
|
||||
final var records = new ArrayList<SignalStorageRecord>();
|
||||
final var records = new ArrayList<SignalStorageRecord>(storageIds.size());
|
||||
for (final var storageId : storageIds) {
|
||||
final var record = buildLocalStorageRecord(connection, storageId);
|
||||
if (record != null) {
|
||||
records.add(record);
|
||||
}
|
||||
records.add(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
private SignalStorageRecord buildLocalStorageRecord(
|
||||
Connection connection, StorageId storageId
|
||||
Connection connection,
|
||||
StorageId storageId
|
||||
) throws SQLException {
|
||||
return switch (ManifestRecord.Identifier.Type.fromValue(storageId.getType())) {
|
||||
case ManifestRecord.Identifier.Type.CONTACT -> {
|
||||
final var recipient = account.getRecipientStore().getRecipient(connection, storageId);
|
||||
final var address = recipient.getAddress().getIdentifier();
|
||||
final var identity = account.getIdentityKeyStore().getIdentityInfo(connection, address);
|
||||
yield StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw());
|
||||
final var record = StorageSyncModels.localToRemoteRecord(recipient, identity);
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().contact(record).build());
|
||||
}
|
||||
case ManifestRecord.Identifier.Type.GROUPV1 -> {
|
||||
final var groupV1 = account.getGroupStore().getGroupV1(connection, storageId);
|
||||
yield StorageSyncModels.localToRemoteRecord(groupV1, storageId.getRaw());
|
||||
final var record = StorageSyncModels.localToRemoteRecord(groupV1);
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV1(record).build());
|
||||
}
|
||||
case ManifestRecord.Identifier.Type.GROUPV2 -> {
|
||||
final var groupV2 = account.getGroupStore().getGroupV2(connection, storageId);
|
||||
yield StorageSyncModels.localToRemoteRecord(groupV2, storageId.getRaw());
|
||||
final var record = StorageSyncModels.localToRemoteRecord(groupV2);
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().groupV2(record).build());
|
||||
}
|
||||
case ManifestRecord.Identifier.Type.ACCOUNT -> {
|
||||
final var selfRecipient = account.getRecipientStore()
|
||||
.getRecipient(connection, account.getSelfRecipientId());
|
||||
yield StorageSyncModels.localToRemoteRecord(account.getConfigurationStore(),
|
||||
|
||||
final var record = StorageSyncModels.localToRemoteRecord(connection,
|
||||
account.getConfigurationStore(),
|
||||
selfRecipient,
|
||||
account.getUsernameLink(),
|
||||
storageId.getRaw());
|
||||
account.getUsernameLink());
|
||||
yield new SignalStorageRecord(storageId, new StorageRecord.Builder().account(record).build());
|
||||
}
|
||||
case null, default -> {
|
||||
throw new AssertionError("Got unknown local storage record type: " + storageId);
|
||||
}
|
||||
case null, default -> throw new AssertionError("Got unknown local storage record type: " + storageId);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -484,7 +588,8 @@ public class StorageHelper {
|
|||
* exclusive to the local data set.
|
||||
*/
|
||||
private static IdDifferenceResult findIdDifference(
|
||||
Collection<StorageId> remoteIds, Collection<StorageId> localIds
|
||||
Collection<StorageId> remoteIds,
|
||||
Collection<StorageId> localIds
|
||||
) {
|
||||
final var base64Encoder = Base64.getEncoder();
|
||||
final var remoteByRawId = remoteIds.stream()
|
||||
|
@ -502,7 +607,7 @@ public class StorageHelper {
|
|||
final var remote = remoteByRawId.get(rawId);
|
||||
final var local = localByRawId.get(rawId);
|
||||
|
||||
if (remote.getType() != local.getType() && local.getType() != 0) {
|
||||
if (remote.getType() != local.getType() && KNOWN_TYPES.contains(local.getType())) {
|
||||
remoteOnlyRawIds.remove(rawId);
|
||||
localOnlyRawIds.remove(rawId);
|
||||
hasTypeMismatch = true;
|
||||
|
@ -520,7 +625,8 @@ public class StorageHelper {
|
|||
}
|
||||
|
||||
private List<StorageId> processKnownRecords(
|
||||
final Connection connection, List<SignalStorageRecord> records
|
||||
final Connection connection,
|
||||
List<SignalStorageRecord> records
|
||||
) throws SQLException {
|
||||
final var unknownRecords = new ArrayList<StorageId>();
|
||||
|
||||
|
@ -530,13 +636,24 @@ public class StorageHelper {
|
|||
final var groupV2RecordProcessor = new GroupV2RecordProcessor(account, connection);
|
||||
|
||||
for (final var record : records) {
|
||||
logger.debug("Reading record of type {}", record.getType());
|
||||
switch (ManifestRecord.Identifier.Type.fromValue(record.getType())) {
|
||||
case ACCOUNT -> accountRecordProcessor.process(record.getAccount().get());
|
||||
case GROUPV1 -> groupV1RecordProcessor.process(record.getGroupV1().get());
|
||||
case GROUPV2 -> groupV2RecordProcessor.process(record.getGroupV2().get());
|
||||
case CONTACT -> contactRecordProcessor.process(record.getContact().get());
|
||||
case null, default -> unknownRecords.add(record.getId());
|
||||
if (record.getProto().account != null) {
|
||||
logger.debug("Reading record {} of type account", record.getId());
|
||||
accountRecordProcessor.process(StorageRecordConvertersKt.toSignalAccountRecord(record.getProto().account,
|
||||
record.getId()));
|
||||
} else if (record.getProto().groupV1 != null) {
|
||||
logger.debug("Reading record {} of type groupV1", record.getId());
|
||||
groupV1RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV1Record(record.getProto().groupV1,
|
||||
record.getId()));
|
||||
} else if (record.getProto().groupV2 != null) {
|
||||
logger.debug("Reading record {} of type groupV2", record.getId());
|
||||
groupV2RecordProcessor.process(StorageRecordConvertersKt.toSignalGroupV2Record(record.getProto().groupV2,
|
||||
record.getId()));
|
||||
} else if (record.getProto().contact != null) {
|
||||
logger.debug("Reading record {} of type contact", record.getId());
|
||||
contactRecordProcessor.process(StorageRecordConvertersKt.toSignalContactRecord(record.getProto().contact,
|
||||
record.getId()));
|
||||
} else {
|
||||
unknownRecords.add(record.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,17 +70,12 @@ public class SyncHelper {
|
|||
requestSyncData(SyncMessage.Request.Type.BLOCKED);
|
||||
requestSyncData(SyncMessage.Request.Type.CONFIGURATION);
|
||||
requestSyncKeys();
|
||||
requestSyncPniIdentity();
|
||||
}
|
||||
|
||||
public void requestSyncKeys() {
|
||||
requestSyncData(SyncMessage.Request.Type.KEYS);
|
||||
}
|
||||
|
||||
public void requestSyncPniIdentity() {
|
||||
requestSyncData(SyncMessage.Request.Type.PNI_IDENTITY);
|
||||
}
|
||||
|
||||
public SendMessageResult sendSyncFetchProfileMessage() {
|
||||
return context.getSendHelper()
|
||||
.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE));
|
||||
|
@ -165,7 +160,7 @@ public class SyncHelper {
|
|||
final var contact = contactPair.second();
|
||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
|
||||
|
||||
final var deviceContact = getDeviceContact(address, recipientId, contact);
|
||||
final var deviceContact = getDeviceContact(address, contact);
|
||||
out.write(deviceContact);
|
||||
deviceContact.getAvatar().ifPresent(a -> {
|
||||
try {
|
||||
|
@ -180,7 +175,7 @@ public class SyncHelper {
|
|||
final var address = account.getSelfRecipientAddress();
|
||||
final var recipientId = account.getSelfRecipientId();
|
||||
final var contact = account.getContactStore().getContact(recipientId);
|
||||
final var deviceContact = getDeviceContact(address, recipientId, contact);
|
||||
final var deviceContact = getDeviceContact(address, contact);
|
||||
out.write(deviceContact);
|
||||
deviceContact.getAvatar().ifPresent(a -> {
|
||||
try {
|
||||
|
@ -216,39 +211,25 @@ public class SyncHelper {
|
|||
}
|
||||
|
||||
@NotNull
|
||||
private DeviceContact getDeviceContact(
|
||||
final RecipientAddress address, final RecipientId recipientId, final Contact contact
|
||||
) throws IOException {
|
||||
var currentIdentity = address.serviceId().isEmpty()
|
||||
? null
|
||||
: account.getIdentityKeyStore().getIdentityInfo(address.serviceId().get());
|
||||
VerifiedMessage verifiedMessage = null;
|
||||
if (currentIdentity != null) {
|
||||
verifiedMessage = new VerifiedMessage(address.toSignalServiceAddress(),
|
||||
currentIdentity.getIdentityKey(),
|
||||
currentIdentity.getTrustLevel().toVerifiedState(),
|
||||
currentIdentity.getDateAddedTimestamp());
|
||||
}
|
||||
|
||||
var profileKey = account.getProfileStore().getProfileKey(recipientId);
|
||||
private DeviceContact getDeviceContact(final RecipientAddress address, final Contact contact) throws IOException {
|
||||
return new DeviceContact(address.aci(),
|
||||
address.number(),
|
||||
Optional.ofNullable(contact == null ? null : contact.getName()),
|
||||
createContactAvatarAttachment(address),
|
||||
Optional.ofNullable(contact == null ? null : contact.color()),
|
||||
Optional.ofNullable(verifiedMessage),
|
||||
Optional.ofNullable(profileKey),
|
||||
Optional.ofNullable(contact == null ? null : contact.messageExpirationTime()),
|
||||
Optional.ofNullable(contact == null ? null : contact.messageExpirationTimeVersion()),
|
||||
Optional.empty(),
|
||||
contact != null && contact.isArchived());
|
||||
Optional.empty());
|
||||
}
|
||||
|
||||
public SendMessageResult sendBlockedList() {
|
||||
var addresses = new ArrayList<SignalServiceAddress>();
|
||||
var addresses = new ArrayList<BlockedListMessage.Individual>();
|
||||
for (var record : account.getContactStore().getContacts()) {
|
||||
if (record.second().isBlocked()) {
|
||||
addresses.add(context.getRecipientHelper().resolveSignalServiceAddress(record.first()));
|
||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(record.first());
|
||||
if (address.aci().isPresent() || address.number().isPresent()) {
|
||||
addresses.add(new BlockedListMessage.Individual(address.aci().orElse(null),
|
||||
address.number().orElse(null)));
|
||||
}
|
||||
}
|
||||
}
|
||||
var groupIds = new ArrayList<byte[]>();
|
||||
|
@ -262,7 +243,9 @@ public class SyncHelper {
|
|||
}
|
||||
|
||||
public SendMessageResult sendVerifiedMessage(
|
||||
SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel
|
||||
SignalServiceAddress destination,
|
||||
IdentityKey identityKey,
|
||||
TrustLevel trustLevel
|
||||
) {
|
||||
var verifiedMessage = new VerifiedMessage(destination,
|
||||
identityKey,
|
||||
|
@ -272,13 +255,16 @@ public class SyncHelper {
|
|||
}
|
||||
|
||||
public SendMessageResult sendKeysMessage() {
|
||||
var keysMessage = new KeysMessage(Optional.ofNullable(account.getOrCreateStorageKey()),
|
||||
Optional.ofNullable(account.getOrCreatePinMasterKey()));
|
||||
var keysMessage = new KeysMessage(account.getOrCreateStorageKey(),
|
||||
account.getOrCreatePinMasterKey(),
|
||||
account.getOrCreateAccountEntropyPool(),
|
||||
account.getOrCreateMediaRootBackupKey());
|
||||
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
|
||||
}
|
||||
|
||||
public SendMessageResult sendStickerOperationsMessage(
|
||||
List<StickerPack> installStickers, List<StickerPack> removeStickers
|
||||
List<StickerPack> installStickers,
|
||||
List<StickerPack> removeStickers
|
||||
) {
|
||||
var installStickerMessages = installStickers.stream().map(s -> getStickerPackOperationMessage(s, true));
|
||||
var removeStickerMessages = removeStickers.stream().map(s -> getStickerPackOperationMessage(s, false));
|
||||
|
@ -288,7 +274,8 @@ public class SyncHelper {
|
|||
}
|
||||
|
||||
private static StickerPackOperationMessage getStickerPackOperationMessage(
|
||||
final StickerPack s, final boolean installed
|
||||
final StickerPack s,
|
||||
final boolean installed
|
||||
) {
|
||||
return new StickerPackOperationMessage(s.packId().serialize(),
|
||||
s.packKey(),
|
||||
|
@ -354,7 +341,7 @@ public class SyncHelper {
|
|||
c = s.read();
|
||||
} catch (IOException e) {
|
||||
if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) {
|
||||
logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
|
||||
logger.debug("Sync contacts contained invalid contact, ignoring: {}", e.getMessage());
|
||||
continue;
|
||||
} else {
|
||||
throw e;
|
||||
|
@ -364,9 +351,6 @@ public class SyncHelper {
|
|||
break;
|
||||
}
|
||||
final var address = new RecipientAddress(c.getAci(), Optional.empty(), c.getE164(), Optional.empty());
|
||||
if (address.matches(account.getSelfRecipientAddress()) && c.getProfileKey().isPresent()) {
|
||||
account.setProfileKey(c.getProfileKey().get());
|
||||
}
|
||||
final var recipientId = account.getRecipientTrustedResolver().resolveRecipientTrusted(address);
|
||||
var contact = account.getContactStore().getContact(recipientId);
|
||||
final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
|
||||
|
@ -378,19 +362,6 @@ public class SyncHelper {
|
|||
builder.withGivenName(c.getName().get());
|
||||
builder.withFamilyName(null);
|
||||
}
|
||||
if (c.getColor().isPresent()) {
|
||||
builder.withColor(c.getColor().get());
|
||||
}
|
||||
if (c.getProfileKey().isPresent()) {
|
||||
account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get());
|
||||
}
|
||||
if (c.getVerified().isPresent()) {
|
||||
final var verifiedMessage = c.getVerified().get();
|
||||
account.getIdentityKeyStore()
|
||||
.setIdentityTrustLevel(verifiedMessage.getDestination().getServiceId(),
|
||||
verifiedMessage.getIdentityKey(),
|
||||
TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
|
||||
}
|
||||
if (c.getExpirationTimer().isPresent()) {
|
||||
if (c.getExpirationTimerVersion().isPresent() && (
|
||||
contact == null || c.getExpirationTimerVersion().get() > contact.messageExpirationTimeVersion()
|
||||
|
@ -399,13 +370,12 @@ public class SyncHelper {
|
|||
builder.withMessageExpirationTimeVersion(c.getExpirationTimerVersion().get());
|
||||
} else {
|
||||
logger.debug(
|
||||
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: ${}",
|
||||
"[ContactSync] {} was synced with an old expiration timer. Ignoring. Received: {} Current: {}",
|
||||
recipientId,
|
||||
c.getExpirationTimerVersion(),
|
||||
contact == null ? 1 : contact.messageExpirationTimeVersion());
|
||||
}
|
||||
}
|
||||
builder.withIsArchived(c.isArchived());
|
||||
account.getContactStore().storeContact(recipientId, builder.build());
|
||||
|
||||
if (c.getAvatar().isPresent()) {
|
||||
|
@ -414,15 +384,14 @@ public class SyncHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public SendMessageResult sendMessageRequestResponse(
|
||||
final MessageRequestResponse.Type type, final GroupId groupId
|
||||
) {
|
||||
public SendMessageResult sendMessageRequestResponse(final MessageRequestResponse.Type type, final GroupId groupId) {
|
||||
final var response = MessageRequestResponseMessage.forGroup(groupId.serialize(), localToRemoteType(type));
|
||||
return context.getSendHelper().sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response));
|
||||
}
|
||||
|
||||
public SendMessageResult sendMessageRequestResponse(
|
||||
final MessageRequestResponse.Type type, final RecipientId recipientId
|
||||
final MessageRequestResponse.Type type,
|
||||
final RecipientId recipientId
|
||||
) {
|
||||
final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
|
||||
if (address.serviceId().isEmpty()) {
|
||||
|
|
|
@ -18,6 +18,8 @@ import java.io.IOException;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
|
||||
public class UnidentifiedAccessHelper {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class);
|
||||
|
@ -109,7 +111,8 @@ public class UnidentifiedAccessHelper {
|
|||
return privacySenderCertificate.getSerialized();
|
||||
}
|
||||
try {
|
||||
final var certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy();
|
||||
final var certificate = handleResponseException(dependencies.getCertificateApi()
|
||||
.getSenderCertificateForPhoneNumberPrivacy());
|
||||
privacySenderCertificate = new SenderCertificate(certificate);
|
||||
return certificate;
|
||||
} catch (IOException | InvalidCertificateException e) {
|
||||
|
@ -125,7 +128,7 @@ public class UnidentifiedAccessHelper {
|
|||
return senderCertificate.getSerialized();
|
||||
}
|
||||
try {
|
||||
final var certificate = dependencies.getAccountManager().getSenderCertificate();
|
||||
final var certificate = handleResponseException(dependencies.getCertificateApi().getSenderCertificate());
|
||||
this.senderCertificate = new SenderCertificate(certificate);
|
||||
return certificate;
|
||||
} catch (IOException | InvalidCertificateException e) {
|
||||
|
@ -158,7 +161,8 @@ public class UnidentifiedAccessHelper {
|
|||
}
|
||||
|
||||
private static byte[] getTargetUnidentifiedAccessKey(
|
||||
final Profile targetProfile, final ProfileKey theirProfileKey
|
||||
final Profile targetProfile,
|
||||
final ProfileKey theirProfileKey
|
||||
) {
|
||||
return switch (targetProfile.getUnidentifiedAccessMode()) {
|
||||
case ENABLED -> theirProfileKey == null ? null : UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.asamk.signal.manager.api.IdentityVerificationCode;
|
|||
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
||||
import org.asamk.signal.manager.api.InvalidNumberException;
|
||||
import org.asamk.signal.manager.api.InvalidStickerException;
|
||||
import org.asamk.signal.manager.api.InvalidUsernameException;
|
||||
import org.asamk.signal.manager.api.LastGroupAdminException;
|
||||
|
@ -47,6 +48,7 @@ import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
|||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.PendingAdminApprovalException;
|
||||
import org.asamk.signal.manager.api.PhoneNumberSharingMode;
|
||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.Profile;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
|
@ -68,7 +70,6 @@ import org.asamk.signal.manager.api.UserStatus;
|
|||
import org.asamk.signal.manager.api.UsernameLinkUrl;
|
||||
import org.asamk.signal.manager.api.UsernameStatus;
|
||||
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.asamk.signal.manager.helper.AccountFileUpdater;
|
||||
import org.asamk.signal.manager.helper.Context;
|
||||
|
@ -88,12 +89,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPack;
|
|||
import org.asamk.signal.manager.util.AttachmentUtils;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.asamk.signal.manager.util.MimeUtils;
|
||||
import org.asamk.signal.manager.util.PhoneNumberFormatter;
|
||||
import org.asamk.signal.manager.util.StickerUtils;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
||||
|
@ -107,8 +108,6 @@ import org.whispersystems.signalservice.api.push.exceptions.CdsiResourceExhauste
|
|||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
@ -133,13 +132,18 @@ import java.util.concurrent.ExecutorService;
|
|||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import okio.Utf8;
|
||||
|
||||
import static org.asamk.signal.manager.config.ServiceConfig.MAX_MESSAGE_SIZE_BYTES;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
import static org.signal.core.util.StringExtensionsKt.splitByByteLength;
|
||||
|
||||
public class ManagerImpl implements Manager {
|
||||
|
||||
|
@ -158,6 +162,7 @@ public class ManagerImpl implements Manager {
|
|||
private final List<Runnable> closedListeners = new ArrayList<>();
|
||||
private final List<Runnable> addressChangedListeners = new ArrayList<>();
|
||||
private final CompositeDisposable disposable = new CompositeDisposable();
|
||||
private final AtomicLong lastMessageTimestamp = new AtomicLong();
|
||||
|
||||
public ManagerImpl(
|
||||
SignalAccount account,
|
||||
|
@ -168,15 +173,7 @@ public class ManagerImpl implements Manager {
|
|||
) {
|
||||
this.account = account;
|
||||
|
||||
final var sessionLock = new SignalSessionLock() {
|
||||
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
|
||||
|
||||
@Override
|
||||
public Lock acquire() {
|
||||
LEGACY_LOCK.lock();
|
||||
return LEGACY_LOCK::unlock;
|
||||
}
|
||||
};
|
||||
final var sessionLock = new ReentrantSignalSessionLock();
|
||||
this.dependencies = new SignalDependencies(serviceEnvironmentConfig,
|
||||
userAgent,
|
||||
account.getCredentialsProvider(),
|
||||
|
@ -288,7 +285,7 @@ public class ManagerImpl implements Manager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) {
|
||||
public Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException {
|
||||
final var registeredUsers = new HashMap<String, RecipientAddress>();
|
||||
for (final var username : usernames) {
|
||||
try {
|
||||
|
@ -417,7 +414,9 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public void startChangeNumber(
|
||||
String newNumber, boolean voiceVerification, String captcha
|
||||
String newNumber,
|
||||
boolean voiceVerification,
|
||||
String captcha
|
||||
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, NotPrimaryDeviceException, VerificationMethodNotAvailableException {
|
||||
if (!account.isPrimaryDevice()) {
|
||||
throw new NotPrimaryDeviceException();
|
||||
|
@ -427,8 +426,10 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public void finishChangeNumber(
|
||||
String newNumber, String verificationCode, String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException {
|
||||
String newNumber,
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IncorrectPinException, PinLockedException, IOException, NotPrimaryDeviceException, PinLockMissingException {
|
||||
if (!account.isPrimaryDevice()) {
|
||||
throw new NotPrimaryDeviceException();
|
||||
}
|
||||
|
@ -447,12 +448,13 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public void submitRateLimitRecaptchaChallenge(
|
||||
String challenge, String captcha
|
||||
String challenge,
|
||||
String captcha
|
||||
) throws IOException, CaptchaRejectedException {
|
||||
captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
|
||||
captcha = captcha == null ? "" : captcha.replace("signalcaptcha://", "");
|
||||
|
||||
try {
|
||||
dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha);
|
||||
handleResponseException(dependencies.getRateLimitChallengeApi().submitCaptchaChallenge(challenge, captcha));
|
||||
} catch (org.whispersystems.signalservice.internal.push.exceptions.CaptchaRejectedException ignored) {
|
||||
throw new CaptchaRejectedException();
|
||||
}
|
||||
|
@ -460,7 +462,7 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public List<Device> getLinkedDevices() throws IOException {
|
||||
var devices = dependencies.getAccountManager().getDevices();
|
||||
var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
|
||||
account.setMultiDevice(devices.size() > 1);
|
||||
var identityKey = account.getAciIdentityKeyPair().getPrivateKey();
|
||||
return devices.stream().map(d -> {
|
||||
|
@ -527,7 +529,8 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public SendGroupMessageResults quitGroup(
|
||||
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins
|
||||
GroupId groupId,
|
||||
Set<RecipientIdentifier.Single> groupAdmins
|
||||
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException {
|
||||
final var newAdmins = context.getRecipientHelper().resolveRecipients(groupAdmins);
|
||||
return context.getGroupHelper().quitGroup(groupId, newAdmins);
|
||||
|
@ -545,7 +548,9 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
String name, Set<RecipientIdentifier.Single> members, String avatarFile
|
||||
String name,
|
||||
Set<RecipientIdentifier.Single> members,
|
||||
String avatarFile
|
||||
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException {
|
||||
return context.getGroupHelper()
|
||||
.createGroup(name,
|
||||
|
@ -555,7 +560,8 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public SendGroupMessageResults updateGroup(
|
||||
final GroupId groupId, final UpdateGroup updateGroup
|
||||
final GroupId groupId,
|
||||
final UpdateGroup updateGroup
|
||||
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
||||
return context.getGroupHelper()
|
||||
.updateGroup(groupId,
|
||||
|
@ -595,8 +601,28 @@ public class ManagerImpl implements Manager {
|
|||
return context.getGroupHelper().joinGroup(inviteLinkUrl);
|
||||
}
|
||||
|
||||
private long getNextMessageTimestamp() {
|
||||
while (true) {
|
||||
final var last = lastMessageTimestamp.get();
|
||||
final var timestamp = System.currentTimeMillis();
|
||||
if (last == timestamp) {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (lastMessageTimestamp.compareAndSet(last, timestamp)) {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SendMessageResults sendMessage(
|
||||
SignalServiceDataMessage.Builder messageBuilder, Set<RecipientIdentifier> recipients, boolean notifySelf
|
||||
SignalServiceDataMessage.Builder messageBuilder,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
boolean notifySelf
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
return sendMessage(messageBuilder, recipients, notifySelf, Optional.empty());
|
||||
}
|
||||
|
@ -608,7 +634,7 @@ public class ManagerImpl implements Manager {
|
|||
Optional<Long> editTargetTimestamp
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
long timestamp = getNextMessageTimestamp();
|
||||
messageBuilder.withTimestamp(timestamp);
|
||||
for (final var recipient : recipients) {
|
||||
if (recipient instanceof RecipientIdentifier.NoteToSelf || (
|
||||
|
@ -644,10 +670,11 @@ public class ManagerImpl implements Manager {
|
|||
}
|
||||
|
||||
private SendMessageResults sendTypingMessage(
|
||||
SignalServiceTypingMessage.Action action, Set<RecipientIdentifier> recipients
|
||||
SignalServiceTypingMessage.Action action,
|
||||
Set<RecipientIdentifier> recipients
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||
final var timestamp = System.currentTimeMillis();
|
||||
final var timestamp = getNextMessageTimestamp();
|
||||
for (var recipient : recipients) {
|
||||
if (recipient instanceof RecipientIdentifier.Single single) {
|
||||
final var message = new SignalServiceTypingMessage(action, timestamp, Optional.empty());
|
||||
|
@ -671,16 +698,15 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public SendMessageResults sendTypingMessage(
|
||||
TypingAction action, Set<RecipientIdentifier> recipients
|
||||
TypingAction action,
|
||||
Set<RecipientIdentifier> recipients
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
return sendTypingMessage(action.toSignalService(), recipients);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendReadReceipt(
|
||||
RecipientIdentifier.Single sender, List<Long> messageIds
|
||||
) {
|
||||
final var timestamp = System.currentTimeMillis();
|
||||
public SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
|
||||
final var timestamp = getNextMessageTimestamp();
|
||||
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
|
||||
messageIds,
|
||||
timestamp);
|
||||
|
@ -689,10 +715,8 @@ public class ManagerImpl implements Manager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public SendMessageResults sendViewedReceipt(
|
||||
RecipientIdentifier.Single sender, List<Long> messageIds
|
||||
) {
|
||||
final var timestamp = System.currentTimeMillis();
|
||||
public SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds) {
|
||||
final var timestamp = getNextMessageTimestamp();
|
||||
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
|
||||
messageIds,
|
||||
timestamp);
|
||||
|
@ -724,7 +748,9 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public SendMessageResults sendMessage(
|
||||
Message message, Set<RecipientIdentifier> recipients, boolean notifySelf
|
||||
Message message,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
boolean notifySelf
|
||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
|
||||
final var selfProfile = context.getProfileHelper().getSelfProfile();
|
||||
if (selfProfile == null || selfProfile.getDisplayName().isEmpty()) {
|
||||
|
@ -738,7 +764,9 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public SendMessageResults sendEditMessage(
|
||||
Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp
|
||||
Message message,
|
||||
Set<RecipientIdentifier> recipients,
|
||||
long editTargetTimestamp
|
||||
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
|
||||
final var messageBuilder = SignalServiceDataMessage.newBuilder();
|
||||
applyMessage(messageBuilder, message);
|
||||
|
@ -746,20 +774,28 @@ public class ManagerImpl implements Manager {
|
|||
}
|
||||
|
||||
private void applyMessage(
|
||||
final SignalServiceDataMessage.Builder messageBuilder, final Message message
|
||||
final SignalServiceDataMessage.Builder messageBuilder,
|
||||
final Message message
|
||||
) throws AttachmentInvalidException, IOException, UnregisteredRecipientException, InvalidStickerException {
|
||||
final var additionalAttachments = new ArrayList<SignalServiceAttachment>();
|
||||
if (message.messageText().length() > ServiceConfig.MAX_MESSAGE_BODY_SIZE) {
|
||||
final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
|
||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||
final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
|
||||
MimeUtils.LONG_TEXT,
|
||||
messageBytes.length);
|
||||
final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
||||
Optional.empty(),
|
||||
uploadSpec);
|
||||
messageBuilder.withBody(message.messageText().substring(0, ServiceConfig.MAX_MESSAGE_BODY_SIZE));
|
||||
additionalAttachments.add(context.getAttachmentHelper().uploadAttachment(textAttachment));
|
||||
if (Utf8.size(message.messageText()) > MAX_MESSAGE_SIZE_BYTES) {
|
||||
final var result = splitByByteLength(message.messageText(), MAX_MESSAGE_SIZE_BYTES);
|
||||
final var trimmed = result.getFirst();
|
||||
final var remainder = result.getSecond();
|
||||
if (remainder != null) {
|
||||
final var messageBytes = message.messageText().getBytes(StandardCharsets.UTF_8);
|
||||
final var uploadSpec = dependencies.getMessageSender().getResumableUploadSpec();
|
||||
final var streamDetails = new StreamDetails(new ByteArrayInputStream(messageBytes),
|
||||
MimeUtils.LONG_TEXT,
|
||||
messageBytes.length);
|
||||
final var textAttachment = AttachmentUtils.createAttachmentStream(streamDetails,
|
||||
Optional.empty(),
|
||||
uploadSpec);
|
||||
messageBuilder.withBody(trimmed);
|
||||
additionalAttachments.add(context.getAttachmentHelper().uploadAttachment(textAttachment));
|
||||
} else {
|
||||
messageBuilder.withBody(message.messageText());
|
||||
}
|
||||
} else {
|
||||
messageBuilder.withBody(message.messageText());
|
||||
}
|
||||
|
@ -774,6 +810,7 @@ public class ManagerImpl implements Manager {
|
|||
} else if (!additionalAttachments.isEmpty()) {
|
||||
messageBuilder.withAttachments(additionalAttachments);
|
||||
}
|
||||
messageBuilder.withViewOnce(message.viewOnce());
|
||||
if (!message.mentions().isEmpty()) {
|
||||
messageBuilder.withMentions(resolveMentions(message.mentions()));
|
||||
}
|
||||
|
@ -863,7 +900,8 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public SendMessageResults sendRemoteDeleteMessage(
|
||||
long targetSentTimestamp, Set<RecipientIdentifier> recipients
|
||||
long targetSentTimestamp,
|
||||
Set<RecipientIdentifier> recipients
|
||||
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
||||
var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
|
||||
final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
|
||||
|
@ -873,7 +911,7 @@ public class ManagerImpl implements Manager {
|
|||
.deleteEntryForRecipientNonGroup(targetSentTimestamp, ACI.from(u.uuid()));
|
||||
} else if (recipient instanceof RecipientIdentifier.Pni pni) {
|
||||
account.getMessageSendLogStore()
|
||||
.deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.parseOrThrow(pni.pni()));
|
||||
.deleteEntryForRecipientNonGroup(targetSentTimestamp, PNI.from(pni.pni()));
|
||||
} else if (recipient instanceof RecipientIdentifier.Single r) {
|
||||
try {
|
||||
final var recipientId = context.getRecipientHelper().resolveRecipient(r);
|
||||
|
@ -915,7 +953,9 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public SendMessageResults sendPaymentNotificationMessage(
|
||||
byte[] receipt, String note, RecipientIdentifier.Single recipient
|
||||
byte[] receipt,
|
||||
String note,
|
||||
RecipientIdentifier.Single recipient
|
||||
) throws IOException {
|
||||
final var paymentNotification = new SignalServiceDataMessage.PaymentNotification(receipt, note);
|
||||
final var payment = new SignalServiceDataMessage.Payment(paymentNotification, null);
|
||||
|
@ -958,7 +998,8 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public SendMessageResults sendMessageRequestResponse(
|
||||
final MessageRequestResponse.Type type, final Set<RecipientIdentifier> recipients
|
||||
final MessageRequestResponse.Type type,
|
||||
final Set<RecipientIdentifier> recipients
|
||||
) {
|
||||
var results = new HashMap<RecipientIdentifier, List<SendMessageResult>>();
|
||||
for (final var recipient : recipients) {
|
||||
|
@ -1021,19 +1062,30 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public void setContactName(
|
||||
RecipientIdentifier.Single recipient, String givenName, final String familyName
|
||||
final RecipientIdentifier.Single recipient,
|
||||
final String givenName,
|
||||
final String familyName,
|
||||
final String nickGivenName,
|
||||
final String nickFamilyName,
|
||||
final String note
|
||||
) throws NotPrimaryDeviceException, UnregisteredRecipientException {
|
||||
if (!account.isPrimaryDevice()) {
|
||||
throw new NotPrimaryDeviceException();
|
||||
}
|
||||
context.getContactHelper()
|
||||
.setContactName(context.getRecipientHelper().resolveRecipient(recipient), givenName, familyName);
|
||||
.setContactName(context.getRecipientHelper().resolveRecipient(recipient),
|
||||
givenName,
|
||||
familyName,
|
||||
nickGivenName,
|
||||
nickFamilyName,
|
||||
note);
|
||||
syncRemoteStorage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContactsBlocked(
|
||||
Collection<RecipientIdentifier.Single> recipients, boolean blocked
|
||||
Collection<RecipientIdentifier.Single> recipients,
|
||||
boolean blocked
|
||||
) throws IOException, UnregisteredRecipientException {
|
||||
if (recipients.isEmpty()) {
|
||||
return;
|
||||
|
@ -1067,7 +1119,8 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public void setGroupsBlocked(
|
||||
final Collection<GroupId> groupIds, final boolean blocked
|
||||
final Collection<GroupId> groupIds,
|
||||
final boolean blocked
|
||||
) throws GroupNotFoundException, IOException {
|
||||
if (groupIds.isEmpty()) {
|
||||
return;
|
||||
|
@ -1093,7 +1146,8 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public void setExpirationTimer(
|
||||
RecipientIdentifier.Single recipient, int messageExpirationTimer
|
||||
RecipientIdentifier.Single recipient,
|
||||
int messageExpirationTimer
|
||||
) throws IOException, UnregisteredRecipientException {
|
||||
var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||
context.getContactHelper().setExpirationTimer(recipientId, messageExpirationTimer);
|
||||
|
@ -1255,7 +1309,9 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public void receiveMessages(
|
||||
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
|
||||
Optional<Duration> timeout,
|
||||
Optional<Integer> maxMessages,
|
||||
ReceiveMessageHandler handler
|
||||
) throws IOException, AlreadyReceivingException {
|
||||
receiveMessages(timeout.orElse(Duration.ofMinutes(1)), timeout.isPresent(), maxMessages.orElse(null), handler);
|
||||
}
|
||||
|
@ -1275,7 +1331,10 @@ public class ManagerImpl implements Manager {
|
|||
}
|
||||
|
||||
private void receiveMessages(
|
||||
Duration timeout, boolean returnOnTimeout, Integer maxMessages, ReceiveMessageHandler handler
|
||||
Duration timeout,
|
||||
boolean returnOnTimeout,
|
||||
Integer maxMessages,
|
||||
ReceiveMessageHandler handler
|
||||
) throws IOException, AlreadyReceivingException {
|
||||
synchronized (messageHandlers) {
|
||||
if (isReceiving()) {
|
||||
|
@ -1431,7 +1490,8 @@ public class ManagerImpl implements Manager {
|
|||
|
||||
@Override
|
||||
public boolean trustIdentityVerified(
|
||||
RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode
|
||||
RecipientIdentifier.Single recipient,
|
||||
IdentityVerificationCode verificationCode
|
||||
) throws UnregisteredRecipientException {
|
||||
return switch (verificationCode) {
|
||||
case IdentityVerificationCode.Fingerprint fingerprint -> trustIdentity(recipient,
|
||||
|
@ -1450,7 +1510,8 @@ public class ManagerImpl implements Manager {
|
|||
}
|
||||
|
||||
private boolean trustIdentity(
|
||||
RecipientIdentifier.Single recipient, Function<RecipientId, Boolean> trustMethod
|
||||
RecipientIdentifier.Single recipient,
|
||||
Function<RecipientId, Boolean> trustMethod
|
||||
) throws UnregisteredRecipientException {
|
||||
final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
|
||||
final var updated = trustMethod.apply(recipientId);
|
||||
|
@ -1546,7 +1607,8 @@ public class ManagerImpl implements Manager {
|
|||
context.close();
|
||||
executor.close();
|
||||
|
||||
dependencies.getSignalWebSocket().disconnect();
|
||||
dependencies.getAuthenticatedSignalWebSocket().disconnect();
|
||||
dependencies.getUnauthenticatedSignalWebSocket().disconnect();
|
||||
dependencies.getPushServiceSocket().close();
|
||||
disposable.dispose();
|
||||
|
||||
|
|
|
@ -29,12 +29,10 @@ import org.asamk.signal.manager.util.KeyUtils;
|
|||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||
import org.whispersystems.signalservice.api.registration.ProvisioningApi;
|
||||
import org.whispersystems.signalservice.api.util.DeviceNameUtil;
|
||||
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
|
@ -58,7 +56,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||
private final Consumer<Manager> newManagerListener;
|
||||
private final AccountsStore accountsStore;
|
||||
|
||||
private final SignalServiceAccountManager accountManager;
|
||||
private final ProvisioningApi provisioningApi;
|
||||
private final IdentityKeyPair tempIdentityKey;
|
||||
private final String password;
|
||||
|
||||
|
@ -77,8 +75,6 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||
|
||||
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
|
||||
password = KeyUtils.createPassword();
|
||||
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
|
||||
final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE);
|
||||
final var credentialsProvider = new DynamicCredentialsProvider(null,
|
||||
null,
|
||||
null,
|
||||
|
@ -87,23 +83,22 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
credentialsProvider,
|
||||
userAgent,
|
||||
clientZkOperations.getProfileOperations(),
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||
accountManager = new SignalServiceAccountManager(pushServiceSocket,
|
||||
new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent),
|
||||
groupsV2Operations);
|
||||
final var provisioningSocket = new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
userAgent);
|
||||
this.provisioningApi = new ProvisioningApi(pushServiceSocket, provisioningSocket, credentialsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getDeviceLinkUri() throws TimeoutException, IOException {
|
||||
var deviceUuid = accountManager.getNewDeviceUuid();
|
||||
var deviceUuid = provisioningApi.getNewDeviceUuid();
|
||||
|
||||
return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
|
||||
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
|
||||
var ret = provisioningApi.getNewDeviceRegistration(tempIdentityKey);
|
||||
var number = ret.getNumber();
|
||||
var aci = ret.getAci();
|
||||
var pni = ret.getPni();
|
||||
|
@ -150,7 +145,9 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||
ret.getAciIdentity(),
|
||||
ret.getPniIdentity(),
|
||||
profileKey,
|
||||
ret.getMasterKey());
|
||||
ret.getMasterKey(),
|
||||
ret.getAccountEntropyPool(),
|
||||
ret.getMediaRootBackupKey());
|
||||
|
||||
account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
|
||||
|
||||
|
@ -158,7 +155,7 @@ public class ProvisioningManagerImpl implements ProvisioningManager {
|
|||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||
|
||||
logger.debug("Finishing new device registration");
|
||||
var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
||||
var deviceId = provisioningApi.finishNewDeviceRegistration(ret.getProvisioningCode(),
|
||||
account.getAccountAttributes(null),
|
||||
aciPreKeys,
|
||||
pniPreKeys);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package org.asamk.signal.manager.internal;
|
||||
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
class ReentrantSignalSessionLock implements SignalSessionLock {
|
||||
|
||||
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
|
||||
|
||||
@Override
|
||||
public Lock acquire() {
|
||||
LEGACY_LOCK.lock();
|
||||
return LEGACY_LOCK::unlock;
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import org.asamk.signal.manager.RegistrationManager;
|
|||
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
||||
import org.asamk.signal.manager.api.IncorrectPinException;
|
||||
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
||||
import org.asamk.signal.manager.api.PinLockMissingException;
|
||||
import org.asamk.signal.manager.api.PinLockedException;
|
||||
import org.asamk.signal.manager.api.RateLimitException;
|
||||
import org.asamk.signal.manager.api.UpdateProfile;
|
||||
|
@ -32,14 +33,11 @@ import org.asamk.signal.manager.helper.PinHelper;
|
|||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.asamk.signal.manager.util.NumberVerificationUtils;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
|
||||
|
@ -48,13 +46,13 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|||
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
|
||||
import static org.asamk.signal.manager.util.Utils.handleResponseException;
|
||||
|
||||
public class RegistrationManagerImpl implements RegistrationManager {
|
||||
|
||||
|
@ -105,7 +103,9 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
|
||||
@Override
|
||||
public void register(
|
||||
boolean voiceVerification, String captcha, final boolean forceRegister
|
||||
boolean voiceVerification,
|
||||
String captcha,
|
||||
final boolean forceRegister
|
||||
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
|
||||
if (account.isRegistered()
|
||||
&& account.getServiceEnvironment() != null
|
||||
|
@ -130,12 +130,15 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
}
|
||||
|
||||
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
||||
logger.trace("Creating verification session");
|
||||
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
|
||||
account.getSessionId(account.getNumber()),
|
||||
id -> account.setSessionId(account.getNumber(), id),
|
||||
voiceVerification,
|
||||
captcha);
|
||||
logger.trace("Requesting verification code");
|
||||
NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
|
||||
logger.debug("Successfully requested verification code");
|
||||
account.setRegistered(false);
|
||||
} catch (DeprecatedVersionException e) {
|
||||
logger.debug("Signal-Server returned deprecated version exception", e);
|
||||
|
@ -145,8 +148,9 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
|
||||
@Override
|
||||
public void verifyAccount(
|
||||
String verificationCode, String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException {
|
||||
String verificationCode,
|
||||
String pin
|
||||
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException {
|
||||
if (account.isRegistered()) {
|
||||
throw new IOException("Account is already registered");
|
||||
}
|
||||
|
@ -196,7 +200,7 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
|
||||
final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
|
||||
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
||||
final var response = Utils.handleResponseException(registrationApi.registerAccount(null,
|
||||
final var response = handleResponseException(registrationApi.registerAccount(null,
|
||||
recoveryPassword,
|
||||
account.getAccountAttributes(null),
|
||||
aciPreKeys,
|
||||
|
@ -218,8 +222,14 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
|
||||
private boolean attemptReactivateAccount() {
|
||||
try {
|
||||
final var accountManager = createAuthenticatedSignalServiceAccountManager();
|
||||
accountManager.setAccountAttributes(account.getAccountAttributes(null));
|
||||
final var dependencies = new SignalDependencies(serviceEnvironmentConfig,
|
||||
userAgent,
|
||||
account.getCredentialsProvider(),
|
||||
account.getSignalServiceDataStore(),
|
||||
null,
|
||||
new ReentrantSignalSessionLock());
|
||||
handleResponseException(dependencies.getAccountApi()
|
||||
.setAccountAttributes(account.getAccountAttributes(null)));
|
||||
account.setRegistered(true);
|
||||
logger.info("Reactivated existing account, verify is not necessary.");
|
||||
if (newManagerListener != null) {
|
||||
|
@ -238,17 +248,6 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
private SignalServiceAccountManager createAuthenticatedSignalServiceAccountManager() {
|
||||
final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
|
||||
final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
account.getCredentialsProvider(),
|
||||
userAgent,
|
||||
clientZkOperations.getProfileOperations(),
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
|
||||
final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE);
|
||||
return new SignalServiceAccountManager(pushServiceSocket, null, groupsV2Operations);
|
||||
}
|
||||
|
||||
private VerifyAccountResponse verifyAccountWithCode(
|
||||
final String sessionId,
|
||||
final String verificationCode,
|
||||
|
@ -258,11 +257,11 @@ public class RegistrationManagerImpl implements RegistrationManager {
|
|||
) throws IOException {
|
||||
final var registrationApi = unauthenticatedAccountManager.getRegistrationApi();
|
||||
try {
|
||||
Utils.handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
|
||||
handleResponseException(registrationApi.verifyAccount(sessionId, verificationCode));
|
||||
} catch (AlreadyVerifiedException e) {
|
||||
// Already verified so can continue registering
|
||||
}
|
||||
return Utils.handleResponseException(registrationApi.registerAccount(sessionId,
|
||||
return handleResponseException(registrationApi.registerAccount(sessionId,
|
||||
null,
|
||||
account.getAccountAttributes(registrationLock),
|
||||
aciPreKeys,
|
||||
|
|
|
@ -2,39 +2,58 @@ package org.asamk.signal.manager.internal;
|
|||
|
||||
import org.asamk.signal.manager.config.ServiceConfig;
|
||||
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
|
||||
import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.protocol.UsePqRatchet;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.account.AccountApi;
|
||||
import org.whispersystems.signalservice.api.attachment.AttachmentApi;
|
||||
import org.whispersystems.signalservice.api.cds.CdsApi;
|
||||
import org.whispersystems.signalservice.api.certificate.CertificateApi;
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
import org.whispersystems.signalservice.api.keys.KeysApi;
|
||||
import org.whispersystems.signalservice.api.link.LinkDeviceApi;
|
||||
import org.whispersystems.signalservice.api.message.MessageApi;
|
||||
import org.whispersystems.signalservice.api.profiles.ProfileApi;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.ratelimit.RateLimitChallengeApi;
|
||||
import org.whispersystems.signalservice.api.registration.RegistrationApi;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceApi;
|
||||
import org.whispersystems.signalservice.api.storage.StorageServiceRepository;
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery;
|
||||
import org.whispersystems.signalservice.api.username.UsernameApi;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
|
||||
import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SignalDependencies {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SignalDependencies.class);
|
||||
|
||||
private final Object LOCK = new Object();
|
||||
|
||||
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
|
||||
|
@ -47,20 +66,31 @@ public class SignalDependencies {
|
|||
private boolean allowStories = true;
|
||||
|
||||
private SignalServiceAccountManager accountManager;
|
||||
private AccountApi accountApi;
|
||||
private RateLimitChallengeApi rateLimitChallengeApi;
|
||||
private CdsApi cdsApi;
|
||||
private UsernameApi usernameApi;
|
||||
private GroupsV2Api groupsV2Api;
|
||||
private RegistrationApi registrationApi;
|
||||
private LinkDeviceApi linkDeviceApi;
|
||||
private StorageServiceApi storageServiceApi;
|
||||
private CertificateApi certificateApi;
|
||||
private AttachmentApi attachmentApi;
|
||||
private MessageApi messageApi;
|
||||
private KeysApi keysApi;
|
||||
private GroupsV2Operations groupsV2Operations;
|
||||
private ClientZkOperations clientZkOperations;
|
||||
|
||||
private PushServiceSocket pushServiceSocket;
|
||||
private ProvisioningSocket provisioningSocket;
|
||||
private Network libSignalNetwork;
|
||||
private SignalWebSocket signalWebSocket;
|
||||
private SignalWebSocket.AuthenticatedWebSocket authenticatedSignalWebSocket;
|
||||
private SignalWebSocket.UnauthenticatedWebSocket unauthenticatedSignalWebSocket;
|
||||
private SignalServiceMessageReceiver messageReceiver;
|
||||
private SignalServiceMessageSender messageSender;
|
||||
|
||||
private List<SecureValueRecovery> secureValueRecovery;
|
||||
private ProfileService profileService;
|
||||
private ProfileApi profileApi;
|
||||
|
||||
SignalDependencies(
|
||||
final ServiceEnvironmentConfig serviceEnvironmentConfig,
|
||||
|
@ -90,7 +120,12 @@ public class SignalDependencies {
|
|||
this.registrationApi = null;
|
||||
this.secureValueRecovery = null;
|
||||
}
|
||||
getSignalWebSocket().forceNewWebSockets();
|
||||
if (this.authenticatedSignalWebSocket != null) {
|
||||
this.authenticatedSignalWebSocket.forceNewWebSocket();
|
||||
}
|
||||
if (this.unauthenticatedSignalWebSocket != null) {
|
||||
this.unauthenticatedSignalWebSocket.forceNewWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,25 +148,45 @@ public class SignalDependencies {
|
|||
() -> pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
credentialsProvider,
|
||||
userAgent,
|
||||
getClientZkProfileOperations(),
|
||||
ServiceConfig.AUTOMATIC_NETWORK_RETRY));
|
||||
}
|
||||
|
||||
public ProvisioningSocket getProvisioningSocket() {
|
||||
return getOrCreate(() -> provisioningSocket,
|
||||
() -> provisioningSocket = new ProvisioningSocket(getServiceEnvironmentConfig().signalServiceConfiguration(),
|
||||
userAgent));
|
||||
public Network getLibSignalNetwork() {
|
||||
return getOrCreate(() -> libSignalNetwork, () -> {
|
||||
libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent);
|
||||
setSignalNetworkProxy(libSignalNetwork);
|
||||
});
|
||||
}
|
||||
|
||||
public Network getLibSignalNetwork() {
|
||||
return getOrCreate(() -> libSignalNetwork,
|
||||
() -> libSignalNetwork = new Network(serviceEnvironmentConfig.netEnvironment(), userAgent));
|
||||
private void setSignalNetworkProxy(Network libSignalNetwork) {
|
||||
final var proxy = Utils.getHttpsProxy();
|
||||
if (proxy.address() instanceof InetSocketAddress addr) {
|
||||
switch (proxy.type()) {
|
||||
case Proxy.Type.DIRECT -> {
|
||||
}
|
||||
case Proxy.Type.HTTP -> {
|
||||
try {
|
||||
libSignalNetwork.setProxy("http", addr.getHostName(), addr.getPort(), null, null);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to set http proxy", e);
|
||||
}
|
||||
}
|
||||
case Proxy.Type.SOCKS -> {
|
||||
try {
|
||||
libSignalNetwork.setProxy("socks", addr.getHostName(), addr.getPort(), null, null);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to set socks proxy", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SignalServiceAccountManager getAccountManager() {
|
||||
return getOrCreate(() -> accountManager,
|
||||
() -> accountManager = new SignalServiceAccountManager(getPushServiceSocket(),
|
||||
getProvisioningSocket(),
|
||||
() -> accountManager = new SignalServiceAccountManager(getAuthenticatedSignalWebSocket(),
|
||||
getAccountApi(),
|
||||
getPushServiceSocket(),
|
||||
getGroupsV2Operations()));
|
||||
}
|
||||
|
||||
|
@ -147,6 +202,23 @@ public class SignalDependencies {
|
|||
ServiceConfig.GROUP_MAX_SIZE);
|
||||
}
|
||||
|
||||
public AccountApi getAccountApi() {
|
||||
return getOrCreate(() -> accountApi, () -> accountApi = new AccountApi(getAuthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public RateLimitChallengeApi getRateLimitChallengeApi() {
|
||||
return getOrCreate(() -> rateLimitChallengeApi,
|
||||
() -> rateLimitChallengeApi = new RateLimitChallengeApi(getAuthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public CdsApi getCdsApi() {
|
||||
return getOrCreate(() -> cdsApi, () -> cdsApi = new CdsApi(getAuthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public UsernameApi getUsernameApi() {
|
||||
return getOrCreate(() -> usernameApi, () -> usernameApi = new UsernameApi(getUnauthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public GroupsV2Api getGroupsV2Api() {
|
||||
return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api());
|
||||
}
|
||||
|
@ -155,6 +227,42 @@ public class SignalDependencies {
|
|||
return getOrCreate(() -> registrationApi, () -> registrationApi = getAccountManager().getRegistrationApi());
|
||||
}
|
||||
|
||||
public LinkDeviceApi getLinkDeviceApi() {
|
||||
return getOrCreate(() -> linkDeviceApi,
|
||||
() -> linkDeviceApi = new LinkDeviceApi(getAuthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
private StorageServiceApi getStorageServiceApi() {
|
||||
return getOrCreate(() -> storageServiceApi,
|
||||
() -> storageServiceApi = new StorageServiceApi(getAuthenticatedSignalWebSocket(),
|
||||
getPushServiceSocket()));
|
||||
}
|
||||
|
||||
public StorageServiceRepository getStorageServiceRepository() {
|
||||
return new StorageServiceRepository(getStorageServiceApi());
|
||||
}
|
||||
|
||||
public CertificateApi getCertificateApi() {
|
||||
return getOrCreate(() -> certificateApi,
|
||||
() -> certificateApi = new CertificateApi(getAuthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public AttachmentApi getAttachmentApi() {
|
||||
return getOrCreate(() -> attachmentApi,
|
||||
() -> attachmentApi = new AttachmentApi(getAuthenticatedSignalWebSocket(), getPushServiceSocket()));
|
||||
}
|
||||
|
||||
public MessageApi getMessageApi() {
|
||||
return getOrCreate(() -> messageApi,
|
||||
() -> messageApi = new MessageApi(getAuthenticatedSignalWebSocket(),
|
||||
getUnauthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public KeysApi getKeysApi() {
|
||||
return getOrCreate(() -> keysApi,
|
||||
() -> keysApi = new KeysApi(getAuthenticatedSignalWebSocket(), getUnauthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public GroupsV2Operations getGroupsV2Operations() {
|
||||
return getOrCreate(() -> groupsV2Operations,
|
||||
() -> groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration()),
|
||||
|
@ -171,33 +279,35 @@ public class SignalDependencies {
|
|||
return clientZkOperations.getProfileOperations();
|
||||
}
|
||||
|
||||
public SignalWebSocket getSignalWebSocket() {
|
||||
return getOrCreate(() -> signalWebSocket, () -> {
|
||||
public SignalWebSocket.AuthenticatedWebSocket getAuthenticatedSignalWebSocket() {
|
||||
return getOrCreate(() -> authenticatedSignalWebSocket, () -> {
|
||||
final var timer = new UptimeSleepTimer();
|
||||
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
|
||||
final var webSocketFactory = new WebSocketFactory() {
|
||||
@Override
|
||||
public WebSocketConnection createWebSocket() {
|
||||
return new OkHttpWebSocketConnection("normal",
|
||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
Optional.of(credentialsProvider),
|
||||
userAgent,
|
||||
healthMonitor,
|
||||
allowStories);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketConnection createUnidentifiedWebSocket() {
|
||||
return new OkHttpWebSocketConnection("unidentified",
|
||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
Optional.empty(),
|
||||
userAgent,
|
||||
healthMonitor,
|
||||
allowStories);
|
||||
}
|
||||
};
|
||||
signalWebSocket = new SignalWebSocket(webSocketFactory);
|
||||
healthMonitor.monitor(signalWebSocket);
|
||||
authenticatedSignalWebSocket = new SignalWebSocket.AuthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
|
||||
"normal",
|
||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
Optional.of(credentialsProvider),
|
||||
userAgent,
|
||||
healthMonitor,
|
||||
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
||||
healthMonitor.monitor(authenticatedSignalWebSocket);
|
||||
});
|
||||
}
|
||||
|
||||
public SignalWebSocket.UnauthenticatedWebSocket getUnauthenticatedSignalWebSocket() {
|
||||
return getOrCreate(() -> unauthenticatedSignalWebSocket, () -> {
|
||||
final var timer = new UptimeSleepTimer();
|
||||
final var healthMonitor = new SignalWebSocketHealthMonitor(timer);
|
||||
|
||||
unauthenticatedSignalWebSocket = new SignalWebSocket.UnauthenticatedWebSocket(() -> new OkHttpWebSocketConnection(
|
||||
"unidentified",
|
||||
serviceEnvironmentConfig.signalServiceConfiguration(),
|
||||
Optional.empty(),
|
||||
userAgent,
|
||||
healthMonitor,
|
||||
allowStories), () -> true, timer, TimeUnit.SECONDS.toMillis(10));
|
||||
healthMonitor.monitor(unauthenticatedSignalWebSocket);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -211,10 +321,14 @@ public class SignalDependencies {
|
|||
() -> messageSender = new SignalServiceMessageSender(getPushServiceSocket(),
|
||||
dataStore,
|
||||
sessionLock,
|
||||
getSignalWebSocket(),
|
||||
getAttachmentApi(),
|
||||
getMessageApi(),
|
||||
getKeysApi(),
|
||||
Optional.empty(),
|
||||
executor,
|
||||
ServiceConfig.MAX_ENVELOPE_SIZE));
|
||||
ServiceConfig.MAX_ENVELOPE_SIZE,
|
||||
() -> true,
|
||||
UsePqRatchet.NO));
|
||||
}
|
||||
|
||||
public List<SecureValueRecovery> getSecureValueRecovery() {
|
||||
|
@ -225,11 +339,19 @@ public class SignalDependencies {
|
|||
.toList());
|
||||
}
|
||||
|
||||
public ProfileApi getProfileApi() {
|
||||
return getOrCreate(() -> profileApi,
|
||||
() -> profileApi = new ProfileApi(getAuthenticatedSignalWebSocket(),
|
||||
getUnauthenticatedSignalWebSocket(),
|
||||
getPushServiceSocket(),
|
||||
getClientZkProfileOperations()));
|
||||
}
|
||||
|
||||
public ProfileService getProfileService() {
|
||||
return getOrCreate(() -> profileService,
|
||||
() -> profileService = new ProfileService(getClientZkProfileOperations(),
|
||||
getMessageReceiver(),
|
||||
getSignalWebSocket()));
|
||||
getAuthenticatedSignalWebSocket(),
|
||||
getUnauthenticatedSignalWebSocket()));
|
||||
}
|
||||
|
||||
public SignalServiceCipher getCipher(ServiceIdType serviceIdType) {
|
||||
|
|
|
@ -2,195 +2,157 @@ package org.asamk.signal.manager.internal;
|
|||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||
import org.whispersystems.signalservice.api.websocket.HealthMonitor;
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState;
|
||||
import org.whispersystems.signalservice.internal.websocket.OkHttpWebSocketConnection;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* Monitors the health of the identified and unidentified WebSockets. If either one appears to be
|
||||
* unhealthy, will trigger restarting both.
|
||||
* <p>
|
||||
* The monitor is also responsible for sending heartbeats/keep-alive messages to prevent
|
||||
* timeouts.
|
||||
*/
|
||||
final class SignalWebSocketHealthMonitor implements HealthMonitor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class);
|
||||
|
||||
/**
|
||||
* This is the amount of time in between sent keep alives. Must be greater than [KEEP_ALIVE_TIMEOUT]
|
||||
*/
|
||||
private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(OkHttpWebSocketConnection.KEEPALIVE_FREQUENCY_SECONDS);
|
||||
private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3;
|
||||
|
||||
private SignalWebSocket signalWebSocket;
|
||||
/**
|
||||
* This is the amount of time we will wait for a response to the keep alive before we consider the websockets dead.
|
||||
* It is required that this value be less than [KEEP_ALIVE_SEND_CADENCE]
|
||||
*/
|
||||
private static final long KEEP_ALIVE_TIMEOUT = TimeUnit.SECONDS.toMillis(20);
|
||||
|
||||
private final Executor executor = Executors.newSingleThreadExecutor();
|
||||
private final SleepTimer sleepTimer;
|
||||
|
||||
private volatile KeepAliveSender keepAliveSender;
|
||||
|
||||
private final HealthState identified = new HealthState();
|
||||
private final HealthState unidentified = new HealthState();
|
||||
private SignalWebSocket webSocket = null;
|
||||
private volatile KeepAliveSender keepAliveSender = null;
|
||||
private boolean needsKeepAlive = false;
|
||||
private long lastKeepAliveReceived = 0;
|
||||
|
||||
public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) {
|
||||
this.sleepTimer = sleepTimer;
|
||||
}
|
||||
|
||||
public void monitor(SignalWebSocket signalWebSocket) {
|
||||
Preconditions.checkNotNull(signalWebSocket);
|
||||
Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once");
|
||||
void monitor(SignalWebSocket webSocket) {
|
||||
Preconditions.checkNotNull(webSocket);
|
||||
Preconditions.checkArgument(this.webSocket == null, "monitor can only be called once");
|
||||
|
||||
this.signalWebSocket = signalWebSocket;
|
||||
executor.execute(() -> {
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
signalWebSocket.getWebSocketState()
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(Schedulers.computation())
|
||||
.distinctUntilChanged()
|
||||
.subscribe(s -> onStateChange(s, identified));
|
||||
this.webSocket = webSocket;
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
signalWebSocket.getUnidentifiedWebSocketState()
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(Schedulers.computation())
|
||||
.distinctUntilChanged()
|
||||
.subscribe(s -> onStateChange(s, unidentified));
|
||||
webSocket.getState()
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(Schedulers.computation())
|
||||
.distinctUntilChanged()
|
||||
.subscribe(this::onStateChanged);
|
||||
|
||||
webSocket.addKeepAliveChangeListener(() -> {
|
||||
executor.execute(this::updateKeepAliveSenderStatus);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) {
|
||||
switch (connectionState) {
|
||||
case CONNECTED -> logger.debug("WebSocket is now connected");
|
||||
case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed");
|
||||
case FAILED -> logger.debug("WebSocket connection failed");
|
||||
}
|
||||
private void onStateChanged(WebSocketConnectionState connectionState) {
|
||||
executor.execute(() -> {
|
||||
needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
|
||||
|
||||
healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED;
|
||||
updateKeepAliveSenderStatus();
|
||||
});
|
||||
}
|
||||
|
||||
if (keepAliveSender == null && isKeepAliveNecessary()) {
|
||||
@Override
|
||||
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
|
||||
final var keepAliveTime = System.currentTimeMillis();
|
||||
executor.execute(() -> lastKeepAliveReceived = keepAliveTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
|
||||
}
|
||||
|
||||
private void updateKeepAliveSenderStatus() {
|
||||
if (keepAliveSender == null && sendKeepAlives()) {
|
||||
keepAliveSender = new KeepAliveSender();
|
||||
keepAliveSender.start();
|
||||
} else if (keepAliveSender != null && !isKeepAliveNecessary()) {
|
||||
} else if (keepAliveSender != null && !sendKeepAlives()) {
|
||||
keepAliveSender.shutdown();
|
||||
keepAliveSender = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) {
|
||||
if (isIdentifiedWebSocket) {
|
||||
identified.lastKeepAliveReceived = System.currentTimeMillis();
|
||||
} else {
|
||||
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageError(int status, boolean isIdentifiedWebSocket) {
|
||||
if (status == 409) {
|
||||
HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified);
|
||||
if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) {
|
||||
logger.warn("Received too many mismatch device errors, forcing new websockets.");
|
||||
signalWebSocket.forceNewWebSockets();
|
||||
signalWebSocket.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isKeepAliveNecessary() {
|
||||
return identified.needsKeepAlive || unidentified.needsKeepAlive;
|
||||
}
|
||||
|
||||
private static class HealthState {
|
||||
|
||||
private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1));
|
||||
|
||||
private volatile boolean needsKeepAlive;
|
||||
private volatile long lastKeepAliveReceived;
|
||||
private boolean sendKeepAlives() {
|
||||
return needsKeepAlive && webSocket != null && webSocket.shouldSendKeepAlives();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If
|
||||
* either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated.
|
||||
* Sends periodic heartbeats/keep-alives over the WebSocket to prevent connection timeouts. If
|
||||
* the WebSocket fails to get a return heartbeat after [KEEP_ALIVE_TIMEOUT] seconds, it is forced to be recreated.
|
||||
*/
|
||||
private class KeepAliveSender extends Thread {
|
||||
private final class KeepAliveSender extends Thread {
|
||||
|
||||
private volatile boolean shouldKeepRunning = true;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
identified.lastKeepAliveReceived = System.currentTimeMillis();
|
||||
unidentified.lastKeepAliveReceived = System.currentTimeMillis();
|
||||
logger.debug("[KeepAliveSender({})] started", this.threadId());
|
||||
lastKeepAliveReceived = System.currentTimeMillis();
|
||||
|
||||
while (shouldKeepRunning && isKeepAliveNecessary()) {
|
||||
var keepAliveSendTime = System.currentTimeMillis();
|
||||
while (shouldKeepRunning && sendKeepAlives()) {
|
||||
try {
|
||||
sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE);
|
||||
final var nextKeepAliveSendTime = keepAliveSendTime + KEEP_ALIVE_SEND_CADENCE;
|
||||
sleepUntil(nextKeepAliveSendTime);
|
||||
|
||||
if (shouldKeepRunning && isKeepAliveNecessary()) {
|
||||
long keepAliveRequiredSinceTime = System.currentTimeMillis()
|
||||
- MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE;
|
||||
if (shouldKeepRunning && sendKeepAlives()) {
|
||||
keepAliveSendTime = System.currentTimeMillis();
|
||||
webSocket.sendKeepAlive();
|
||||
}
|
||||
|
||||
if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime
|
||||
|| unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) {
|
||||
logger.warn("Missed keep alives, identified last: "
|
||||
+ identified.lastKeepAliveReceived
|
||||
+ " unidentified last: "
|
||||
+ unidentified.lastKeepAliveReceived
|
||||
+ " needed by: "
|
||||
+ keepAliveRequiredSinceTime);
|
||||
signalWebSocket.forceNewWebSockets();
|
||||
signalWebSocket.connect();
|
||||
} else {
|
||||
signalWebSocket.sendKeepAlive();
|
||||
final var responseRequiredTime = keepAliveSendTime + KEEP_ALIVE_TIMEOUT;
|
||||
sleepUntil(responseRequiredTime);
|
||||
|
||||
if (shouldKeepRunning && sendKeepAlives()) {
|
||||
if (lastKeepAliveReceived < keepAliveSendTime) {
|
||||
logger.debug("Missed keep alive, last: {} needed by: {}",
|
||||
lastKeepAliveReceived,
|
||||
responseRequiredTime);
|
||||
webSocket.forceNewWebSocket();
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.warn("Error occurred in KeepAliveSender, ignoring ...", e);
|
||||
logger.warn("Keep alive sender failed", e);
|
||||
}
|
||||
}
|
||||
logger.debug("[KeepAliveSender({})] ended", threadId());
|
||||
}
|
||||
|
||||
void sleepUntil(long timeMillis) {
|
||||
while (System.currentTimeMillis() < timeMillis) {
|
||||
final var waitTime = timeMillis - System.currentTimeMillis();
|
||||
if (waitTime > 0) {
|
||||
try {
|
||||
sleepTimer.sleep(waitTime);
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("WebSocket health monitor interrupted", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
void shutdown() {
|
||||
shouldKeepRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HttpErrorTracker {
|
||||
|
||||
private final long[] timestamps;
|
||||
private final long errorTimeRange;
|
||||
|
||||
public HttpErrorTracker(int samples, long errorTimeRange) {
|
||||
this.timestamps = new long[samples];
|
||||
this.errorTimeRange = errorTimeRange;
|
||||
}
|
||||
|
||||
public synchronized boolean addSample(long now) {
|
||||
long errorsMustBeAfter = now - errorTimeRange;
|
||||
int count = 1;
|
||||
int minIndex = 0;
|
||||
|
||||
for (int i = 0; i < timestamps.length; i++) {
|
||||
if (timestamps[i] < errorsMustBeAfter) {
|
||||
timestamps[i] = 0;
|
||||
} else if (timestamps[i] != 0) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (timestamps[i] < timestamps[minIndex]) {
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
timestamps[minIndex] = now;
|
||||
|
||||
if (count >= timestamps.length) {
|
||||
Arrays.fill(timestamps, 0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,27 @@ import java.io.IOException;
|
|||
|
||||
public class SyncStorageJob implements Job {
|
||||
|
||||
private final boolean forcePush;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SyncStorageJob.class);
|
||||
|
||||
public SyncStorageJob() {
|
||||
this.forcePush = false;
|
||||
}
|
||||
|
||||
public SyncStorageJob(final boolean forcePush) {
|
||||
this.forcePush = forcePush;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(Context context) {
|
||||
logger.trace("Running storage sync job");
|
||||
try {
|
||||
context.getStorageHelper().syncDataWithStorage();
|
||||
if (forcePush) {
|
||||
context.getStorageHelper().forcePushToStorage();
|
||||
} else {
|
||||
context.getStorageHelper().syncDataWithStorage();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to sync storage data", e);
|
||||
}
|
||||
|
|
|
@ -611,7 +611,8 @@ public class AccountDatabase extends Database {
|
|||
}
|
||||
|
||||
private static void createUuidMappingTable(
|
||||
final Connection connection, final Statement statement
|
||||
final Connection connection,
|
||||
final Statement statement
|
||||
) throws SQLException {
|
||||
statement.executeUpdate("""
|
||||
CREATE TABLE tmp_mapping_table (
|
||||
|
|
|
@ -22,7 +22,8 @@ public class AttachmentStore {
|
|||
}
|
||||
|
||||
public void storeAttachmentPreview(
|
||||
final SignalServiceAttachmentPointer pointer, final AttachmentStorer storer
|
||||
final SignalServiceAttachmentPointer pointer,
|
||||
final AttachmentStorer storer
|
||||
) throws IOException {
|
||||
storeAttachment(getAttachmentPreviewFile(pointer.getRemoteId(),
|
||||
pointer.getFileName(),
|
||||
|
@ -30,7 +31,8 @@ public class AttachmentStore {
|
|||
}
|
||||
|
||||
public void storeAttachment(
|
||||
final SignalServiceAttachmentPointer pointer, final AttachmentStorer storer
|
||||
final SignalServiceAttachmentPointer pointer,
|
||||
final AttachmentStorer storer
|
||||
) throws IOException {
|
||||
storeAttachment(getAttachmentFile(pointer), storer);
|
||||
}
|
||||
|
@ -54,22 +56,24 @@ public class AttachmentStore {
|
|||
}
|
||||
|
||||
private File getAttachmentPreviewFile(
|
||||
SignalServiceAttachmentRemoteId attachmentId, Optional<String> filename, Optional<String> contentType
|
||||
SignalServiceAttachmentRemoteId attachmentId,
|
||||
Optional<String> filename,
|
||||
Optional<String> contentType
|
||||
) {
|
||||
final var extension = getAttachmentExtension(filename, contentType);
|
||||
return new File(attachmentsPath, attachmentId.toString() + extension + ".preview");
|
||||
}
|
||||
|
||||
private File getAttachmentFile(
|
||||
SignalServiceAttachmentRemoteId attachmentId, Optional<String> filename, Optional<String> contentType
|
||||
SignalServiceAttachmentRemoteId attachmentId,
|
||||
Optional<String> filename,
|
||||
Optional<String> contentType
|
||||
) {
|
||||
final var extension = getAttachmentExtension(filename, contentType);
|
||||
return new File(attachmentsPath, attachmentId.toString() + extension);
|
||||
}
|
||||
|
||||
private static String getAttachmentExtension(
|
||||
final Optional<String> filename, final Optional<String> contentType
|
||||
) {
|
||||
private static String getAttachmentExtension(final Optional<String> filename, final Optional<String> contentType) {
|
||||
return filename.filter(f -> f.contains("."))
|
||||
.map(f -> f.substring(f.lastIndexOf(".") + 1))
|
||||
.or(() -> contentType.flatMap(MimeUtils::guessExtensionFromMimeType))
|
||||
|
|
|
@ -24,7 +24,8 @@ public abstract class Database implements AutoCloseable {
|
|||
}
|
||||
|
||||
public static <T extends Database> T initDatabase(
|
||||
File databaseFile, Function<HikariDataSource, T> newDatabase
|
||||
File databaseFile,
|
||||
Function<HikariDataSource, T> newDatabase
|
||||
) throws SQLException {
|
||||
HikariDataSource dataSource = null;
|
||||
|
||||
|
@ -94,10 +95,12 @@ public abstract class Database implements AutoCloseable {
|
|||
sqliteConfig.setTransactionMode(SQLiteConfig.TransactionMode.IMMEDIATE);
|
||||
|
||||
HikariConfig config = new HikariConfig();
|
||||
config.setJdbcUrl("jdbc:sqlite:" + databaseFile);
|
||||
config.setJdbcUrl("jdbc:sqlite:" + databaseFile + "?foreign_keys=ON&journal_mode=wal");
|
||||
config.setDataSourceProperties(sqliteConfig.toProperties());
|
||||
config.setMinimumIdle(1);
|
||||
config.setConnectionInitSql("PRAGMA foreign_keys=ON");
|
||||
config.setConnectionTimeout(90_000);
|
||||
config.setMaximumPoolSize(50);
|
||||
config.setMaxLifetime(0);
|
||||
return new HikariDataSource(config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,10 +65,12 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
|
|||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes;
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection;
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
@ -114,7 +116,7 @@ public class SignalAccount implements Closeable {
|
|||
private static final Logger logger = LoggerFactory.getLogger(SignalAccount.class);
|
||||
|
||||
private static final int MINIMUM_STORAGE_VERSION = 1;
|
||||
private static final int CURRENT_STORAGE_VERSION = 9;
|
||||
private static final int CURRENT_STORAGE_VERSION = 10;
|
||||
|
||||
private final Object LOCK = new Object();
|
||||
|
||||
|
@ -138,6 +140,8 @@ public class SignalAccount implements Closeable {
|
|||
private String registrationLockPin;
|
||||
private MasterKey pinMasterKey;
|
||||
private StorageKey storageKey;
|
||||
private AccountEntropyPool accountEntropyPool;
|
||||
private MediaRootBackupKey mediaRootBackupKey;
|
||||
private ProfileKey profileKey;
|
||||
|
||||
private Settings settings;
|
||||
|
@ -189,7 +193,10 @@ public class SignalAccount implements Closeable {
|
|||
}
|
||||
|
||||
public static SignalAccount load(
|
||||
File dataPath, String accountPath, boolean waitForLock, final Settings settings
|
||||
File dataPath,
|
||||
String accountPath,
|
||||
boolean waitForLock,
|
||||
final Settings settings
|
||||
) throws IOException {
|
||||
logger.trace("Opening account file");
|
||||
final var fileName = getFileName(dataPath, accountPath);
|
||||
|
@ -285,7 +292,9 @@ public class SignalAccount implements Closeable {
|
|||
final IdentityKeyPair aciIdentity,
|
||||
final IdentityKeyPair pniIdentity,
|
||||
final ProfileKey profileKey,
|
||||
final MasterKey masterKey
|
||||
final MasterKey masterKey,
|
||||
final AccountEntropyPool accountEntropyPool,
|
||||
final MediaRootBackupKey mediaRootBackupKey
|
||||
) {
|
||||
this.deviceId = 0;
|
||||
this.number = number;
|
||||
|
@ -301,7 +310,14 @@ public class SignalAccount implements Closeable {
|
|||
this.registered = false;
|
||||
this.isMultiDevice = true;
|
||||
setLastReceiveTimestamp(0L);
|
||||
this.pinMasterKey = masterKey;
|
||||
if (accountEntropyPool != null) {
|
||||
this.pinMasterKey = null;
|
||||
this.accountEntropyPool = accountEntropyPool;
|
||||
} else {
|
||||
this.pinMasterKey = masterKey;
|
||||
this.accountEntropyPool = null;
|
||||
}
|
||||
this.mediaRootBackupKey = mediaRootBackupKey;
|
||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||
this.setStorageManifest(null);
|
||||
this.storageKey = null;
|
||||
|
@ -316,7 +332,9 @@ public class SignalAccount implements Closeable {
|
|||
}
|
||||
|
||||
public void finishLinking(
|
||||
final int deviceId, final PreKeyCollection aciPreKeys, final PreKeyCollection pniPreKeys
|
||||
final int deviceId,
|
||||
final PreKeyCollection aciPreKeys,
|
||||
final PreKeyCollection pniPreKeys
|
||||
) {
|
||||
this.registered = true;
|
||||
this.deviceId = deviceId;
|
||||
|
@ -334,6 +352,7 @@ public class SignalAccount implements Closeable {
|
|||
final PreKeyCollection pniPreKeys
|
||||
) {
|
||||
this.pinMasterKey = masterKey;
|
||||
this.accountEntropyPool = null;
|
||||
getKeyValueStore().storeEntry(storageManifestVersion, -1L);
|
||||
this.setStorageManifest(null);
|
||||
this.storageKey = null;
|
||||
|
@ -375,7 +394,9 @@ public class SignalAccount implements Closeable {
|
|||
}
|
||||
|
||||
private void mergeRecipients(
|
||||
final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
|
||||
final Connection connection,
|
||||
RecipientId recipientId,
|
||||
RecipientId toBeMergedRecipientId
|
||||
) throws SQLException {
|
||||
getMessageCache().mergeRecipients(recipientId, toBeMergedRecipientId);
|
||||
getGroupStore().mergeRecipients(connection, recipientId, toBeMergedRecipientId);
|
||||
|
@ -438,9 +459,7 @@ public class SignalAccount implements Closeable {
|
|||
return f.exists() && !f.isDirectory() && f.length() > 0L;
|
||||
}
|
||||
|
||||
private void load(
|
||||
File dataPath, String accountPath, final Settings settings
|
||||
) throws IOException {
|
||||
private void load(File dataPath, String accountPath, final Settings settings) throws IOException {
|
||||
logger.trace("Loading account file {}", accountPath);
|
||||
this.dataPath = dataPath;
|
||||
this.accountPath = accountPath;
|
||||
|
@ -494,6 +513,12 @@ public class SignalAccount implements Closeable {
|
|||
if (storage.storageKey != null) {
|
||||
storageKey = new StorageKey(base64.decode(storage.storageKey));
|
||||
}
|
||||
if (storage.accountEntropyPool != null) {
|
||||
accountEntropyPool = new AccountEntropyPool(storage.accountEntropyPool);
|
||||
}
|
||||
if (storage.mediaRootBackupKey != null) {
|
||||
mediaRootBackupKey = new MediaRootBackupKey(base64.decode(storage.mediaRootBackupKey));
|
||||
}
|
||||
if (storage.profileKey != null) {
|
||||
try {
|
||||
profileKey = new ProfileKey(base64.decode(storage.profileKey));
|
||||
|
@ -786,7 +811,8 @@ public class SignalAccount implements Closeable {
|
|||
}
|
||||
|
||||
private void loadLegacyStores(
|
||||
final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore
|
||||
final JsonNode rootNode,
|
||||
final LegacyJsonSignalProtocolStore legacySignalProtocolStore
|
||||
) {
|
||||
var legacyRecipientStoreNode = rootNode.get("recipientStore");
|
||||
if (legacyRecipientStoreNode != null) {
|
||||
|
@ -801,6 +827,7 @@ public class SignalAccount implements Closeable {
|
|||
|
||||
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
|
||||
logger.debug("Migrating legacy pre key store.");
|
||||
aciAccountData.getPreKeyStore().removeAllPreKeys();
|
||||
for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
|
||||
try {
|
||||
aciAccountData.getPreKeyStore().storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
|
||||
|
@ -812,6 +839,7 @@ public class SignalAccount implements Closeable {
|
|||
|
||||
if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
|
||||
logger.debug("Migrating legacy signed pre key store.");
|
||||
aciAccountData.getSignedPreKeyStore().removeAllSignedPreKeys();
|
||||
for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
|
||||
try {
|
||||
aciAccountData.getSignedPreKeyStore()
|
||||
|
@ -975,6 +1003,8 @@ public class SignalAccount implements Closeable {
|
|||
registrationLockPin,
|
||||
pinMasterKey == null ? null : base64.encodeToString(pinMasterKey.serialize()),
|
||||
storageKey == null ? null : base64.encodeToString(storageKey.serialize()),
|
||||
accountEntropyPool == null ? null : accountEntropyPool.getValue(),
|
||||
mediaRootBackupKey == null ? null : base64.encodeToString(mediaRootBackupKey.getValue()),
|
||||
profileKey == null ? null : base64.encodeToString(profileKey.serialize()),
|
||||
usernameLink == null ? null : base64.encodeToString(usernameLink.getEntropy()),
|
||||
usernameLink == null ? null : usernameLink.getServerId().toString());
|
||||
|
@ -1436,6 +1466,10 @@ public class SignalAccount implements Closeable {
|
|||
return selfRecipientId;
|
||||
}
|
||||
|
||||
public Profile getSelfRecipientProfile() {
|
||||
return recipientStore.getProfile(selfRecipientId);
|
||||
}
|
||||
|
||||
public String getSessionId(final String forNumber) {
|
||||
final var keyValueStore = getKeyValueStore();
|
||||
final var sessionNumber = keyValueStore.getEntry(verificationSessionNumber);
|
||||
|
@ -1506,16 +1540,28 @@ public class SignalAccount implements Closeable {
|
|||
public MasterKey getPinBackedMasterKey() {
|
||||
if (registrationLockPin == null) {
|
||||
return null;
|
||||
} else if (!isPrimaryDevice()) {
|
||||
return getMasterKey();
|
||||
}
|
||||
return pinMasterKey;
|
||||
return getOrCreatePinMasterKey();
|
||||
}
|
||||
|
||||
public MasterKey getOrCreatePinMasterKey() {
|
||||
if (pinMasterKey == null) {
|
||||
pinMasterKey = KeyUtils.createMasterKey();
|
||||
save();
|
||||
final var key = getMasterKey();
|
||||
if (key != null) {
|
||||
return key;
|
||||
}
|
||||
return pinMasterKey;
|
||||
|
||||
return getOrCreateAccountEntropyPool().deriveMasterKey();
|
||||
}
|
||||
|
||||
private MasterKey getMasterKey() {
|
||||
if (pinMasterKey != null) {
|
||||
return pinMasterKey;
|
||||
} else if (accountEntropyPool != null) {
|
||||
return accountEntropyPool.deriveMasterKey();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setMasterKey(MasterKey masterKey) {
|
||||
|
@ -1523,14 +1569,19 @@ public class SignalAccount implements Closeable {
|
|||
return;
|
||||
}
|
||||
this.pinMasterKey = masterKey;
|
||||
if (masterKey != null) {
|
||||
this.storageKey = null;
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
public StorageKey getOrCreateStorageKey() {
|
||||
if (pinMasterKey != null) {
|
||||
return pinMasterKey.deriveStorageServiceKey();
|
||||
} else if (storageKey != null) {
|
||||
if (storageKey != null) {
|
||||
return storageKey;
|
||||
} else if (pinMasterKey != null) {
|
||||
return pinMasterKey.deriveStorageServiceKey();
|
||||
} else if (accountEntropyPool != null) {
|
||||
return accountEntropyPool.deriveMasterKey().deriveStorageServiceKey();
|
||||
} else if (!isPrimaryDevice() || !isMultiDevice()) {
|
||||
// Only upload storage, if a pin master key already exists or linked devices exist
|
||||
return null;
|
||||
|
@ -1547,6 +1598,40 @@ public class SignalAccount implements Closeable {
|
|||
save();
|
||||
}
|
||||
|
||||
public AccountEntropyPool getOrCreateAccountEntropyPool() {
|
||||
if (accountEntropyPool == null) {
|
||||
accountEntropyPool = AccountEntropyPool.Companion.generate();
|
||||
save();
|
||||
}
|
||||
return accountEntropyPool;
|
||||
}
|
||||
|
||||
public void setAccountEntropyPool(final AccountEntropyPool accountEntropyPool) {
|
||||
this.accountEntropyPool = accountEntropyPool;
|
||||
if (accountEntropyPool != null) {
|
||||
this.storageKey = null;
|
||||
this.pinMasterKey = null;
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
public boolean needsStorageKeyMigration() {
|
||||
return isPrimaryDevice() && (storageKey != null || pinMasterKey != null);
|
||||
}
|
||||
|
||||
public MediaRootBackupKey getOrCreateMediaRootBackupKey() {
|
||||
if (mediaRootBackupKey == null) {
|
||||
mediaRootBackupKey = KeyUtils.createMediaRootBackupKey();
|
||||
save();
|
||||
}
|
||||
return mediaRootBackupKey;
|
||||
}
|
||||
|
||||
public void setMediaRootBackupKey(final MediaRootBackupKey mediaRootBackupKey) {
|
||||
this.mediaRootBackupKey = mediaRootBackupKey;
|
||||
save();
|
||||
}
|
||||
|
||||
public String getRecoveryPassword() {
|
||||
final var masterKey = getPinBackedMasterKey();
|
||||
if (masterKey == null) {
|
||||
|
@ -1569,7 +1654,7 @@ public class SignalAccount implements Closeable {
|
|||
return Optional.empty();
|
||||
}
|
||||
try (var inputStream = new FileInputStream(storageManifestFile)) {
|
||||
return Optional.of(SignalStorageManifest.deserialize(inputStream.readAllBytes()));
|
||||
return Optional.of(SignalStorageManifest.Companion.deserialize(inputStream.readAllBytes()));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Failed to read local storage manifest.", e);
|
||||
return Optional.empty();
|
||||
|
@ -1876,6 +1961,8 @@ public class SignalAccount implements Closeable {
|
|||
String registrationLockPin,
|
||||
String pinMasterKey,
|
||||
String storageKey,
|
||||
String accountEntropyPool,
|
||||
String mediaRootBackupKey,
|
||||
String profileKey,
|
||||
String usernameLinkEntropy,
|
||||
String usernameLinkServerId
|
||||
|
|
|
@ -41,9 +41,7 @@ public class UnknownStorageIdStore {
|
|||
}
|
||||
}
|
||||
|
||||
public List<StorageId> getUnknownStorageIds(
|
||||
Connection connection, Collection<Integer> types
|
||||
) throws SQLException {
|
||||
public List<StorageId> getUnknownStorageIds(Connection connection, Collection<Integer> types) throws SQLException {
|
||||
final var typesCommaSeparated = types.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||
final var sql = (
|
||||
"""
|
||||
|
|
|
@ -72,7 +72,8 @@ public class Utils {
|
|||
}
|
||||
|
||||
public static <T> T executeQuerySingleRow(
|
||||
PreparedStatement statement, ResultSetMapper<T> mapper
|
||||
PreparedStatement statement,
|
||||
ResultSetMapper<T> mapper
|
||||
) throws SQLException {
|
||||
final var resultSet = statement.executeQuery();
|
||||
if (!resultSet.next()) {
|
||||
|
@ -82,7 +83,8 @@ public class Utils {
|
|||
}
|
||||
|
||||
public static <T> Optional<T> executeQueryForOptional(
|
||||
PreparedStatement statement, ResultSetMapper<T> mapper
|
||||
PreparedStatement statement,
|
||||
ResultSetMapper<T> mapper
|
||||
) throws SQLException {
|
||||
final var resultSet = statement.executeQuery();
|
||||
if (!resultSet.next()) {
|
||||
|
@ -92,7 +94,8 @@ public class Utils {
|
|||
}
|
||||
|
||||
public static <T> Stream<T> executeQueryForStream(
|
||||
PreparedStatement statement, ResultSetMapper<T> mapper
|
||||
PreparedStatement statement,
|
||||
ResultSetMapper<T> mapper
|
||||
) throws SQLException {
|
||||
final var resultSet = statement.executeQuery();
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package org.asamk.signal.manager.storage.accounts;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||
|
@ -10,7 +11,6 @@ import org.asamk.signal.manager.util.IOUtils;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -41,7 +41,9 @@ public class AccountsStore {
|
|||
private final AccountLoader accountLoader;
|
||||
|
||||
public AccountsStore(
|
||||
final File dataPath, final ServiceEnvironment serviceEnvironment, final AccountLoader accountLoader
|
||||
final File dataPath,
|
||||
final ServiceEnvironment serviceEnvironment,
|
||||
final AccountLoader accountLoader
|
||||
) throws IOException {
|
||||
this.dataPath = dataPath;
|
||||
this.serviceEnvironment = getServiceEnvironmentString(serviceEnvironment);
|
||||
|
@ -179,7 +181,7 @@ public class AccountsStore {
|
|||
return Arrays.stream(files)
|
||||
.filter(File::isFile)
|
||||
.map(File::getName)
|
||||
.filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
|
||||
.filter(file -> PhoneNumberUtil.getInstance().isPossibleNumber(file, null))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
@ -202,7 +204,9 @@ public class AccountsStore {
|
|||
}
|
||||
|
||||
private AccountsStorage upgradeAccountsFile(
|
||||
final FileChannel fileChannel, final AccountsStorage storage, final int accountsVersion
|
||||
final FileChannel fileChannel,
|
||||
final AccountsStorage storage,
|
||||
final int accountsVersion
|
||||
) {
|
||||
try {
|
||||
List<AccountsStorage.Account> newAccounts = storage.accounts();
|
||||
|
|
|
@ -36,6 +36,10 @@ public class ConfigurationStore {
|
|||
return keyValueStore.getEntry(readReceipts);
|
||||
}
|
||||
|
||||
public Boolean getReadReceipts(final Connection connection) throws SQLException {
|
||||
return keyValueStore.getEntry(connection, readReceipts);
|
||||
}
|
||||
|
||||
public void setReadReceipts(final boolean value) {
|
||||
if (keyValueStore.storeEntry(readReceipts, value)) {
|
||||
recipientStore.rotateSelfStorageId();
|
||||
|
@ -52,6 +56,10 @@ public class ConfigurationStore {
|
|||
return keyValueStore.getEntry(unidentifiedDeliveryIndicators);
|
||||
}
|
||||
|
||||
public Boolean getUnidentifiedDeliveryIndicators(final Connection connection) throws SQLException {
|
||||
return keyValueStore.getEntry(connection, unidentifiedDeliveryIndicators);
|
||||
}
|
||||
|
||||
public void setUnidentifiedDeliveryIndicators(final boolean value) {
|
||||
if (keyValueStore.storeEntry(unidentifiedDeliveryIndicators, value)) {
|
||||
recipientStore.rotateSelfStorageId();
|
||||
|
@ -59,7 +67,8 @@ public class ConfigurationStore {
|
|||
}
|
||||
|
||||
public void setUnidentifiedDeliveryIndicators(
|
||||
final Connection connection, final boolean value
|
||||
final Connection connection,
|
||||
final boolean value
|
||||
) throws SQLException {
|
||||
if (keyValueStore.storeEntry(connection, unidentifiedDeliveryIndicators, value)) {
|
||||
recipientStore.rotateSelfStorageId(connection);
|
||||
|
@ -70,6 +79,10 @@ public class ConfigurationStore {
|
|||
return keyValueStore.getEntry(typingIndicators);
|
||||
}
|
||||
|
||||
public Boolean getTypingIndicators(final Connection connection) throws SQLException {
|
||||
return keyValueStore.getEntry(connection, typingIndicators);
|
||||
}
|
||||
|
||||
public void setTypingIndicators(final boolean value) {
|
||||
if (keyValueStore.storeEntry(typingIndicators, value)) {
|
||||
recipientStore.rotateSelfStorageId();
|
||||
|
@ -86,6 +99,10 @@ public class ConfigurationStore {
|
|||
return keyValueStore.getEntry(linkPreviews);
|
||||
}
|
||||
|
||||
public Boolean getLinkPreviews(final Connection connection) throws SQLException {
|
||||
return keyValueStore.getEntry(connection, linkPreviews);
|
||||
}
|
||||
|
||||
public void setLinkPreviews(final boolean value) {
|
||||
if (keyValueStore.storeEntry(linkPreviews, value)) {
|
||||
recipientStore.rotateSelfStorageId();
|
||||
|
@ -102,6 +119,10 @@ public class ConfigurationStore {
|
|||
return keyValueStore.getEntry(phoneNumberUnlisted);
|
||||
}
|
||||
|
||||
public Boolean getPhoneNumberUnlisted(final Connection connection) throws SQLException {
|
||||
return keyValueStore.getEntry(connection, phoneNumberUnlisted);
|
||||
}
|
||||
|
||||
public void setPhoneNumberUnlisted(final boolean value) {
|
||||
if (keyValueStore.storeEntry(phoneNumberUnlisted, value)) {
|
||||
recipientStore.rotateSelfStorageId();
|
||||
|
@ -118,6 +139,10 @@ public class ConfigurationStore {
|
|||
return keyValueStore.getEntry(phoneNumberSharingMode);
|
||||
}
|
||||
|
||||
public PhoneNumberSharingMode getPhoneNumberSharingMode(final Connection connection) throws SQLException {
|
||||
return keyValueStore.getEntry(connection, phoneNumberSharingMode);
|
||||
}
|
||||
|
||||
public void setPhoneNumberSharingMode(final PhoneNumberSharingMode value) {
|
||||
if (keyValueStore.storeEntry(phoneNumberSharingMode, value)) {
|
||||
recipientStore.rotateSelfStorageId();
|
||||
|
@ -125,7 +150,8 @@ public class ConfigurationStore {
|
|||
}
|
||||
|
||||
public void setPhoneNumberSharingMode(
|
||||
final Connection connection, final PhoneNumberSharingMode value
|
||||
final Connection connection,
|
||||
final PhoneNumberSharingMode value
|
||||
) throws SQLException {
|
||||
if (keyValueStore.storeEntry(connection, phoneNumberSharingMode, value)) {
|
||||
recipientStore.rotateSelfStorageId(connection);
|
||||
|
@ -136,6 +162,10 @@ public class ConfigurationStore {
|
|||
return keyValueStore.getEntry(usernameLinkColor);
|
||||
}
|
||||
|
||||
public String getUsernameLinkColor(final Connection connection) throws SQLException {
|
||||
return keyValueStore.getEntry(connection, usernameLinkColor);
|
||||
}
|
||||
|
||||
public void setUsernameLinkColor(final String color) {
|
||||
if (keyValueStore.storeEntry(usernameLinkColor, color)) {
|
||||
recipientStore.rotateSelfStorageId();
|
||||
|
|
|
@ -31,7 +31,9 @@ public final class GroupInfoV2 extends GroupInfo {
|
|||
private final RecipientResolver recipientResolver;
|
||||
|
||||
public GroupInfoV2(
|
||||
final GroupIdV2 groupId, final GroupMasterKey masterKey, final RecipientResolver recipientResolver
|
||||
final GroupIdV2 groupId,
|
||||
final GroupMasterKey masterKey,
|
||||
final RecipientResolver recipientResolver
|
||||
) {
|
||||
this.groupId = groupId;
|
||||
this.masterKey = masterKey;
|
||||
|
|
|
@ -121,7 +121,10 @@ public class GroupStore {
|
|||
}
|
||||
|
||||
public void storeStorageRecord(
|
||||
final Connection connection, final GroupId groupId, final StorageId storageId, final byte[] storageRecord
|
||||
final Connection connection,
|
||||
final GroupId groupId,
|
||||
final StorageId storageId,
|
||||
final byte[] storageRecord
|
||||
) throws SQLException {
|
||||
final var groupTable = groupId instanceof GroupIdV1 ? TABLE_GROUP_V1 : TABLE_GROUP_V2;
|
||||
final var deleteSql = (
|
||||
|
@ -250,7 +253,8 @@ public class GroupStore {
|
|||
}
|
||||
|
||||
public GroupInfoV2 getGroupOrPartialMigrate(
|
||||
Connection connection, final GroupMasterKey groupMasterKey
|
||||
Connection connection,
|
||||
final GroupMasterKey groupMasterKey
|
||||
) throws SQLException {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
|
||||
final var groupId = GroupUtils.getGroupIdV2(groupSecretParams);
|
||||
|
@ -258,9 +262,7 @@ public class GroupStore {
|
|||
return getGroupOrPartialMigrate(connection, groupMasterKey, groupId);
|
||||
}
|
||||
|
||||
public GroupInfoV2 getGroupOrPartialMigrate(
|
||||
final GroupMasterKey groupMasterKey, final GroupIdV2 groupId
|
||||
) {
|
||||
public GroupInfoV2 getGroupOrPartialMigrate(final GroupMasterKey groupMasterKey, final GroupIdV2 groupId) {
|
||||
try (final var connection = database.getConnection()) {
|
||||
return getGroupOrPartialMigrate(connection, groupMasterKey, groupId);
|
||||
} catch (SQLException e) {
|
||||
|
@ -269,7 +271,9 @@ public class GroupStore {
|
|||
}
|
||||
|
||||
private GroupInfoV2 getGroupOrPartialMigrate(
|
||||
Connection connection, final GroupMasterKey groupMasterKey, final GroupIdV2 groupId
|
||||
Connection connection,
|
||||
final GroupMasterKey groupMasterKey,
|
||||
final GroupIdV2 groupId
|
||||
) throws SQLException {
|
||||
switch (getGroup(connection, (GroupId) groupId)) {
|
||||
case GroupInfoV1 groupInfoV1 -> {
|
||||
|
@ -325,7 +329,9 @@ public class GroupStore {
|
|||
}
|
||||
|
||||
public void mergeRecipients(
|
||||
final Connection connection, final RecipientId recipientId, final RecipientId toBeMergedRecipientId
|
||||
final Connection connection,
|
||||
final RecipientId recipientId,
|
||||
final RecipientId toBeMergedRecipientId
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -360,7 +366,9 @@ public class GroupStore {
|
|||
}
|
||||
|
||||
public void updateStorageIds(
|
||||
Connection connection, Map<GroupIdV1, StorageId> storageIdV1Map, Map<GroupIdV2, StorageId> storageIdV2Map
|
||||
Connection connection,
|
||||
Map<GroupIdV1, StorageId> storageIdV1Map,
|
||||
Map<GroupIdV2, StorageId> storageIdV2Map
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -385,9 +393,7 @@ public class GroupStore {
|
|||
}
|
||||
}
|
||||
|
||||
public void updateStorageId(
|
||||
Connection connection, GroupId groupId, StorageId storageId
|
||||
) throws SQLException {
|
||||
public void updateStorageId(Connection connection, GroupId groupId, StorageId storageId) throws SQLException {
|
||||
final var sqlV1 = (
|
||||
"""
|
||||
UPDATE %s
|
||||
|
@ -460,7 +466,9 @@ public class GroupStore {
|
|||
}
|
||||
|
||||
private void insertOrReplaceGroup(
|
||||
final Connection connection, Long internalId, final GroupInfo group
|
||||
final Connection connection,
|
||||
Long internalId,
|
||||
final GroupInfo group
|
||||
) throws SQLException {
|
||||
if (group instanceof GroupInfoV1 groupV1) {
|
||||
if (internalId != null) {
|
||||
|
|
|
@ -151,7 +151,8 @@ public class LegacyGroupStore {
|
|||
|
||||
@Override
|
||||
public List<Member> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
var addresses = new ArrayList<Member>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
@ -184,7 +185,8 @@ public class LegacyGroupStore {
|
|||
|
||||
@Override
|
||||
public List<Object> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
var groups = new ArrayList<>();
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
|
|
@ -11,9 +11,7 @@ public class IdentityInfo {
|
|||
private final TrustLevel trustLevel;
|
||||
private final long addedTimestamp;
|
||||
|
||||
IdentityInfo(
|
||||
final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp
|
||||
) {
|
||||
IdentityInfo(final String address, IdentityKey identityKey, TrustLevel trustLevel, long addedTimestamp) {
|
||||
this.address = address;
|
||||
this.identityKey = identityKey;
|
||||
this.trustLevel = trustLevel;
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientStore;
|
|||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
@ -49,7 +50,9 @@ public class IdentityKeyStore {
|
|||
}
|
||||
|
||||
public IdentityKeyStore(
|
||||
final Database database, final TrustNewIdentity trustNewIdentity, RecipientStore recipientStore
|
||||
final Database database,
|
||||
final TrustNewIdentity trustNewIdentity,
|
||||
RecipientStore recipientStore
|
||||
) {
|
||||
this.database = database;
|
||||
this.trustNewIdentity = trustNewIdentity;
|
||||
|
@ -60,19 +63,21 @@ public class IdentityKeyStore {
|
|||
return identityChanges;
|
||||
}
|
||||
|
||||
public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
|
||||
public IdentityChange saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
|
||||
return saveIdentity(serviceId.toString(), identityKey);
|
||||
}
|
||||
|
||||
public boolean saveIdentity(
|
||||
final Connection connection, final ServiceId serviceId, final IdentityKey identityKey
|
||||
public IdentityChange saveIdentity(
|
||||
final Connection connection,
|
||||
final ServiceId serviceId,
|
||||
final IdentityKey identityKey
|
||||
) throws SQLException {
|
||||
return saveIdentity(connection, serviceId.toString(), identityKey);
|
||||
}
|
||||
|
||||
boolean saveIdentity(final String address, final IdentityKey identityKey) {
|
||||
IdentityChange saveIdentity(final String address, final IdentityKey identityKey) {
|
||||
if (isRetryingDecryption) {
|
||||
return false;
|
||||
return IdentityChange.NEW_OR_UNCHANGED;
|
||||
}
|
||||
try (final var connection = database.getConnection()) {
|
||||
return saveIdentity(connection, address, identityKey);
|
||||
|
@ -81,18 +86,24 @@ public class IdentityKeyStore {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean saveIdentity(
|
||||
final Connection connection, final String address, final IdentityKey identityKey
|
||||
private IdentityChange saveIdentity(
|
||||
final Connection connection,
|
||||
final String address,
|
||||
final IdentityKey identityKey
|
||||
) throws SQLException {
|
||||
final var identityInfo = loadIdentity(connection, address);
|
||||
if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) {
|
||||
if (identityInfo == null) {
|
||||
saveNewIdentity(connection, address, identityKey, true);
|
||||
return IdentityChange.NEW_OR_UNCHANGED;
|
||||
}
|
||||
if (identityInfo.getIdentityKey().equals(identityKey)) {
|
||||
// Identity already exists, not updating the trust level
|
||||
logger.trace("Not storing new identity for recipient {}, identity already stored", address);
|
||||
return false;
|
||||
return IdentityChange.NEW_OR_UNCHANGED;
|
||||
}
|
||||
|
||||
saveNewIdentity(connection, address, identityKey, identityInfo == null);
|
||||
return true;
|
||||
saveNewIdentity(connection, address, identityKey, false);
|
||||
return IdentityChange.REPLACED_EXISTING;
|
||||
}
|
||||
|
||||
public void setRetryingDecryption(final boolean retryingDecryption) {
|
||||
|
@ -230,9 +241,7 @@ public class IdentityKeyStore {
|
|||
logger.debug("Complete identities migration took {}ms", (System.nanoTime() - start) / 1000000);
|
||||
}
|
||||
|
||||
private IdentityInfo loadIdentity(
|
||||
final Connection connection, final String address
|
||||
) throws SQLException {
|
||||
private IdentityInfo loadIdentity(final Connection connection, final String address) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
|
||||
|
|
|
@ -41,7 +41,9 @@ public class LegacyIdentityKeyStore {
|
|||
static final Pattern identityFileNamePattern = Pattern.compile("(\\d+)");
|
||||
|
||||
private static List<IdentityInfo> getIdentities(
|
||||
final File identitiesPath, final RecipientResolver resolver, final RecipientAddressResolver addressResolver
|
||||
final File identitiesPath,
|
||||
final RecipientResolver resolver,
|
||||
final RecipientAddressResolver addressResolver
|
||||
) {
|
||||
final var files = identitiesPath.listFiles();
|
||||
if (files == null) {
|
||||
|
@ -66,7 +68,9 @@ public class LegacyIdentityKeyStore {
|
|||
}
|
||||
|
||||
private static IdentityInfo loadIdentityLocked(
|
||||
final RecipientId recipientId, RecipientAddressResolver addressResolver, final File identitiesPath
|
||||
final RecipientId recipientId,
|
||||
RecipientAddressResolver addressResolver,
|
||||
final File identitiesPath
|
||||
) {
|
||||
final var file = getIdentityFile(recipientId, identitiesPath);
|
||||
if (!file.exists()) {
|
||||
|
|
|
@ -33,7 +33,7 @@ public class SignalIdentityKeyStore implements org.signal.libsignal.protocol.sta
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return identityKeyStore.saveIdentity(address.getName(), identityKey);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import java.sql.PreparedStatement;
|
|||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
public class KeyValueStore {
|
||||
|
@ -18,6 +19,7 @@ public class KeyValueStore {
|
|||
private static final Logger logger = LoggerFactory.getLogger(KeyValueStore.class);
|
||||
|
||||
private final Database database;
|
||||
private final HashMap<KeyValueEntry<?>, Object> cache = new HashMap<>();
|
||||
|
||||
public static void createSql(Connection connection) throws SQLException {
|
||||
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
|
||||
|
@ -36,11 +38,18 @@ public class KeyValueStore {
|
|||
this.database = database;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getEntry(KeyValueEntry<T> key) {
|
||||
synchronized (cache) {
|
||||
if (cache.containsKey(key)) {
|
||||
logger.trace("Got entry for key {} from cache", key.key());
|
||||
return (T) cache.get(key);
|
||||
}
|
||||
}
|
||||
try (final var connection = database.getConnection()) {
|
||||
return getEntry(connection, key);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed read from pre_key store", e);
|
||||
throw new RuntimeException("Failed read from key_value store", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,7 +61,7 @@ public class KeyValueStore {
|
|||
}
|
||||
}
|
||||
|
||||
private <T> T getEntry(final Connection connection, final KeyValueEntry<T> key) throws SQLException {
|
||||
public <T> T getEntry(final Connection connection, final KeyValueEntry<T> key) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT key, value
|
||||
|
@ -63,20 +72,28 @@ public class KeyValueStore {
|
|||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setString(1, key.key());
|
||||
|
||||
final var result = Utils.executeQueryForOptional(statement,
|
||||
resultSet -> readValueFromResultSet(key, resultSet)).orElse(null);
|
||||
var result = Utils.executeQueryForOptional(statement, resultSet -> readValueFromResultSet(key, resultSet))
|
||||
.orElse(null);
|
||||
|
||||
if (result == null) {
|
||||
return key.defaultValue();
|
||||
logger.trace("Got entry for key {} from default value", key.key());
|
||||
result = key.defaultValue();
|
||||
} else {
|
||||
logger.trace("Got entry for key {} from db", key.key());
|
||||
}
|
||||
synchronized (cache) {
|
||||
cache.put(key, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public <T> boolean storeEntry(
|
||||
final Connection connection, final KeyValueEntry<T> key, final T value
|
||||
final Connection connection,
|
||||
final KeyValueEntry<T> key,
|
||||
final T value
|
||||
) throws SQLException {
|
||||
final var entry = getEntry(key);
|
||||
final var entry = getEntry(connection, key);
|
||||
if (Objects.equals(entry, value)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -93,12 +110,16 @@ public class KeyValueStore {
|
|||
setParameterValue(statement, 2, key.clazz(), value);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
synchronized (cache) {
|
||||
cache.put(key, value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T readValueFromResultSet(
|
||||
final KeyValueEntry<T> key, final ResultSet resultSet
|
||||
final KeyValueEntry<T> key,
|
||||
final ResultSet resultSet
|
||||
) throws SQLException {
|
||||
Object value;
|
||||
final var clazz = key.clazz();
|
||||
|
@ -134,7 +155,10 @@ public class KeyValueStore {
|
|||
}
|
||||
|
||||
private static <T> void setParameterValue(
|
||||
final PreparedStatement statement, final int parameterIndex, final Class<T> clazz, final T value
|
||||
final PreparedStatement statement,
|
||||
final int parameterIndex,
|
||||
final Class<T> clazz,
|
||||
final T value
|
||||
) throws SQLException {
|
||||
if (clazz == int.class || clazz == Integer.class) {
|
||||
if (value == null) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
@ -75,7 +76,7 @@ public class MessageCache {
|
|||
return cachedMessage;
|
||||
}
|
||||
logger.debug("Moving cached message {} to {}", cachedMessage.getFile().toPath(), cacheFile.toPath());
|
||||
Files.move(cachedMessage.getFile().toPath(), cacheFile.toPath());
|
||||
Files.move(cachedMessage.getFile().toPath(), cacheFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
return new CachedMessage(cacheFile);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
|
|||
import org.asamk.signal.manager.storage.Utils;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.InvalidKeyIdException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -176,8 +177,8 @@ public class PreKeyStore implements SignalServicePreKeyStore {
|
|||
private PreKeyRecord getPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException {
|
||||
try {
|
||||
final var keyId = resultSet.getInt("key_id");
|
||||
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0);
|
||||
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key"));
|
||||
final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
|
||||
final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
|
||||
return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey));
|
||||
} catch (InvalidKeyException e) {
|
||||
return null;
|
||||
|
|
|
@ -4,8 +4,9 @@ import org.asamk.signal.manager.storage.Database;
|
|||
import org.asamk.signal.manager.storage.Utils;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.InvalidKeyIdException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -238,8 +239,8 @@ public class SignedPreKeyStore implements org.signal.libsignal.protocol.state.Si
|
|||
private SignedPreKeyRecord getSignedPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException {
|
||||
try {
|
||||
final var keyId = resultSet.getInt("key_id");
|
||||
final var publicKey = Curve.decodePoint(resultSet.getBytes("public_key"), 0);
|
||||
final var privateKey = Curve.decodePrivatePoint(resultSet.getBytes("private_key"));
|
||||
final var publicKey = new ECPublicKey(resultSet.getBytes("public_key"));
|
||||
final var privateKey = new ECPrivateKey(resultSet.getBytes("private_key"));
|
||||
final var signature = resultSet.getBytes("signature");
|
||||
final var timestamp = resultSet.getLong("timestamp");
|
||||
return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature);
|
||||
|
|
|
@ -34,7 +34,8 @@ public class LegacyProfileStore {
|
|||
|
||||
@Override
|
||||
public List<LegacySignalProfileEntry> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ public interface ProfileStore {
|
|||
void storeProfileKey(RecipientId recipientId, ProfileKey profileKey);
|
||||
|
||||
void storeExpiringProfileKeyCredential(
|
||||
RecipientId recipientId, ExpiringProfileKeyCredential expiringProfileKeyCredential
|
||||
RecipientId recipientId,
|
||||
ExpiringProfileKeyCredential expiringProfileKeyCredential
|
||||
);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,9 @@ public class LegacyJsonIdentityKeyStore {
|
|||
private final int localRegistrationId;
|
||||
|
||||
private LegacyJsonIdentityKeyStore(
|
||||
final List<LegacyIdentityInfo> identities, IdentityKeyPair identityKeyPair, int localRegistrationId
|
||||
final List<LegacyIdentityInfo> identities,
|
||||
IdentityKeyPair identityKeyPair,
|
||||
int localRegistrationId
|
||||
) {
|
||||
this.identities = identities;
|
||||
this.identityKeyPair = identityKeyPair;
|
||||
|
@ -77,7 +79,8 @@ public class LegacyJsonIdentityKeyStore {
|
|||
|
||||
@Override
|
||||
public LegacyJsonIdentityKeyStore deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ public class LegacyJsonPreKeyStore {
|
|||
|
||||
@Override
|
||||
public LegacyJsonPreKeyStore deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ public class LegacyJsonSessionStore {
|
|||
|
||||
@Override
|
||||
public LegacyJsonSessionStore deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ public class LegacyJsonSignedPreKeyStore {
|
|||
|
||||
@Override
|
||||
public LegacyJsonSignedPreKeyStore deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
public IdentityChange saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return identityKeyStore.saveIdentity(address, identityKey);
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,9 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||
|
||||
@Override
|
||||
public void storeSenderKey(
|
||||
final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record
|
||||
final SignalProtocolAddress sender,
|
||||
final UUID distributionId,
|
||||
final SenderKeyRecord record
|
||||
) {
|
||||
senderKeyStore.storeSenderKey(sender, distributionId, record);
|
||||
}
|
||||
|
@ -189,7 +191,8 @@ public class SignalProtocolStore implements SignalServiceAccountDataStore {
|
|||
|
||||
@Override
|
||||
public void markSenderKeySharedWith(
|
||||
final DistributionId distributionId, final Collection<SignalProtocolAddress> addresses
|
||||
final DistributionId distributionId,
|
||||
final Collection<SignalProtocolAddress> addresses
|
||||
) {
|
||||
senderKeyStore.markSenderKeySharedWith(distributionId, addresses);
|
||||
}
|
||||
|
|
|
@ -98,9 +98,7 @@ public class CdsiStore {
|
|||
}
|
||||
}
|
||||
|
||||
private static void removeNumbers(
|
||||
final Connection connection, final Set<String> numbers
|
||||
) throws SQLException {
|
||||
private static void removeNumbers(final Connection connection, final Set<String> numbers) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
DELETE FROM %s
|
||||
|
@ -116,7 +114,9 @@ public class CdsiStore {
|
|||
}
|
||||
|
||||
private static void addNumbers(
|
||||
final Connection connection, final Set<String> numbers, final long lastSeen
|
||||
final Connection connection,
|
||||
final Set<String> numbers,
|
||||
final long lastSeen
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -135,7 +135,9 @@ public class CdsiStore {
|
|||
}
|
||||
|
||||
private static void updateLastSeen(
|
||||
final Connection connection, final Set<String> numbers, final long lastSeen
|
||||
final Connection connection,
|
||||
final Set<String> numbers,
|
||||
final long lastSeen
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager.storage.recipients;
|
||||
|
||||
public class InvalidAddress extends AssertionError {
|
||||
|
||||
InvalidAddress(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -27,7 +27,8 @@ public class LegacyRecipientStore {
|
|||
|
||||
@Override
|
||||
public List<RecipientAddress> deserialize(
|
||||
JsonParser jsonParser, DeserializationContext deserializationContext
|
||||
JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext
|
||||
) throws IOException {
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ public class MergeRecipientHelper {
|
|||
private static final Logger logger = LoggerFactory.getLogger(MergeRecipientHelper.class);
|
||||
|
||||
static Pair<RecipientId, List<RecipientId>> resolveRecipientTrustedLocked(
|
||||
Store store, RecipientAddress address
|
||||
Store store,
|
||||
RecipientAddress address
|
||||
) throws SQLException {
|
||||
// address has at least one of serviceId/pni and optionally number/username
|
||||
|
||||
|
@ -38,7 +39,7 @@ public class MergeRecipientHelper {
|
|||
)
|
||||
) || recipient.address().aci().equals(address.aci())) {
|
||||
logger.debug("Got existing recipient {}, updating with high trust address", recipient.id());
|
||||
store.updateRecipientAddress(recipient.id(), recipient.address().withIdentifiersFrom(address));
|
||||
store.updateRecipientAddress(recipient.id(), address.withOtherIdentifiersFrom(recipient.address()));
|
||||
return new Pair<>(recipient.id(), List.of());
|
||||
}
|
||||
|
||||
|
@ -82,24 +83,25 @@ public class MergeRecipientHelper {
|
|||
recipientsToBeStripped.add(recipient);
|
||||
}
|
||||
|
||||
logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}) and strip ({})",
|
||||
logger.debug("Got separate recipients for high trust identifiers {}, need to merge ({}, {}) and strip ({})",
|
||||
address,
|
||||
recipientsToBeMerged.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")),
|
||||
recipientsToBeStripped.stream().map(r -> r.id().toString()).collect(Collectors.joining(", ")));
|
||||
resultingRecipient.map(RecipientWithAddress::address),
|
||||
recipientsToBeMerged.stream().map(r -> r.address().toString()).collect(Collectors.joining(", ")),
|
||||
recipientsToBeStripped.stream().map(r -> r.address().toString()).collect(Collectors.joining(", ")));
|
||||
|
||||
RecipientAddress finalAddress = resultingRecipient.map(RecipientWithAddress::address).orElse(null);
|
||||
for (final var recipient : recipientsToBeMerged) {
|
||||
if (finalAddress == null) {
|
||||
finalAddress = recipient.address();
|
||||
} else {
|
||||
finalAddress = finalAddress.withIdentifiersFrom(recipient.address());
|
||||
finalAddress = finalAddress.withOtherIdentifiersFrom(recipient.address());
|
||||
}
|
||||
store.removeRecipientAddress(recipient.id());
|
||||
}
|
||||
if (finalAddress == null) {
|
||||
finalAddress = address;
|
||||
} else {
|
||||
finalAddress = finalAddress.withIdentifiersFrom(address);
|
||||
finalAddress = address.withOtherIdentifiersFrom(finalAddress);
|
||||
}
|
||||
|
||||
for (final var recipient : recipientsToBeStripped) {
|
||||
|
|
|
@ -27,7 +27,7 @@ public record RecipientAddress(
|
|||
pni = Optional.empty();
|
||||
}
|
||||
if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) {
|
||||
throw new AssertionError("Must have either a ServiceId, username or E164 number!");
|
||||
throw new InvalidAddress("Must have either a ServiceId, username or E164 number!");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,8 +69,8 @@ public record RecipientAddress(
|
|||
}
|
||||
|
||||
public RecipientAddress(org.asamk.signal.manager.api.RecipientAddress address) {
|
||||
this(address.aci().map(ACI::parseOrNull),
|
||||
address.pni().map(PNI::parseOrNull),
|
||||
this(address.aci().map(ACI::parseOrThrow),
|
||||
address.pni().map(PNI::parseOrThrow),
|
||||
address.number(),
|
||||
address.username());
|
||||
}
|
||||
|
@ -79,11 +79,11 @@ public record RecipientAddress(
|
|||
this(Optional.of(serviceId), Optional.empty());
|
||||
}
|
||||
|
||||
public RecipientAddress withIdentifiersFrom(RecipientAddress address) {
|
||||
return new RecipientAddress(address.aci.or(this::aci),
|
||||
address.pni.or(this::pni),
|
||||
address.number.or(this::number),
|
||||
address.username.or(this::username));
|
||||
public RecipientAddress withOtherIdentifiersFrom(RecipientAddress address) {
|
||||
return new RecipientAddress(this.aci.or(address::aci),
|
||||
this.pni.or(address::pni),
|
||||
this.number.or(address::number),
|
||||
this.username.or(address::username));
|
||||
}
|
||||
|
||||
public RecipientAddress removeIdentifiersFrom(RecipientAddress address) {
|
||||
|
|
|
@ -208,7 +208,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
public RecipientId resolveRecipientByNumber(
|
||||
final String number, Supplier<ServiceId> serviceIdSupplier
|
||||
final String number,
|
||||
Supplier<ServiceId> serviceIdSupplier
|
||||
) throws UnregisteredRecipientException {
|
||||
final Optional<RecipientWithAddress> byNumber;
|
||||
try (final var connection = database.getConnection()) {
|
||||
|
@ -238,7 +239,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
public RecipientId resolveRecipientByUsername(
|
||||
final String username, Supplier<ACI> aciSupplier
|
||||
final String username,
|
||||
Supplier<ACI> aciSupplier
|
||||
) throws UnregisteredRecipientException {
|
||||
final Optional<RecipientWithAddress> byUsername;
|
||||
try (final var connection = database.getConnection()) {
|
||||
|
@ -301,7 +303,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
|
||||
@Override
|
||||
public RecipientId resolveRecipientTrusted(
|
||||
final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
|
||||
final Optional<ACI> aci,
|
||||
final Optional<PNI> pni,
|
||||
final Optional<String> number
|
||||
) {
|
||||
return resolveRecipientTrusted(new RecipientAddress(aci, pni, number, Optional.empty()));
|
||||
}
|
||||
|
@ -335,7 +339,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
"""
|
||||
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
|
||||
FROM %s r
|
||||
WHERE (r.number 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
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
|
||||
try (final var connection = database.getConnection()) {
|
||||
|
@ -388,11 +392,23 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, storageId.getRaw());
|
||||
return Utils.executeQuerySingleRow(statement, this::getRecipientFromResultSet);
|
||||
} catch (InvalidAddress e) {
|
||||
try (final var statement = connection.prepareStatement("""
|
||||
UPDATE %s SET aci=NULL, pni=NULL, username=NULL, number=NULL, storage_id=NULL WHERE storage_id = ?
|
||||
""".formatted(TABLE_RECIPIENT))) {
|
||||
statement.setBytes(1, storageId.getRaw());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
connection.commit();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Recipient> getRecipients(
|
||||
boolean onlyContacts, Optional<Boolean> blocked, Set<RecipientId> recipientIds, Optional<String> name
|
||||
boolean onlyContacts,
|
||||
Optional<Boolean> blocked,
|
||||
Set<RecipientId> recipientIds,
|
||||
Optional<String> name
|
||||
) {
|
||||
final var sqlWhere = new ArrayList<String>();
|
||||
if (onlyContacts) {
|
||||
|
@ -419,7 +435,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
r.discoverable,
|
||||
r.storage_record
|
||||
FROM %s r
|
||||
WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
|
||||
WHERE (r.number IS NOT NULL OR r.pni IS NOT NULL OR r.aci IS NOT NULL) AND %s
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT, sqlWhere.isEmpty() ? "TRUE" : String.join(" AND ", sqlWhere));
|
||||
final var selfAddress = selfAddressProvider.getSelfAddress();
|
||||
|
@ -505,7 +521,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
"""
|
||||
SELECT r._id
|
||||
FROM %s r
|
||||
WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL)
|
||||
WHERE (r.aci IS NOT NULL OR r.pni IS NOT NULL)
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
|
@ -518,7 +534,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
"""
|
||||
SELECT r._id
|
||||
FROM %s r
|
||||
WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL
|
||||
WHERE r.storage_id IS NULL AND r.unregistered_timestamp IS NULL AND (r.aci IS NOT NULL OR r.pni IS NOT NULL)
|
||||
"""
|
||||
).formatted(TABLE_RECIPIENT);
|
||||
final var updateSql = (
|
||||
|
@ -614,14 +630,17 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
public void storeProfileKey(
|
||||
Connection connection, RecipientId recipientId, final ProfileKey profileKey
|
||||
Connection connection,
|
||||
RecipientId recipientId,
|
||||
final ProfileKey profileKey
|
||||
) throws SQLException {
|
||||
storeProfileKey(connection, recipientId, profileKey, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeExpiringProfileKeyCredential(
|
||||
RecipientId recipientId, final ExpiringProfileKeyCredential profileKeyCredential
|
||||
RecipientId recipientId,
|
||||
final ExpiringProfileKeyCredential profileKeyCredential
|
||||
) {
|
||||
try (final var connection = database.getConnection()) {
|
||||
storeExpiringProfileKeyCredential(connection, recipientId, profileKeyCredential);
|
||||
|
@ -661,7 +680,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
public void updateStorageId(
|
||||
Connection connection, RecipientId recipientId, StorageId storageId
|
||||
Connection connection,
|
||||
RecipientId recipientId,
|
||||
StorageId storageId
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -813,7 +834,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
public void storeContact(
|
||||
final Connection connection, final RecipientId recipientId, final Contact contact
|
||||
final Connection connection,
|
||||
final RecipientId recipientId,
|
||||
final Contact contact
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -852,7 +875,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
public int removeStorageIdsFromLocalOnlyUnregisteredRecipients(
|
||||
final Connection connection, final List<StorageId> storageIds
|
||||
final Connection connection,
|
||||
final List<StorageId> storageIds
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -910,7 +934,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
public void markUndiscoverablePossiblyUnregistered(final Set<String> numbers) {
|
||||
logger.debug("Marking {} numbers as unregistered", numbers.size());
|
||||
logger.debug("Marking {} numbers as undiscoverable", numbers.size());
|
||||
try (final var connection = database.getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
for (final var number : numbers) {
|
||||
|
@ -965,11 +989,17 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private void markUnregisteredAndSplitIfNecessary(
|
||||
final Connection connection, final RecipientId recipientId
|
||||
final Connection connection,
|
||||
final RecipientId recipientId
|
||||
) throws SQLException {
|
||||
markUnregistered(connection, recipientId);
|
||||
final var address = resolveRecipientAddress(connection, recipientId);
|
||||
if (address.aci().isPresent() && address.pni().isPresent()) {
|
||||
final var needSplit = address.aci().isPresent() && address.pni().isPresent();
|
||||
logger.trace("Marking unregistered recipient {} as unregistered (and split={}): {}",
|
||||
recipientId,
|
||||
needSplit,
|
||||
address);
|
||||
if (needSplit) {
|
||||
final var numberAddress = new RecipientAddress(address.pni().get(), address.number().orElse(null));
|
||||
updateRecipientAddress(connection, recipientId, address.removeIdentifiersFrom(numberAddress));
|
||||
addNewRecipient(connection, numberAddress);
|
||||
|
@ -977,7 +1007,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private void markDiscoverable(
|
||||
final Connection connection, final RecipientId recipientId, final boolean discoverable
|
||||
final Connection connection,
|
||||
final RecipientId recipientId,
|
||||
final boolean discoverable
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -993,9 +1025,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
}
|
||||
|
||||
private void markRegistered(
|
||||
final Connection connection, final RecipientId recipientId
|
||||
) throws SQLException {
|
||||
private void markRegistered(final Connection connection, final RecipientId recipientId) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
|
@ -1009,9 +1039,7 @@ 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) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
|
@ -1046,7 +1074,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
public void storeProfile(
|
||||
final Connection connection, final RecipientId recipientId, final Profile profile
|
||||
final Connection connection,
|
||||
final RecipientId recipientId,
|
||||
final Profile profile
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -1079,7 +1109,10 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private void storeProfileKey(
|
||||
Connection connection, RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile
|
||||
Connection connection,
|
||||
RecipientId recipientId,
|
||||
final ProfileKey profileKey,
|
||||
boolean resetProfile
|
||||
) throws SQLException {
|
||||
if (profileKey != null) {
|
||||
final var recipientProfileKey = getProfileKey(connection, recipientId);
|
||||
|
@ -1111,7 +1144,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private RecipientAddress resolveRecipientAddress(
|
||||
final Connection connection, final RecipientId recipientId
|
||||
final Connection connection,
|
||||
final RecipientId recipientId
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -1150,7 +1184,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private Pair<RecipientId, List<RecipientId>> resolveRecipientTrustedLocked(
|
||||
final Connection connection, final RecipientAddress address, final boolean isSelf
|
||||
final Connection connection,
|
||||
final RecipientAddress address,
|
||||
final boolean isSelf
|
||||
) throws SQLException {
|
||||
if (address.hasSingleIdentifier() || (
|
||||
!isSelf && selfAddressProvider.getSelfAddress().matches(address)
|
||||
|
@ -1168,7 +1204,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private void mergeRecipients(
|
||||
final Connection connection, final RecipientId recipientId, final List<RecipientId> toBeMergedRecipientIds
|
||||
final Connection connection,
|
||||
final RecipientId recipientId,
|
||||
final List<RecipientId> toBeMergedRecipientIds
|
||||
) throws SQLException {
|
||||
for (final var toBeMergedRecipientId : toBeMergedRecipientIds) {
|
||||
recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId);
|
||||
|
@ -1177,9 +1215,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
}
|
||||
|
||||
private RecipientId resolveRecipientLocked(
|
||||
Connection connection, RecipientAddress address
|
||||
) throws SQLException {
|
||||
private RecipientId resolveRecipientLocked(Connection connection, RecipientAddress address) throws SQLException {
|
||||
final var byAci = address.aci().isEmpty()
|
||||
? Optional.<RecipientWithAddress>empty()
|
||||
: findByServiceId(connection, address.aci().get());
|
||||
|
@ -1236,7 +1272,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private RecipientId addNewRecipient(
|
||||
final Connection connection, final RecipientAddress address
|
||||
final Connection connection,
|
||||
final RecipientAddress address
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -1277,7 +1314,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private void updateRecipientAddress(
|
||||
Connection connection, RecipientId recipientId, final RecipientAddress address
|
||||
Connection connection,
|
||||
RecipientId recipientId,
|
||||
final RecipientAddress address
|
||||
) throws SQLException {
|
||||
recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId));
|
||||
final var sql = (
|
||||
|
@ -1312,7 +1351,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private void mergeRecipientsLocked(
|
||||
Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
|
||||
Connection connection,
|
||||
RecipientId recipientId,
|
||||
RecipientId toBeMergedRecipientId
|
||||
) throws SQLException {
|
||||
final var contact = getContact(connection, recipientId);
|
||||
if (contact == null) {
|
||||
|
@ -1343,7 +1384,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private Optional<RecipientWithAddress> findByNumber(
|
||||
final Connection connection, final String number
|
||||
final Connection connection,
|
||||
final String number
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.aci, r.pni, r.username
|
||||
|
@ -1358,7 +1400,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private Optional<RecipientWithAddress> findByUsername(
|
||||
final Connection connection, final String username
|
||||
final Connection connection,
|
||||
final String username
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.aci, r.pni, r.username
|
||||
|
@ -1373,7 +1416,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private Optional<RecipientWithAddress> findByServiceId(
|
||||
final Connection connection, final ServiceId serviceId
|
||||
final Connection connection,
|
||||
final ServiceId serviceId
|
||||
) throws SQLException {
|
||||
var recipientWithAddress = Optional.ofNullable(recipientAddressCache.get(serviceId));
|
||||
if (recipientWithAddress.isPresent()) {
|
||||
|
@ -1394,7 +1438,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private Set<RecipientWithAddress> findAllByAddress(
|
||||
final Connection connection, final RecipientAddress address
|
||||
final Connection connection,
|
||||
final RecipientAddress address
|
||||
) throws SQLException {
|
||||
final var sql = """
|
||||
SELECT r._id, r.number, r.aci, r.pni, r.username
|
||||
|
@ -1447,7 +1492,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
}
|
||||
|
||||
private ExpiringProfileKeyCredential getExpiringProfileKeyCredential(
|
||||
final Connection connection, final RecipientId recipientId
|
||||
final Connection connection,
|
||||
final RecipientId recipientId
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
"""
|
||||
|
@ -1593,7 +1639,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
public interface RecipientMergeHandler {
|
||||
|
||||
void mergeRecipients(
|
||||
final Connection connection, RecipientId recipientId, RecipientId toBeMergedRecipientId
|
||||
final Connection connection,
|
||||
RecipientId recipientId,
|
||||
RecipientId toBeMergedRecipientId
|
||||
) throws SQLException;
|
||||
}
|
||||
|
||||
|
@ -1617,7 +1665,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
|
|||
|
||||
@Override
|
||||
public void updateRecipientAddress(
|
||||
final RecipientId recipientId, final RecipientAddress address
|
||||
final RecipientId recipientId,
|
||||
final RecipientAddress address
|
||||
) throws SQLException {
|
||||
RecipientStore.this.updateRecipientAddress(connection, recipientId, address);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,9 @@ public interface RecipientTrustedResolver {
|
|||
|
||||
@Override
|
||||
public RecipientId resolveRecipientTrusted(
|
||||
final Optional<ACI> aci, final Optional<PNI> pni, final Optional<String> number
|
||||
final Optional<ACI> aci,
|
||||
final Optional<PNI> pni,
|
||||
final Optional<String> number
|
||||
) {
|
||||
return recipientTrustedResolverSupplier.get().resolveRecipientTrusted(aci, pni, number);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue