Compare commits

..

758 Commits

Author SHA1 Message Date
AsamK
3c17e01c37 Fix storage sync issues 2026-08-21 21:39:51 +02:00
AsamK
9605c94f09 Improve contact merging 2026-08-21 20:47:35 +02:00
AsamK
d2f5696df1 Update libsignal-service 2026-08-21 20:35:42 +02:00
AsamK
81e513d6b7 Implement storage sync loop detector 2026-08-21 18:46:03 +02:00
AsamK
dbf11fd006 Update graalvm build tools 2026-08-21 18:37:35 +02:00
Bazyli Brzóska
7e802166a3
fix(json): expose data message metadata (#2104) 2026-08-17 12:40:27 +02:00
Bazyli Brzóska
0f88410465
feat(json): expose voice-note attachment metadata (#2100)
Include isVoiceNote in receive JSON and JSON-RPC attachment payloads. Add regression coverage for true and false serialization.
2026-08-10 10:38:27 +02:00
Javair Ratliff
13819c8f09
Support the GroupsV2 "end group" (terminate) feature (#2098)
* Show and enforce the terminated state of a group

A group admin can permanently terminate a group. Afterwards the group is
read-only for everyone, not even an admin can send messages or start calls;
members keep access to the existing history. Other clients silently drop
messages sent to a terminated group.

Read the DecryptedGroup.terminated flag through GroupInfo.isTerminated() and:

- surface it in listGroups (json and plain text) and as the DBus IsTerminated
  group property
- refuse to send to a terminated group locally (messages, reactions, typing and
  group stories) rather than send one that other clients ignore
- drop incoming messages addressed to a terminated group

* Add terminateGroup command to terminate a group for everyone

Let a group admin permanently terminate a GroupV2 group via the
TerminateGroupAction (Groups.proto field 28, change epoch 7) the pinned library
already exposes as GroupsV2Operations.createTerminateGroup().

- GroupV2Helper.terminateGroup builds and commits the change
- GroupHelper.terminateGroup resolves/refreshes the group (v2 only), sends the
  update to members and syncs storage, with the same conflict-retry as
  updateGroup
- exposed through Manager.terminateGroup, the terminateGroup CLI/JSON-RPC command
  and the DBus Group.terminateGroup method

---------

Co-authored-by: FailSpy <FailSpy@users.noreply.github.com>
2026-08-08 17:16:49 +02:00
AsamK
85580d1ff6 Fix graalvm issue with image dimension probing
Fixes #2096
2026-08-04 13:16:58 +02:00
AsamK
f8ea8ed314 Update graalvm build tools 2026-08-04 13:14:29 +02:00
AsamK
472784e614 Replace unmaintained release actions 2026-08-02 08:45:09 +02:00
AsamK
db97731f63 Update redhat-actions in workflow 2026-08-02 08:45:09 +02:00
AsamK
ff9f2028ae Prepare next release 2026-08-02 08:45:09 +02:00
AsamK
9ee5262876 Prepare next release 2026-08-01 10:07:44 +02:00
AsamK
b01b6b370d Fix build pipeline 2026-08-01 09:38:01 +02:00
AsamK
890f798a03 Bump version to 0.14.7 2026-08-01 09:30:18 +02:00
AsamK
3c319f8b4a Reformat files 2026-08-01 09:24:09 +02:00
AsamK
fd3f572499 Use nickname for displaying recipient names 2026-08-01 09:21:52 +02:00
AsamK
854cb35d15 Explicitly store pni signature verified 2026-07-31 13:20:34 +02:00
AsamK
e78521bb5c Always set e164 in sync if available 2026-07-31 13:09:00 +02:00
AsamK
86ad7bf53f Always set the read receipts setting during provisioning if available 2026-07-31 13:08:13 +02:00
AsamK
de5daf0ad4 Cleanup incomplete account data after unsuccessful link
Fixes #2080
2026-07-31 09:01:08 +02:00
AsamK
ecc76ec52b Improve handling of pni change number messages 2026-07-30 22:44:06 +02:00
AsamK
6fa1d1bf90 Fix marking sender as needs pni signature 2026-07-30 22:44:06 +02:00
AsamK
d29f6c1641 Fix removing self from pending group invite via PNI 2026-07-30 22:44:06 +02:00
AsamK
b3e3eda4f7 Use upstream provisioning functionality 2026-07-30 22:44:06 +02:00
AsamK
1e59c6f6cc Readd job to update github dependency graph 2026-07-30 17:39:40 +02:00
AsamK
457658aca0 Update dependencies 2026-07-29 22:33:49 +02:00
AsamK
2cf5e25e9e Implement sticker pack sync 2026-07-29 22:33:49 +02:00
AsamK
151b42c6e9 Update libsignal-service 2026-07-29 22:33:48 +02:00
AsamK
91138452bb Update graalvm build tools 2026-07-29 19:20:31 +02:00
AsamK
304ffb0e33 Add special handling for file input stream attachments 2026-07-29 19:20:22 +02:00
mikesimone
987832099d
Set width/height on outgoing image attachments (#2093)
createAttachmentStream() never populated SignalServiceAttachmentStream's
width/height fields, even though the Builder has supported them since at
least the currently pinned libsignal-service version. Without them, other
Signal clients render outgoing image attachments as a square-cropped
thumbnail instead of their actual aspect ratio.

Probe dimensions via ImageIO for image/* attachments up to 20MiB (buffered
in memory only for that check) and pass them through to the builder.
Falls back to width/height 0 (today's behavior) for non-images, oversized
files, or anything ImageIO can't parse.
2026-07-29 19:07:06 +02:00
Arnout Engelen
64a629002d
docs: clarify register vs link (#2094)
This wasn't obvious to me, happy I asked in #2092 :)
2026-07-26 18:47:30 +02:00
AsamK
c26e0632c5 Prepare next release 2026-07-13 09:12:11 +02:00
AsamK
21dd00d96a Bump version to 0.14.6 2026-07-13 00:16:14 +02:00
AsamK
95c6dae927 Replace deprecated native-image-configure 2026-07-13 00:16:14 +02:00
AsamK
bb2409a7f2 Improve tests 2026-07-13 00:16:14 +02:00
AsamK
89756bb7a7 Refactor group story send 2026-07-12 17:04:32 +02:00
tonycpsu
6808b66897
Add group story support via --group-id (#2083)
* Add group story support to core library layer

Extend Manager.sendStory() with an optional GroupId parameter and add
SendHelper.sendGroupStoryMessage() for endorsement-aware group story
delivery, laying the groundwork for group story support (task 1 of 4).
The existing My Story code path is unchanged.

* Add --group-id support to SendStoryCommand

Passes an optional GroupId through to Manager.sendStory so stories can
be posted to a group instead of only My Story, and surfaces
GroupNotFoundException / NotAGroupMemberException as user errors.

* Update stubs for 3-parameter sendStory and add empty-recipient guard

Updates DbusManagerImpl and StubManager (in SubscribeCallEventsTest) to
match the new 3-parameter sendStory signature: (String attachment,
boolean allowsReplies, Optional<GroupId> groupId).

Also includes the empty-recipient guard added after Task 1 review to
prevent stories from being sent to groups where the user is the only member.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MR2KF56Qcf9qNH1URj3XWs

* Document group story support in man page and changelog

- Add --group-id (-g) option to sendStory command in man page
- Update CHANGELOG to mention group story support via --group-id
- Maintain alphabetical order of options in sendStory section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MR2KF56Qcf9qNH1URj3XWs

* Address review findings: endorsement safety and error handling

- Filter group story recipients by ACI type and endorsement availability
  to prevent ClassCastException and NPE on edge cases
- Add empty-recipient guard after endorsement filtering
- Skip known-unregistered recipients before address resolution, matching
  the pattern from sendGroupMessageInternal
- Add debug logging when recipients are filtered out
- Fix redundant error message prefixing in SendStoryCommand
- Add .superpowers/ to .gitignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MR2KF56Qcf9qNH1URj3XWs

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-12 16:32:40 +02:00
AsamK
14f98602d2 Update README 2026-07-12 15:35:50 +02:00
AsamK
fd98cce8b3 Don't send read receipt if disabled in configuration
Only send sync read message in this case
2026-07-12 10:50:43 +02:00
AsamK
4f4a4b9fb4 Refactor manager mock 2026-07-12 10:21:37 +02:00
AsamK
e15c29e4e8 Remove old svr2 enclave 2026-07-12 10:10:36 +02:00
dependabot[bot]
2e367644d9
Bump com.fasterxml.jackson.core:jackson-databind from 2.20.2 to 2.21.5 (#2085)
Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.20.2 to 2.21.5.
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 10:03:21 +02:00
tonycpsu
78dba20a59
Bound SessionStore session cache to prevent unbounded memory growth (#2087)
The cachedSessions HashMap grows with every unique (address, deviceId)
pair seen during message processing and is never evicted. In a
long-running daemon handling group messages, this causes linear memory
growth (~47 MB/hour observed) as SessionRecord objects accumulate for
every contact/device the daemon has ever communicated with.

Replace the unbounded HashMap with an LRU-bounded LinkedHashMap (access
order, max 1000 entries). Evicted sessions are reloaded from SQLite on
next access, so correctness is preserved.


Claude-Session: https://claude.ai/code/session_01HHzM2XLKQoX9iraEdhoh3h

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-12 08:12:30 +02:00
tonycpsu
b48ccf2605
Bound RecipientStore address cache to prevent unbounded memory growth (#2088)
The recipientAddressCache grows with every unique ServiceId resolved via
findByServiceId() and entries are never evicted. In a long-running daemon
handling group messages from many contacts, this map grows monotonically.

Replace the unbounded HashMap with an LRU-bounded LinkedHashMap (access
order, max 2000 entries). Evicted entries are reloaded from SQLite on
next access via an indexed lookup, so correctness is preserved.


Claude-Session: https://claude.ai/code/session_01HHzM2XLKQoX9iraEdhoh3h

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-12 08:11:24 +02:00
AsamK
54d713ea7d Improve loading accounts by aci 2026-07-11 19:03:48 +02:00
AsamK
4dda7582a3 Add getSelfACI to manager 2026-07-11 18:52:52 +02:00
tilllt
248c0c0dab
Fix HTTP handler to accept ACI/UUID account parameter in SSE endpoint (#2079)
* Fix HTTP handler to accept ACI/UUID account parameter in SSE endpoint

MultiAccountManagerImpl.getManager() only looked up accounts by phone
number, causing HTTP 400 errors when the SSE events endpoint was called
with ?account=<ACI-UUID> (e.g., cc528f93-527e-4566-8c62-d12dc99dbce0).

Changes:
- SignalAccountFiles: Add initManagerByAci() method for ACI-based lookup
- MultiAccountManagerImpl: getManager() now tries ACI lookup when
  phone number lookup fails
- HttpServerHandler: getManagerFromQuery() falls back to returning all
  managers when the specific account identifier is not found (instead of
  HTTP 400)

* Fix SSE endpoint for UUID account parameter & preserve '+' in phone numbers

Three interrelated fixes for the HTTP SSE endpoint:

1. **SignalAccountFiles** — Replace ACI.parseOrThrow() with UUID-string
   lookup from accountsStore.getAllAccounts(). The old approach failed when
   a raw UUID string (from URL query param) was passed. Added
   getAccountNumberByAci() helper to reduce duplication.

2. **MultiAccountManagerImpl** — Catch IllegalArgumentException in
   getManager() for both phone number and ACI lookup paths. Also check if
   the UUID corresponds to an already-loaded manager before trying to
   initByAci(), preventing OverlappingFileLockException when SSE requests
   arrive with a UUID for an account that was loaded at startup.

3. **Util.getQueryMap()** — Preserve '+' characters in query parameter
   values by escaping them before URLDecoder.decode(). Without this,
   URLDecoder converts '+' to space, breaking phone numbers like
   '+4915422389' which become ' 4915422389'.

* fix: address AsamK's review comments

- HttpServerHandler.getManagerFromQuery(): return null when account not
  found instead of falling back to all managers (AsamK: 'should stay
  return null here')
- MultiAccountManagerImpl.getManager(): use UuidUtil.isUuid() to branch
  early on ACI vs phone number, eliminating the try-number-then-fallback
  pattern (AsamK: 'check if identifier is a uuid first')

---------

Co-authored-by: Till L T <tilllt@users.noreply.github.com>
2026-07-11 18:19:15 +02:00
Gara Dorta
e70bddd790
feat: add schemas to the release CI (#2040) 2026-07-11 16:47:41 +02:00
tonycpsu
ac5ed431d3
Add sendStory command for posting file attachment stories (#2082)
* Add sendStory method for posting file attachment stories to My Story

Adds Manager.sendStory(attachment, allowsReplies), which uploads a file
attachment, builds a SignalServiceStoryMessage, and sends it to all
registered, non-blocked, non-hidden contacts that haven't opted out of
seeing the user's story (Contact.hideStory), excluding self. SendHelper
gains sendStoryMessage(), which resolves recipient addresses and
unidentified access and delegates to
SignalServiceMessageSender.sendGroupStory() against
DistributionId.MY_STORY, following the same address/access resolution
pattern used for group sends. A sync transcript is sent afterwards via
sendStorySyncMessage() so linked devices see the story was posted.

This is core library plumbing only; no CLI command, stub
implementations, or documentation are added yet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MR2KF56Qcf9qNH1URj3XWs

* Add sendStory command for posting stories via CLI and JSON-RPC

Implements SendStoryCommand to allow users to post stories through the
CLI and JSON-RPC interfaces. Command accepts an attachment file path
(required) and optional --no-replies flag to disable replies on the story.

Handles AttachmentInvalidException and IOException appropriately and
outputs results using SendMessageResultUtils.

Registered in Commands.java in alphabetical order.

* Add sendStory stubs to DbusManagerImpl and StubManager

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MR2KF56Qcf9qNH1URj3XWs

* Document sendStory in man page and changelog

* Validate story attachment MIME type and fix D-Bus stub

- Reject non-image/video attachments before uploading, since stories
  only support image and video content
- Change DbusManagerImpl.sendStory to throw UnsupportedOperationException
  to match the pattern used by all other unimplemented D-Bus methods

* Resolve recipients before uploading story attachment

Move recipient resolution ahead of the attachment upload so that an
empty contact list is caught early without wasting bandwidth on an
upload that would reach nobody.

* Address review feedback: fix hideStory filter and remove redundant sync

- Fix hideStory filter to require contact exists (!=null &&) instead of
  permitting null contacts (==null ||), matching the intent of filtering
  to contacts who haven't hidden stories
- Remove manual sendStorySyncMessage call, as the library's sendStory
  already handles sync internally

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHzM2XLKQoX9iraEdhoh3h

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-11 16:34:58 +02:00
AsamK
fb10e1a501 Fix sync issue 2026-07-11 14:37:20 +02:00
AsamK
2a744edd43 Only sync e164 if pni is also present 2026-07-11 14:00:43 +02:00
AsamK
c95a67862e Update libsignal-service 2026-07-11 13:18:45 +02:00
AsamK
5a525f51f6 Update gradle 2026-07-11 12:58:35 +02:00
AsamK
eab6ffbd8c Fix gradle deprecation warning 2026-07-11 12:58:04 +02:00
AsamK
8809b90b72 Update graalvm build tools 2026-07-11 12:57:42 +02:00
AsamK
98d9960e66 Update graalvm build tools 2026-07-02 10:31:14 +02:00
AsamK
254efba643 Prepare next release 2026-07-02 10:30:43 +02:00
AsamK
bb433ebe59 Print hangup message details 2026-07-02 10:29:51 +02:00
KritBlade
5ac06a5ccd
Skip known-unregistered recipients when sending to groups (#2077)
Group sends built the recipient list from the full group membership, so
members already known to be unregistered were retried on every send via
the legacy 1:1 fan-out. On large groups this made a single send take
tens of seconds and could time out, leaving the message undelivered.

Filter out recipients whose unregistered timestamp is set before
sending, returning an unregisteredFailure result for each (so callers
and CLI output are unchanged) without the network attempt. The flag is
maintained independently by profile/CDS discovery and is cleared when a
recipient registers again, so skipped recipients are re-included
automatically.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:56:22 +02:00
AsamK
debb8a20e6 Send HangupMessage with deviceId 0 like Signal-Android
Closes #2068
2026-06-11 20:12:34 +02:00
AsamK
6bef205b3f Bump version 0.14.5 2026-06-11 12:53:41 +02:00
AsamK
443b4da8f8 Update graalvm build tools 2026-06-11 12:34:09 +02:00
AsamK
afe8c24665 Update user agent 2026-06-11 12:00:14 +02:00
AsamK
9f0676d563 Disable host validation when binding on 0.0.0.0
Fixes #2056
2026-06-11 11:43:49 +02:00
AsamK
b3c1b6a4f6 Update svr2 mrenclave 2026-06-10 23:05:39 +02:00
AsamK
bf1376d74d Update libsignal-service 2026-06-10 23:03:11 +02:00
AsamK
de678596c6 Update test script 2026-05-25 17:39:53 +02:00
AsamK
29e65a5c8e Add missing graalvm metadata
Fixes #2050
2026-05-25 11:51:08 +02:00
AsamK
60779c91c6 Add missing graalvm metadata
Fixes #2051
2026-05-25 11:40:46 +02:00
AsamK
bda4e7fc0f Prepare next release 2026-05-24 01:20:23 +02:00
AsamK
12ffc34967 Bump version to 0.14.4.1 2026-05-24 00:51:21 +02:00
AsamK
9e3585dcce Fix cli default values 2026-05-24 00:51:12 +02:00
AsamK
46c61c5aac Bump version to 0.14.4 2026-05-23 23:01:29 +02:00
AsamK
8d6264e02e Update dependencies 2026-05-23 22:56:09 +02:00
AsamK
f057c5031c Fix use of deprecated API 2026-05-23 22:55:54 +02:00
AsamK
40b1928844 Fix removal of local only unregistered accounts in storage sync 2026-05-23 22:39:59 +02:00
AsamK
2a827f1285 Don't log empty alerts 2026-05-23 21:34:33 +02:00
AsamK
ced9560040 Update libsignal-service 2026-05-23 20:54:42 +02:00
AsamK
44d54b3215 Add support for a global configuration file 2026-05-23 17:56:35 +02:00
AsamK
393e1efcd1 Create temp file with limited permissions 2026-05-23 14:30:33 +02:00
AsamK
f34b552054 Validate host header for http daemon 2026-05-23 14:21:25 +02:00
AsamK
46ce552589 Normalize attachment ids 2026-05-23 14:17:28 +02:00
AsamK
6da5c37504 Prevent attaching files from the signal-cli data directory 2026-05-23 13:50:39 +02:00
AsamK
4601e60118 Adapt containerfile to older apt versions 2026-05-16 11:14:17 +02:00
AsamK
fcf82b9318 Adapt containerfile to older apt versions 2026-05-16 10:48:12 +02:00
AsamK
9c8137fafa Update dependencies 2026-05-15 17:06:21 +02:00
AsamK
0a1531dcce Improve destination/source checks for incoming messages 2026-05-13 18:29:26 +02:00
AsamK
c10f618a3e Update gradle 2026-05-13 18:26:12 +02:00
AsamK
4a3d9d90a6 Update libsignal-service 2026-05-13 18:25:35 +02:00
AsamK
b4275414e1 Pass correct serviceId to SignalServiceCipher
Fixes #2036
2026-05-13 17:39:44 +02:00
Connor Lanigan
5f94b7b6d1
fix: Attempted immutable list modification causes runtime exception (#2038) 2026-05-05 10:15:21 +02:00
legacycode
dc43e44020
fix: flush SSE response headers immediately on connect (#2034)
Without an initial flush(), the JVM HttpServer buffers all
output until the first flush() in the 15-second keep-alive loop.
Clients with shorter timeouts (e.g. 10 s) abort before receiving
any data.

Add a flush() call directly after creating ServerSentEventSender,
before the wait loop, so the HTTP 200 response and headers reach the
client immediately upon connection.

Adds regression test SseInitialFlushTest that verifies at least one
byte arrives within 2 seconds of connecting to GET /api/v1/events.
2026-04-29 22:52:34 +02:00
AsamK
251bd2d87a Refactor profile key extraction 2026-04-27 16:36:22 +02:00
AsamK
a3fcda7598 Update kotlin jvm version for buildSrc 2026-04-27 16:27:15 +02:00
Patrick Dattilio
c9e2504349
Store profile keys from group requesting members (#2031)
When filling or updating a V2 group, profile keys were copied from
DecryptedGroup.members into the local profile store but not from
requestingMembers. Admins who never had a prior session with a user in
the join queue then lacked profile keys and could not decrypt profiles
(e.g. for listContacts).

Also process DecryptedRequestingMember entries the same way as full
members, using DecryptedMember / DecryptedRequestingMember types so the
lib module does not require a direct protobuf dependency.

Made-with: Cursor
2026-04-27 16:25:47 +02:00
dependabot[bot]
9b09df5f17
Bump rustls-webpki from 0.103.12 to 0.103.13 in /client (#2030)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.12 to 0.103.13.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.12...v/0.103.13)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 19:55:35 +02:00
AsamK
5fe94ff44a Temporarily disable 26 build due to container issue 2026-04-23 21:58:10 +02:00
AsamK
6286a054eb Update libsignal-service 2026-04-23 20:37:46 +02:00
AsamK
e6635d1bb0 Prepare next release 2026-04-23 20:37:46 +02:00
AsamK
056878fad7 Bump version to 0.14.3 2026-04-22 23:16:58 +02:00
AsamK
da214817be Reduce number of installed packages 2026-04-22 22:43:13 +02:00
AsamK
d6edaf3be2 Update reproducible build container images 2026-04-22 22:42:49 +02:00
AsamK
47e50988b5 Make SOURCE_DATE_EPOCH configurable 2026-04-22 22:24:26 +02:00
AsamK
aa446619f2 Add script to update pinned container versions 2026-04-22 22:08:10 +02:00
AsamK
6405655127 Only build native/client containers if required 2026-04-22 21:43:26 +02:00
AsamK
b8d990b0f9 Adapt workflows to use previous naming scheme 2026-04-22 21:36:08 +02:00
AsamK
417d2ce971 Keep websocket connection alive during call 2026-04-16 21:04:20 +02:00
AsamK
33b2b563b3 Don't send busy call response to allow linked devices to accept call 2026-04-16 20:54:19 +02:00
AsamK
740cd6f89b Update dependencies 2026-04-16 20:53:58 +02:00
AsamK
7887ed408d Update libsignal-service 2026-04-16 20:47:39 +02:00
tonycpsu
ddfad2c4ce
Add distinct JSON-RPC error code for captcha rejection (#2021)
* Add distinct JSON-RPC error code for captcha rejection

Previously submitRateLimitChallenge mapped CaptchaRejectedException to
the generic USER_ERROR code (-1), making it indistinguishable from any
other user error (bad params, unknown command, etc.).

Introduce CaptchaRejectedErrorException and wire it to a new error code
(-6 / CAPTCHA_REJECTED_ERROR) throughout the JSON-RPC layer. Callers can
now reliably distinguish a rejected captcha token (user must obtain a
fresh token) from a network failure (transient, worth retrying) or a
generic argument error.

The CLI exit code for this path becomes 6, consistent with the existing
per-error-type exit code convention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add exit code 6 to man page

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 08:00:39 +02:00
AsamK
7e95ea7403 Log identifier of failed profile download
Fixes #2003
2026-04-15 21:18:26 +02:00
BarbossHack
2991cdafe7
Add reproducible builds (#1954) 2026-04-15 21:10:42 +02:00
AsamK
561dfc373f Refactor retry after handling 2026-04-15 21:01:29 +02:00
Gara Dorta
5bfb044245
JSON Schema for JSON-RPC (#1952)
* Add OpenAPIDocs

* Remove the json prefix from the names

* Format file

* Rename models to schemas

* Add required = true to all the required attributes

* Add missing required = true schemas

* Deprecated fields are not required

* switch to micronout json generation

* Fix generator for JsonUnwrapped files

* Fix layout of manual schemas

* Pretty print the json files

* Remove @JsonProperty(required = true)

* Make references local

* Updated the readme

* Removed uneeded import

* Remove extra empty lines

* Clean readme

* Add docs depedency only when needed

* Revert uneeded changes

* Revert more changes

* Better formatting

* Simplified name

* fix: remove jsonunwrapped workaround by upgrading to micronaut-json-schema version 2.0.0-M6

* Simplified jsonSchemas task definition

* Updated readme with the new schemas path

* typo fixing

* Remove empty space from merge
2026-04-15 20:57:36 +02:00
tonycpsu
e1b17bf863
Surface server Retry-After for rate-limit send failures (#2016)
* Surface retry-after seconds for plain rate-limit failures

libsignal-service's RateLimitException exposes retryAfterMilliseconds
for HTTP 413 responses, but signal-cli only forwarded retry-after for
ProofRequired (428) failures. Clients had no signal for when it was
safe to retry plain rate-limited sends, so every failed retry
potentially extended the server-side window.

SendMessageResult now carries an optional rateLimitRetryAfterSeconds,
populated from the upstream Optional<Long>. JsonSendMessageResult
exposes it for RATE_LIMIT_FAILURE type. Text output includes the
window when known. Aggregate RateLimitErrorException now carries the
real nextAttemptTimestamp (was hardcoded to 0).

Closes #1996.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: include proof-required retry-after and ceiling-round millis

Codex adversarial review flagged two issues in the phase 1 retry-after
plumbing:

* Aggregate retry-after ignored proof-required failures. Because
  isRateLimitFailure is true for proof-required cases but
  rateLimitRetryAfterSeconds was only populated from plain 413s, an
  all-proof-required batch (or a mixed batch where the proof-required
  delay was longer) could flow into outputResult() and produce a
  RateLimitException(0), telling callers to retry immediately.

* Millisecond Retry-After values were truncated by integer division,
  so 1..999ms became 0 and non-second-aligned values lost up to 999ms.
  A retry suggested from the floored value can land before the
  server's real deadline and re-trigger the limit.

SendMessageResult.from(...) now populates rateLimitRetryAfterSeconds
from either the proof-required seconds or the plain rate-limit ms
(converted via ceiling division), giving maxRateLimitRetryAfterSeconds
a single source of truth. JsonSendMessageResult.from(...) reads the
unified field. New millisToCeilingSeconds helper plus boundary test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Preserve source compat and document retry-after field change

Add a non-canonical 8-arg SendMessageResult constructor that delegates
to the canonical form with null retry-after. This keeps source
compatibility for any downstream code that constructs the record
directly (tests, mocks) without changing the canonical shape. Records
permit additional constructors alongside the canonical one.

Document the retryAfterSeconds meaning change in the CHANGELOG. The
field was previously populated only for proof-required failures; it
is now populated whenever the server sends a Retry-After header. The
canonical proof-required discriminator is still token != null.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:17:54 +02:00
AsamK
aafb40fd94 Increase connection disconnect duration to match android app
Related #2018
2026-04-15 00:17:44 +02:00
Stefan Meinecke
7dc55eba81
Fix sender key re-distribution on every group message (#2019)
* Fix sender key re-distribution on every group message in daemon mode

sendGroupMessageInternalWithSenderKey() calls sender.send() which handles
distribution and delivery, but never calls markSenderKeySharedWith() on
success. SenderKeySharedStore therefore has no record that the distribution
was sent, causing it to re-distribute to all recipients on every subsequent
sendGroupMessage call.

This results in a fresh unidentified TLS connection being opened for each
group message (~6s delay per send), even for back-to-back sends to the
same group. All send modes are affected: DBus daemon, JSON-RPC socket/http,
and CLI send command all share the same code path.

The fix mirrors the existing pattern in resendMessage() (line 307): after
a successful send, record each successful recipient's address+device in
the sender key shared store.

* Fix sender key re-distribution on every group message

SenderKeySharedStore.markSenderKeysSharedWith() stored the address using
entry.toString() instead of entry.address(). Since SenderKeySharedEntry is
a Java record, toString() returns the full record representation:

  SenderKeySharedEntry[address=<uuid>, deviceId=1]

instead of just the UUID. When signal-service-java later calls
getSenderKeySharedWith() and compares the retrieved addresses against the
current group member UUIDs, the comparison always fails — causing the
distribution message to be re-sent to all recipients on every
sendGroupMessage call.

This results in a fresh unidentified TLS connection being opened for each
group message (~6s delay per send), even for immediate consecutive sends
to the same group. All send modes are affected: DBus daemon, JSON-RPC
socket/http, and the CLI send command all share the same code path.

The fix is a one-character change: entry.address() instead of
entry.toString().
2026-04-15 00:06:19 +02:00
AsamK
a03d17a9e4 Use padded and encrypted attachment size for upload spec
Fixes #2014
2026-04-12 22:34:59 +02:00
AsamK
364f89f1d0 Prepare next release 2026-04-12 22:09:36 +02:00
AsamK
d0ee90dbbc Reformat files 2026-04-11 12:29:16 +02:00
AsamK
398faa50b0 Make address cache synchronized 2026-04-11 12:26:41 +02:00
AsamK
e9eabbeeb5 Add commits in early returns 2026-04-11 12:26:24 +02:00
tonycpsu
132dfb95dc
Fix SQLiteException in resolveRecipient by checking cache before opening connection (#2011) 2026-04-11 12:23:15 +02:00
AsamK
2651823d4d Fix add group member handling for already members 2026-04-11 11:56:50 +02:00
AsamK
4709cfacc7 Update multiple member roles in one change
Fixes #2009
2026-04-11 11:55:59 +02:00
AsamK
9bc4c0ecd8 Update libsignal-service 2026-04-10 18:24:57 +02:00
AsamK
763ddf85e6 Bump version to 0.14.2 2026-04-04 15:05:24 +02:00
AsamK
b2bab0d0dc Add libsignal-version file
Fixes #1964
2026-04-03 11:40:08 +02:00
AsamK
62fc96c4c9 Handle MustRequestNewCodeException
Fixes #1968
2026-04-03 11:20:46 +02:00
AsamK
2667688139 Update gradle wrapper 2026-04-03 11:20:01 +02:00
AsamK
990d1eab58 Add java 26 to ci build 2026-04-03 10:11:04 +02:00
AsamK
e6b33b8da7 Check for missing attachment id
Closes #1989
2026-04-02 22:06:10 +02:00
AsamK
d40f62ec21 Adapt exception handling to libsignal-service changes 2026-04-02 21:32:19 +02:00
AsamK
265369e353 Update libsignal-service 2026-04-01 22:47:54 +02:00
AsamK
d1106299fe Pass sender device id to ice handler 2026-04-01 22:47:54 +02:00
AsamK
7919a0f4aa Change subscribeCallEvents command to match subscribeReceive 2026-04-01 22:47:54 +02:00
AsamK
7a8a34f45e Some call refactoring 2026-04-01 22:47:54 +02:00
AsamK
0a777ea7df Some minor code improvements 2026-04-01 22:08:08 +02:00
AsamK
103a0807ca Update graalvm buildtools 2026-04-01 22:07:48 +02:00
Shaheen Gandhi
135d3a1677
Add voice calling support (#1932)
* Add voice call API types, protobuf definitions, and build dependencies

Define call method interfaces in Manager, create API records (CallInfo,
CallOffer, TurnServer), and hand-coded protobuf parsers for RingRTC
signaling messages (ConnectionParametersV4, RtpDataMessage).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Implement call signaling state machine and message routing

Add CallSignalingHelper for x25519 key generation and HKDF-based SRTP
key derivation. Add CallManager for tracking active calls, spawning
call tunnel subprocesses, and handling call lifecycle (offer, answer,
ICE candidates, hangup, busy). Wire call message routing in
IncomingMessageHandler and implement Manager call methods in ManagerImpl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add call state notification mechanism for JSON-RPC clients

Implement CallEventListener callback pattern that fires on every call
state transition (RINGING_INCOMING, RINGING_OUTGOING, CONNECTING,
CONNECTED, ENDED). The JSON-RPC layer auto-subscribes and pushes
callEvent notifications alongside receive notifications.

Changes:
- Manager.java: Add CallEventListener interface and methods
- ManagerImpl.java: Implement add/removeCallEventListener with cleanup
- DbusManagerImpl.java: Add stub implementation (not supported over DBus)
- JsonCallEvent.java: JSON notification record for call events
- SignalJsonRpcDispatcherHandler.java: Auto-subscribe call event listeners

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Add JSON-RPC commands for voice call control

Add startCall, acceptCall, hangupCall, rejectCall, and listCalls
commands for the JSON-RPC daemon interface. Register commands and
update GraalVM metadata for native image support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add call tunnel documentation

Add documentation about the architecture, protocol, and implementation of
signal-call-tunnel, the secure tunnel subprocess for voice calling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove unused integration test tag from lib/build.gradle.kts

The excludeTags("integration") block was added but no tests use the
@Tag("integration") annotation. Revert to upstream's simple
useJUnitPlatform() call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Derive install dir from jar location instead of nonexistent property

The signal.cli.install.dir system property was never set by the Gradle
start script or anywhere else. Replace it with code source detection:
resolve the jar's parent directory to find the install root, then look
for bin/signal-call-tunnel relative to that.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove explicit success responses from hangup and reject commands

Successful commands with no additional information should not return
a response, matching the pattern used by other signal-cli commands
like SendSyncRequestCommand and UpdateConfigurationCommand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use instanceof pattern matching for call ID extraction

Replace explicit null check and Number cast with instanceof pattern
matching in AcceptCallCommand, HangupCallCommand, and
RejectCallCommand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Guard handleIncoming* methods against missing call event listeners

Skip processing incoming call offers when no call event listeners are
registered, since there is nobody to notify about the call. For hangup
and busy, also guard when there are no listeners AND no active call
(the tunnel may still need cleanup if already spawned).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use Jackson JSON serialization in CallManager

Replace all manual JSON string concatenation with Jackson ObjectNode
construction and ObjectMapper serialization. Use BigInteger for call
IDs to properly represent unsigned 64-bit values in JSON.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add subscribeCallEvents command for opt-in call event notifications

Call events are no longer subscribed by default. JSON-RPC clients must
explicitly call subscribeCallEvents to receive callEvent notifications
and enable incoming call handling. This avoids sending unwanted call
events to clients that don't use voice calling.

Also adds unsubscribeCallEvents for cleanup, idempotent subscription
guard, and updates CALL_TUNNEL.md to document the subscription step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Replace Unix socket with stdin/stdout for tunnel communication

Use the tunnel subprocess' stdin for sending control messages and
stdout for receiving control events, instead of a separate Unix
domain socket. This eliminates:
- Temporary directory creation (/tmp/sc-<random>/)
- Socket path and auth token in config JSON
- Connection retry loop (50x at 200ms)
- Auth message handshake
- Socket cleanup on call end

The tunnel's stderr is captured separately for logging. Config JSON
is written as the first line on stdin, followed by control messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 22:07:16 +02:00
Kevin Kickartz-Grabowsky
59a0bd87cd
Add --voice-note flag for send command (#1973)
Add support for marking audio attachments as voice notes when sending
messages. Voice notes are displayed inline with a play button in Signal
clients, rather than as file attachments.

This addresses a longstanding TODO in AttachmentUtils.java and resolves
the feature request in #1601.

Changes:
- Add 'voiceNote' field to Message record
- Pass voiceNote flag through AttachmentHelper to AttachmentUtils
- Add .withVoiceNote() to SignalServiceAttachmentStream builder
- Add --voice-note CLI argument to SendCommand
- Support voiceNote parameter in JSON-RPC mode

Usage:
  signal-cli send -a audio.m4a --voice-note +1234567890

JSON-RPC:
  {"method":"send","params":{"attachment":"audio.m4a","voiceNote":true,...}}

Closes #1601
2026-03-28 12:28:13 +01:00
AsamK
c94da00212 Update libsignal-service 2026-03-21 22:32:38 +01:00
AsamK
36649a8526 Update CONTRIBUTING.md 2026-03-21 22:04:48 +01:00
AsamK
3297acd3f4 Update gradle 2026-03-21 22:04:48 +01:00
AsamK
3e5caf9284 Prepare next release 2026-03-21 22:04:48 +01:00
dependabot[bot]
53ae9e4266
Bump rustls-webpki from 0.103.9 to 0.103.10 in /client (#1982)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.9 to 0.103.10.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.9...v/0.103.10)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 16:37:53 +01:00
joeykrim
313f5392ef
Fix updateGroup --admin and --remove-member silent failures (#1970)
Fix type mismatch in retainAll() calls that cause updateGroup --admin
and --remove-member to silently do nothing.

retainAll() compared Set<RecipientId> against Collection<GroupMemberInfo>,
which always evaluates to false (different types), emptying the set.

Replace group.getMembers() with group.getMemberRecipientIds() in all
three affected locations.

Fixes: updateGroup --admin silently failing to promote members
Fixes: updateGroup --remove-member silently failing to remove members

Co-authored-by: joey <joey@Mac-Studio.local>
2026-03-13 08:08:02 +01:00
moppman
7d89375d3a
Update GraalVM metadata (#1966)
Add isArchived from #1957
2026-03-09 19:10:36 +01:00
Zachary Johnson
db0f660d08
enforce poll options between 1 and 100 characters; update documentation (#1965)
pre-PR cleanup
2026-03-09 19:09:58 +01:00
AsamK
b498d2050a Bump version to 0.14.1 2026-03-08 12:55:39 +01:00
AsamK
27a722dc75 Add flags to update group member labels 2026-03-08 12:23:26 +01:00
AsamK
7014f629fe Show member labels in listGroups command 2026-03-08 11:55:19 +01:00
AsamK
30b57bdb3d Update graalvm build tools 2026-03-08 08:41:44 +01:00
AsamK
b94162afbc Enable spqr capability 2026-03-07 15:58:01 +01:00
AsamK
2885ffeee8 Prepare next release 2026-03-03 17:50:00 +01:00
moppman
6071291f16
Expose a chat's isArchived status in JSON output (#1957)
Closes #1955
2026-03-03 12:46:34 +01:00
Zachary Johnson
37b8a4a996 enforce poll choices are between 2 and 10 2026-03-03 08:37:10 +01:00
AsamK
af56a28b94 Fix client folder name 2026-03-01 10:30:02 +01:00
AsamK
dfc7e3b495 Bump version to 0.14.0 2026-03-01 10:14:45 +01:00
AsamK
e9114ae8fc Add signal-cli-client to release and create container 2026-03-01 10:14:45 +01:00
AsamK
f77a74d93f Fix optional name parameter in client link command 2026-03-01 10:14:45 +01:00
AsamK
5cda87ee0e Fix native access warning in native build 2026-03-01 10:14:45 +01:00
AsamK
7384407823 Update man page 2026-03-01 09:50:22 +01:00
AsamK
7fa56a37fd Update CI actions 2026-03-01 09:29:19 +01:00
AsamK
8fcd953ece Always download long text attachments and use them as message body
Fixes #1901
2026-03-01 09:29:19 +01:00
AsamK
775236efc3 Update tests 2026-02-28 14:05:55 +01:00
AsamK
4b8dec26a9 Downgrade jackson library 2026-02-28 13:45:56 +01:00
AsamK
d94e05c38c Update test file 2026-02-28 13:45:56 +01:00
AsamK
1bbf98fac0 Update dependencies 2026-02-28 13:45:30 +01:00
AsamK
c70515035f Add missing flags to jsonrpc client 2026-02-28 13:45:30 +01:00
AsamK
a9d235b7f1 Add new commands to jsonrpc client 2026-02-28 13:45:30 +01:00
AsamK
92ded3fdf2 Fix incorrect cli definition 2026-02-28 13:45:30 +01:00
AsamK
aa1ed9e233 Add support for sending adminDelete messages 2026-02-28 11:59:31 +01:00
AsamK
3b6c199b1d Add pinMessage and unpinMessage commands
Closes #1923
2026-02-28 11:50:44 +01:00
AsamK
6d22ceef24 Support receiving admin delete messages 2026-02-28 11:30:33 +01:00
AsamK
54ff59737e Update libsignal-service-java
Fixes #1937
2026-02-28 11:14:53 +01:00
AsamK
516a37ba69 Fix accepting group invite using PNI
Fixes #1841
2026-02-28 10:15:05 +01:00
AsamK
6a6bebd503 Add support for receiving pin/unpin messages
Related #1923
2026-02-28 09:37:29 +01:00
AsamK
f9cbfa6d6c Invert urgent boolean in Message to be consistent 2026-02-25 21:43:52 +01:00
Kai Kozlov
d4b3816c5d
Add --no-urgent flag to send command (#1933)
* Add --no-push flag to send command

Expose the server's `urgent` parameter so callers can skip sending a
push notification (FCM/APNs) to the recipient. The message is still
delivered in real-time over WebSocket if the recipient's app is active.

The flag is added to the Message record (following the same pattern as
viewOnce) and threaded through ManagerImpl and SendHelper, keeping the
Manager interface unchanged.

* Rename --no-push flag to --no-urgent

Align with the protocol naming as suggested by the maintainer.
The flag controls the 'urgent' parameter on the server request.
2026-02-25 21:42:51 +01:00
AsamK
4a35d47515 Set explicit console colors for qr code 2026-02-25 21:37:41 +01:00
AsamK
52d4d61e2b Improve aci/pni handling in storage contact sync 2026-02-25 21:36:10 +01:00
AsamK
5bff902394 Configure signal service logger 2026-02-25 21:02:22 +01:00
AsamK
f33eb86335 Fix remote updates of unregistered contacts 2026-02-25 20:23:44 +01:00
AsamK
2ea26b9d1b Load recipient profiles in listContacts command if required 2026-02-25 20:08:23 +01:00
AsamK
10fa3e1619 Load unregistered_timestamp for recipient 2026-02-25 20:08:01 +01:00
AsamK
956e17c81c Improve aci/pni comparison in contact record processor 2026-02-25 19:49:29 +01:00
AsamK
6f749352d8 Split unregistered recipients when loading profile fails with 404 2026-02-25 19:48:58 +01:00
AsamK
5f3f6c071b Update libsignal-service-java
Fixes #1931
2026-02-25 00:50:20 +01:00
Karel Vervaeke
5795d43d0d print qr code when producing a device link uri, but only if System.console is not null 2026-02-19 18:35:04 +01:00
Denys Filonenko
de42f55e37 Fix typo in SendPollCreateCommand.java
Fix typo in error message, when options < 2
2026-02-16 18:22:38 +01:00
AsamK
7e93a15204 Add missing dbus reachability metadata
Fixes #1925
2026-02-15 14:29:13 +01:00
AsamK
715f819c3e Move graalvm reachability-metadata to resources 2026-02-15 14:07:47 +01:00
AsamK
345de8fb5d Update reachability metadata 2026-02-14 15:20:12 +01:00
AsamK
667a47c03a Update dependencies 2026-02-14 13:15:37 +01:00
AsamK
5dd5e304bd Update gradle wrapper 2026-02-14 12:54:45 +01:00
AsamK
60a1589616 Use new graalvm metadata format 2026-02-14 12:54:29 +01:00
AsamK
4d1d28672d Refactor message receive 2026-02-14 12:15:27 +01:00
Brian (bex) Exelbierd
fefca7d837 Add --ignore-avatars and --ignore-stickers CLI flags
Implement two new CLI flags to disable downloading avatars and sticker
packs during message reception, following the existing pattern of
--ignore-attachments and --ignore-stories flags.

Changes:
- Add --ignore-avatars and --ignore-stickers flags to ReceiveCommand,
  DaemonCommand, and JsonRpcDispatcherCommand
- Extend ReceiveConfig record with ignoreAvatars and ignoreStickers
  fields
- Pass ignoreAvatars as explicit boolean parameter to ProfileHelper,
  SyncHelper, and GroupHelper methods (per maintainer feedback)
- Gate avatar downloads in ProfileHelper (profile avatars), SyncHelper
  (contact avatars), and GroupHelper (group avatars for V1 and V2)
- Gate sticker pack downloads in IncomingMessageHandler for both
  direct sticker messages and sync sticker pack operations
- Update handleSignalServiceDataMessage and handleSyncMessage to pass
  full ReceiveConfig instead of individual boolean flags
- Update man page (signal-cli.1.adoc) with flag documentation
- Add entries to CHANGELOG.md

When these flags are set, the respective content is not downloaded
during message reception. Metadata (avatar paths, sticker pack IDs)
is still stored, and existing FileNotFoundException handling will
surface if content is later requested but wasn't downloaded.

Fixes #1903
Closes #1904
2026-02-14 12:15:13 +01:00
AsamK
d9f5a573cd Update graalvm jni config
Fixes #1919
2026-02-06 19:26:07 +01:00
AsamK
4c77cde9da Bump version to 0.13.24 2026-02-05 21:35:06 +01:00
AsamK
658b098c3a Update graalvm config 2026-02-05 17:57:41 +01:00
AsamK
70644ba31b Update libsignal-service 2026-02-05 17:52:11 +01:00
dependabot[bot]
06a706d070 Bump bytes from 1.10.1 to 1.11.1 in /client
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.1...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-04 10:56:09 +01:00
AsamK
14297986f2 Use SequencedCollection instead of List 2026-01-24 17:24:27 +01:00
AsamK
0bd4d554d8 Use virtual threads 2026-01-24 17:24:27 +01:00
AsamK
32c8d4f801 Update to java 25 2026-01-24 15:38:02 +01:00
AsamK
82abc20871 Remove deprecated functionality 2026-01-24 15:37:00 +01:00
AsamK
e8ab01f665 Fix use of deprecated API in plugin 2026-01-24 15:20:55 +01:00
AsamK
dee557a9ad Prepare next release 2026-01-24 15:09:23 +01:00
AsamK
f1fa2eba1d Bump version to 0.13.23 2026-01-24 14:21:40 +01:00
AsamK
ccd58bbf23 Use graalvm-community 2026-01-24 14:21:40 +01:00
AsamK
54700d9cd0 Update libsignal-service 2026-01-24 12:36:29 +01:00
AsamK
984ea47f9d Update gradle 2026-01-24 12:36:29 +01:00
AsamK
9458972d15 Update dependencies 2026-01-24 12:36:29 +01:00
AsamK
8eb9662694 Implement new updateDevice command to update device names
Fixes #1906
2026-01-23 20:00:25 +01:00
Benjamin Loison
a9be1aa608 Remove an unnecessary space in README.md 2026-01-23 19:37:26 +01:00
AsamK
3af9dff0ed Update libsignal-service 2025-12-10 19:49:57 +01:00
AsamK
c5e4b250b8 Parse binary aci/pni in storage records 2025-12-10 18:07:25 +01:00
AsamK
5fafa24974 Create correct nickname record if nickname is empty 2025-12-08 19:09:57 +01:00
AsamK
b26c521930 Extend username documentation
Fixes #1886
2025-12-08 17:44:06 +01:00
AsamK
eb52380ecf Remove now unnecessary check for primary device from updateContact
Fixes #1880
2025-12-08 17:08:38 +01:00
AsamK
f1de69d7ff Set same toolchain in lib module as in main module 2025-12-07 19:58:25 +01:00
AsamK
ba2214d8c7 Silence unchecked error 2025-12-07 19:47:53 +01:00
AsamK
fca4d7459c Clear verification sessionId after registration/changeNumber
Fixes #1882
2025-12-05 21:28:36 +01:00
AsamK
ad2338b898 Add documentation for polls
Closes #1885
2025-12-05 21:20:53 +01:00
AsamK
c237f98044 Mark legacy accounts without ACI as unregistered 2025-12-05 20:08:07 +01:00
AsamK
87945ac506 Ignore authorization failed errors in multi account mode
Fixes #1884
2025-12-05 20:08:07 +01:00
Artemii Bigdan
11d96c894d Print user number in UserAlreadyExistsException DBus error
Currently, the error looks like "UserAlreadyExistsException null", which does not give enough information to handle this situation in the multi-account daemon mode.

This change adds a phone number to the error message resolving my issue and achieving functional parity with CLI interface.
2025-12-03 18:39:48 +01:00
AsamK
8dcc16d640 Update README.md 2025-11-19 20:31:54 +01:00
Ankur Heramb Joshi
9a1ddd0d41 Revise installation commands in README.md
Updated installation instructions for signal-cli to use the latest version dynamically.

1] Uncompressed signal-cli-"${VERSION}".tar.gz directly produces executable named signal-cli and NOT named signal-cli-"${VERSION}" , thus was getting error in installation. 
This change should rectify the issue.

2] Also finding latest release version is simplified
2025-11-19 20:31:54 +01:00
AsamK
b8bb58b083 Prepare next release 2025-11-14 20:38:10 +01:00
AsamK
7feba3c8c7 Bump version to 0.13.22 2025-11-14 19:58:26 +01:00
AsamK
dae44c81d2 Update dependencies 2025-11-14 19:54:40 +01:00
AsamK
c2ac74a6be Update libsignal-service 2025-11-14 19:52:32 +01:00
AsamK
62811d92c5 Update gradle 2025-11-14 19:23:52 +01:00
AsamK
feaee2bfe1 Add support for receiving and sending polls 2025-11-02 20:50:18 +01:00
AsamK
91701f609a Update libsignal-service 2025-11-02 18:50:29 +01:00
AsamK
d27aea2220 Add --notiy-self to sendReaction command 2025-11-02 18:49:26 +01:00
AsamK
e832a57f35 Remove old svr2 mrenclave 2025-11-02 09:31:48 +01:00
AsamK
13e6445498 Regard pin set/remove as successful if at least one svr version worked
Fixes #1868
2025-11-02 09:31:23 +01:00
AsamK
552ce6b06d Fix device link URI parsing
Fixes #1864
2025-10-31 21:48:26 +01:00
AsamK
c3bce6730d Fix timeout handling for receive command
Fixes #1865
2025-10-26 13:37:59 +01:00
AsamK
5d2daedba3 Prepare next release 2025-10-26 13:15:42 +01:00
AsamK
6ee043449b Bump version to 0.13.21 2025-10-25 13:16:40 +02:00
AsamK
5510d1e20d Update dependencies 2025-10-25 11:51:40 +02:00
AsamK
cad61dae37 Update libsignal-service 2025-10-25 11:31:48 +02:00
AsamK
1fcedca0f1 Improve error message for missing verification session
Fixes #1854
2025-10-24 10:09:27 +02:00
AsamK
7b08225c57 Prevent invalid profile keys from being processed for storage service 2025-10-24 09:52:43 +02:00
AsamK
06fd350e9c Update libsignal-service-java
Fixes #1859
2025-10-16 14:34:47 +02:00
AsamK
9ee747271a Prepare next release 2025-10-16 14:34:47 +02:00
Bradley Erickson
ef3c1ea0e7 add missing isExpirationUpdate field to JSON serialization 2025-10-10 07:08:24 +02:00
AsamK
3b784aa32b Bump version to 0.13.20 2025-09-23 20:36:35 +02:00
AsamK
05a9635dc4 Update graalvm config 2025-09-23 20:36:35 +02:00
AsamK
6abe3c7a06 Fix shutdown delay regression in daemon mode 2025-09-23 18:33:05 +02:00
AsamK
98b46353ea Improve error message for missing and invalid account parameter 2025-09-23 18:32:38 +02:00
AsamK
af0f4a6b01 Try to load manager in MultiAccountManager if it doesn't exist yet 2025-09-23 18:29:51 +02:00
AntonKun
96ec6624c5 Updated the JSON-RPC handler on a file lock overlap 2025-09-23 17:48:15 +02:00
AsamK
9f1ae5f4db Replace deprecated API 2025-09-19 23:29:54 +02:00
AsamK
1d06f042e1 Ignore unchecked warning due to kotlin incompatibility 2025-09-19 23:29:38 +02:00
AsamK
761bcdd064 Update gradle wrapper 2025-09-19 23:13:06 +02:00
AsamK
c108d59cfc Disable old fallback to create v1 group
Fixes #1850
2025-09-19 23:11:22 +02:00
AsamK
429c4dca07 Update CI build to Java 25 2025-09-18 21:45:15 +02:00
AsamK
52d818be45 Close all managers in parallel in multi account mode 2025-09-18 21:44:58 +02:00
AsamK
21a546d2bc Refactor timeout handling in receive helper 2025-09-18 21:43:54 +02:00
AsamK
e65fee0efb Update libsignal-service-java
Fixes #1772
2025-09-18 20:55:21 +02:00
AsamK
29dc873535 Add new unidentified sender trust root 2025-09-18 19:33:02 +02:00
AsamK
69d892e5b1 Fix sending group message to legacy targets without group send endorsements
Fixes #1849
2025-09-18 18:42:43 +02:00
AsamK
3be0aefa20 Prepare next release 2025-09-18 18:42:43 +02:00
AsamK
a399666fba Bump version to 0.13.19 2025-09-15 11:43:12 +02:00
AsamK
1af03e3e16 Fix hiding contacts 2025-09-15 11:34:27 +02:00
AsamK
7a82283762 Update jsonrpc man page 2025-09-15 11:06:06 +02:00
AsamK
fb92bc3894 Update dependencies 2025-09-14 19:28:56 +02:00
AsamK
a22af8303a Force a group refresh when using listGroups command with groupId
Fixes #1802
2025-09-14 19:28:56 +02:00
AsamK
371dc06842 Handle unregistered username correctly when sending message
Fixes #1824
2025-09-14 19:28:56 +02:00
AsamK
e60b80effc Prevent splitting UTF-8 chars when reading from stdin
Fixes #1827
2025-09-14 19:28:56 +02:00
AsamK
b9008f0b66 Handle missing storage manifest version correctly
Fixes #1829
2025-09-14 19:28:56 +02:00
AsamK
2239231601 Remove old SVR2 mrenclave
Fixes #1837
2025-09-14 19:28:56 +02:00
AsamK
9a45138d8b Store last receive timestamp only every four hours
Fixes #1832
2025-09-14 19:28:56 +02:00
AsamK
edd52a97b4 Replace INSERT OR REPLACE with INSERT ON CONFLICT DO UPDATE
The latter doesn't cause foreign key delete cascades
2025-09-14 19:28:56 +02:00
AsamK
f469a4d166 Implement group send endorsements 2025-09-14 19:28:56 +02:00
AsamK
b2076cfc12 Fix hide contact functionality to not delete recipient 2025-09-14 19:28:56 +02:00
AsamK
991ff04196 Refactor UnidentifiedAccessHelper 2025-09-14 19:28:56 +02:00
AsamK
597f0368cf Refactor group send 2025-09-14 19:28:56 +02:00
AsamK
c37d8c6ce1 Update libsignal-service 2025-09-14 19:28:56 +02:00
d1vbyz3r0
61bc30eb43 Fix typo for group-id CLI parameter 2025-09-11 18:15:30 +02:00
AsamK
f6d81e3c05 Update gradle 2025-08-17 17:35:59 +02:00
AsamK
42f10670b6 Replace deprecated groovy utils 2025-08-17 17:35:14 +02:00
AsamK
b453d7a0b9 Add new svr2 mrenclave 2025-08-02 12:05:01 +02:00
AsamK
f9a36c6e04 Fix send parameters to be all camel case
Fixes #1814
2025-07-16 20:59:26 +02:00
AsamK
be48afb2b5 Fix container build 2025-07-16 20:56:02 +02:00
AsamK
a0960fcabd Prepare next release 2025-07-16 20:55:50 +02:00
AsamK
dbc454ba9e Bump version to 0.13.18 2025-07-16 19:40:10 +02:00
AsamK
2225e69277 Update sqlite-jdbc 2025-07-16 19:37:03 +02:00
AsamK
783201d12e Fix incorrect error message 2025-07-16 19:17:21 +02:00
AsamK
3e981d66e9 Fix null pointer regression 2025-07-14 18:52:41 +02:00
AsamK
7c7fc76a64 Add support for sending view once messages
Closes #1812
2025-07-14 16:42:06 +02:00
AsamK
c924d5c03a Update libsignal-service-java 2025-07-14 16:21:47 +02:00
AsamK
dc787be17b Build rust json-rpc client in CI 2025-07-12 11:57:23 +02:00
AsamK
3d4070a139 Compile UnixStream support only on unix systems 2025-07-12 11:42:12 +02:00
AsamK
dbdff83132 Update README
Fixes #1803
2025-07-12 11:09:24 +02:00
AsamK
4ce194afe2 Add missing username parameter to getUserStatus command in json-rpc client 2025-07-12 11:03:54 +02:00
AsamK
ca33249170 Handle rate limit exception correctly when querying usernames
Fixes #1797
2025-07-12 11:03:28 +02:00
AsamK
a96626c468 Update to rust 2024 edition 2025-07-12 10:16:57 +02:00
AsamK
d54be747da Remove unused dependency 2025-07-12 10:15:27 +02:00
AsamK
ff846bc678 Fix clippy warnings 2025-07-12 10:05:57 +02:00
AsamK
1b7f755590 Update dependencies 2025-07-12 10:05:14 +02:00
AsamK
887ed3bb44 Show better error message when sending fails due to missing pre keys 2025-07-08 17:35:17 +02:00
AsamK
3180eba836 Exit if account check fails at startup
Fixes #1804
2025-07-08 17:34:04 +02:00
AsamK
cb06cbdcca Shut down when dbus daemon connection goes away unexpectedly
Fixes #1800
2025-06-29 11:22:30 +02:00
AsamK
069325af47 Extend shutdown request with optional error 2025-06-29 11:22:30 +02:00
AsamK
e7ca02f1fb Prepare next release 2025-06-29 11:22:30 +02:00
AsamK
fa9bb3c210 Bump version to 0.13.17 2025-06-28 14:57:20 +02:00
AsamK
e6113d4d96 Update libsignal-service-java 2025-06-28 14:35:56 +02:00
AsamK
6cc3a6f561 Update dependencies 2025-06-25 00:20:42 +02:00
AsamK
70c79eac01 Keep all unhandled fields of remote storage record
Fixes #1792
2025-06-24 23:13:00 +02:00
AsamK
5dc66f839d Close attachment input streams after upload
Fixes #1790
2025-06-10 19:36:52 +02:00
AsamK
a0d5744c49 Improve behavior when pin data doesn't exist on the server 2025-06-08 16:22:03 +02:00
AsamK
6b60a6d5a5 Fix NPR when loading an inactive group
Fixes #1786
2025-06-08 14:48:25 +02:00
AsamK
0257344940 Prepare next release 2025-06-08 14:47:20 +02:00
AsamK
17cd99be59 Bump version to 0.13.16 2025-06-07 16:58:00 +02:00
AsamK
2f8328847c Update dependencies 2025-06-07 16:14:55 +02:00
AsamK
7e9727aa38 Update tests 2025-06-07 16:14:55 +02:00
AsamK
bf87fcc652 Ensure messages are created with a unique timestamp
Fixes #1783
2025-06-03 22:22:51 +02:00
AsamK
6b46314eab Update dependencies 2025-06-03 21:59:38 +02:00
AsamK
e89803464b Update libsignal-service 2025-06-01 21:51:03 +02:00
AsamK
a9bb8d9aae Update gradle 2025-06-01 16:11:21 +02:00
AsamK
74909408c4 Add missing reflect config
Fixes #1768
2025-05-10 10:18:03 +02:00
AsamK
bb124a922d Prepare next release 2025-05-08 22:56:55 +02:00
AsamK
56e11d0857 Update codeql v3 2025-05-08 22:55:26 +02:00
AsamK
d0d0021f57 Bump version to 0.13.15 2025-05-08 21:49:51 +02:00
AsamK
7aafb05995 Update dependencies 2025-05-08 21:31:17 +02:00
AsamK
e594f3b237 Update libsignal-service 2025-05-08 21:31:17 +02:00
AsamK
bb86830a61 Add compatibility flag for graalvm native build 2025-05-08 20:42:13 +02:00
AsamK
bcc1eadc7d Remove unused e164 field from account record 2025-05-08 20:36:27 +02:00
AsamK
4fd9e55c3c Enable native access required by Java 24
Fixes #1765
2025-05-08 20:04:49 +02:00
AsamK
a2900085c9 Use java 24 in CI 2025-05-08 19:37:57 +02:00
AsamK
5e11cf1c50 Update gradle 2025-05-08 19:37:41 +02:00
AsamK
4e455d85d6 Add more logging to register 2025-04-26 09:04:05 +02:00
AsamK
1e685c7cab Extend merge/split logging 2025-04-09 20:44:10 +02:00
AsamK
ce813e4529 Update client dependencies 2025-04-08 16:24:57 +02:00
AsamK
bd7948e246 Prepare next release 2025-04-08 16:24:30 +02:00
AsamK
b998f322f5 Bump version to 0.13.14 2025-04-06 20:10:46 +02:00
AsamK
db2182aa7d Update libsignal-service 2025-04-06 20:02:02 +02:00
AsamK
69a9b30732 Update libsignal-service 2025-03-31 14:56:56 +02:00
AsamK
3dc8844cb4 Update libraries 2025-03-31 09:19:20 +02:00
AsamK
adb6787d5b Refresh prekeys when receiving message with invalid key id 2025-03-31 09:11:28 +02:00
AsamK
14b07be0dc Always renew session when failing to decrypt message 2025-03-31 09:11:05 +02:00
AsamK
6befda7ef1 Update graalvm build tools 2025-03-22 10:55:16 +01:00
AsamK
67302eb9c3 Replace cached envelopes when moving
Fixes #1730
2025-03-18 18:20:44 +01:00
AsamK
f18015ff2e f 2025-03-18 18:13:38 +01:00
AsamK
1295ef69ca Use record patterns 2025-03-16 22:07:29 +01:00
AsamK
f26a0d2891 Update libsignal-service 2025-03-16 22:06:58 +01:00
AsamK
2b150112ff Remove previous prekeys when importing legacy prekeys 2025-03-16 12:22:21 +01:00
AsamK
7aede7c17f Remove previous prekeys when importing legacy prekeys 2025-03-16 12:18:59 +01:00
AsamK
b92cbc6a7c Exclude libsignal-client testing libraries 2025-03-04 10:04:34 +01:00
AsamK
68b7416e57 Update gradle wrapper 2025-03-04 08:32:56 +01:00
AsamK
4feba68afd Replace deprecated gradle api 2025-03-04 08:29:05 +01:00
AsamK
4eb34c7a93 Update apt repository before installing packages 2025-02-28 09:46:45 +01:00
AsamK
26fd3e379a Prepare next release 2025-02-28 09:46:28 +01:00
AsamK
93d281e712 Bump version to 0.13.13 2025-02-28 09:36:27 +01:00
AsamK
985af6e445 Update libsignal-service 2025-02-28 09:32:17 +01:00
AsamK
5693d871f7 Update dependencies 2025-02-28 09:32:17 +01:00
AsamK
dba8cf7a6f Update reflect-config 2025-02-27 18:01:21 +01:00
AsamK
141d3326ab Add in-memory cache to KeyValueStore 2025-02-27 17:21:31 +01:00
AsamK
d3d2caac5a Tweak hikari config 2025-02-27 17:21:31 +01:00
AsamK
e1f4dae5c2 Show better error message when receiving an empty JSON RPC line
Fixes #1715
2025-02-27 11:39:31 +01:00
AsamK
cf5c943127 Remove legacy SVR2 enclave 2025-02-27 11:34:23 +01:00
AsamK
ed79e0b377 Check if required quote-author parameter is missing
Fixes #1716
2025-02-27 11:14:54 +01:00
Enguerran P.
a089a5ef04 Update README > landline procedure
Update landline (`--voice`) registration procedure.

This closes #1666 (🤘)
2025-02-07 23:16:46 +01:00
AsamK
90145655f4 Update CHANGELOG.md 2025-02-07 19:15:55 +01:00
AsamK
3cd07ae9cd Set libsignal network proxy to match java proxy
Fixes #1523
2025-02-07 18:30:10 +01:00
AsamK
8aa71c132f Fix log message 2025-02-07 18:30:10 +01:00
AsamK
b579935846 Update README 2025-02-07 18:30:10 +01:00
AsamK
dfa886fae9 Update dependencies 2025-02-07 18:30:10 +01:00
AsamK
f04f789231 Update gradle action 2025-01-31 16:45:47 +01:00
AsamK
a6ec71dc31 Add --mobilecoin-address as alias to updateProfile
Closes #1638
2025-01-30 20:18:07 +01:00
AsamK
47d65586cd Improve handling of unknown storage records
Fixes #1696
2025-01-30 19:57:44 +01:00
AsamK
b8d8413a22 Fix creating builder from contact
Fixes #1678
2025-01-23 17:12:40 +01:00
AsamK
5e16123632 Extend updateContact command with nick given/family name and note 2025-01-23 17:11:33 +01:00
AsamK
d57442bd2a Prepare next release 2025-01-19 13:26:12 +01:00
AsamK
70313c45a9 Update reflect-config.json
Fixes #1686
2025-01-19 13:25:08 +01:00
AsamK
f14c204764 Bump version to 0.13.12 2025-01-18 20:53:46 +01:00
AsamK
71d3b83a1c Update user agent 2025-01-18 20:24:01 +01:00
AsamK
148bf7dee2 Add man page to build tar file
Fixes #1660
2025-01-18 20:07:41 +01:00
AsamK
2d1ba6b4ca Extend man Makefile 2025-01-18 19:56:23 +01:00
AsamK
055a8ee8b9 Add general description for global/subcommand arguments
Fixes #1649
2025-01-18 16:59:25 +01:00
AsamK
407a20d4bb Update client dependencies 2025-01-18 16:53:09 +01:00
AsamK
05cd6aee6a Add version and group to all modules 2025-01-18 16:30:28 +01:00
AsamK
a1378507b2 Rename lib to libsignal-cli 2025-01-18 16:30:07 +01:00
AsamK
78cd0b13de Update dependencies 2025-01-18 16:08:43 +01:00
AsamK
c25468a71e Update reflect-config.json 2025-01-17 16:09:39 +01:00
AsamK
a5d2e1ea23 Use getRawQuery to prevent double decoding the query
Fixes #1682
2025-01-17 16:03:00 +01:00
AsamK
6acf16ef4e Improve tests 2025-01-14 23:12:45 +01:00
AsamK
e11e093020 Enable sqlite WAL journal_mode
Related #1670
2025-01-14 22:35:45 +01:00
AsamK
74c2604dc8 Set sqlite PRAGMA via Url 2025-01-14 22:31:36 +01:00
AsamK
e4af0be0ad Use existing connection to read configuration during storage sync 2025-01-14 21:33:12 +01:00
AsamK
5ac5938c8b Reduce log level of invalid sync contact address
Fixes #1663
2025-01-14 20:41:04 +01:00
AsamK
94269744ad Improve final address when merging recipients 2025-01-14 20:30:06 +01:00
AsamK
7a25ae5b9c Fix reading nickname from storage record
Fixes #1664
2025-01-14 20:30:06 +01:00
AsamK
cbd92654cf Store pni correctly in storage record 2025-01-14 20:30:06 +01:00
AsamK
bd95373a70 Parse unregisteredAtTimestamp correctly
Fixes #1651
Fixes #1646
2025-01-14 20:30:06 +01:00
AsamK
d982633215 Update dependencies 2025-01-14 20:30:06 +01:00
AsamK
f91ca82902 Prepare next release 2025-01-14 20:30:06 +01:00
Slayer
c55ee85c5c Fixing RW connections deadlock on SQLite
Without this change we're getting a connection in the same thread we hold one already.
2025-01-14 12:21:12 +01:00
AsamK
a3776c88bd Bump version to 0.13.11 2024-12-26 20:31:28 +01:00
AsamK
4a781656b4 Update dependencies 2024-12-26 20:23:08 +01:00
AsamK
11d38f29ef Improve splitting of long message bodies 2024-12-26 20:15:19 +01:00
AsamK
22a0ff976a Update libsignal-service 2024-12-26 20:14:53 +01:00
AsamK
c05b47e4d0 Delete storage id of unregistered recipients after remote update 2024-12-25 17:19:33 +01:00
AsamK
ac145e6a27 Ignore destination if it's an empty uuid
Fixes #1643
2024-12-25 16:23:36 +01:00
AsamK
f00b8523d9 Update dependencies 2024-12-15 21:18:02 +01:00
AsamK
c3f8d68ceb Create account entropy pool instead of master key 2024-12-15 21:14:40 +01:00
AsamK
9d92a3e06b Update libsignal-service 2024-12-15 21:13:59 +01:00
AsamK
f2df600d38 Update CHANGELOG.md
Fixes #1641
2024-12-01 10:12:46 +01:00
AsamK
24d344fda4 Prepare next release 2024-11-30 16:32:31 +01:00
AsamK
0a296e77a0 Bump version to 0.13.10 2024-11-30 16:14:53 +01:00
AsamK
ba147a48f8 Update reflect config 2024-11-30 15:45:26 +01:00
AsamK
77a5c454b7 Improve documentation for recipient arguments
Fixes #1639
2024-11-29 21:10:46 +01:00
AsamK
2c68b5a9e1 Add support for using PNI as recipient 2024-11-29 21:10:46 +01:00
AsamK
68c9d84d19 Update libsignal-service
Fixes #1633
2024-11-24 13:04:04 +01:00
AsamK
fe752e0c79 Add simplification for single recipient reactions 2024-11-24 11:51:54 +01:00
AsamK
26b5a4c582 Small code improvement 2024-11-23 23:57:23 +01:00
AsamK
10ee295ea3 Fix receiving shared contacts in graalvm mode
Fixes #1629
2024-11-23 23:57:23 +01:00
AsamK
6a5ea5fc01 Workaround issue with invalid address in recipient store 2024-11-23 23:57:23 +01:00
AsamK
ff6cb5262a Update libsignal-service
Support for storage encryption v2 and account entropy pool

Fixes #1632
2024-11-23 23:57:23 +01:00
AsamK
f2005593ec Reformat files 2024-11-23 22:35:06 +01:00
AsamK
3533500b73 Update dependencies 2024-11-23 22:35:06 +01:00
AsamK
e5251ae158 Switch to using toml version catalogs 2024-11-21 21:17:33 +01:00
AsamK
a5e272be3f Prepare next release 2024-10-28 23:45:03 +01:00
AsamK
277652e3f2 Bump version to 0.13.9 2024-10-28 23:37:26 +01:00
AsamK
181086aefe Update graalvm config
Fixes #1612
2024-10-28 10:58:14 +01:00
AsamK
a1d552698a Fix verifyAccount
Fixes #1614
2024-10-28 10:42:26 +01:00
AsamK
1b959608c3 Simplify verification code request 2024-10-28 10:35:18 +01:00
AsamK
acddef6e35 Prepare next release 2024-10-26 15:02:28 +02:00
AsamK
0e77870b59 Bump version to 0.13.7 2024-10-26 15:01:05 +02:00
AsamK
0a287b0b3e Reformat files 2024-10-26 13:10:33 +02:00
AsamK
5a4f4ba6db Implement message expiration timer version
Fixes #1605
2024-10-26 13:08:21 +02:00
AsamK
5171107a29 Run CI with Java 23 2024-10-25 17:22:47 +02:00
AsamK
47e6fc1769 Update dependencies 2024-10-25 17:20:07 +02:00
AsamK
9afd4e4328 Update libsignal-service 2024-10-25 17:20:07 +02:00
Jailson Dias
eac2a47163 add group info on json message 2024-10-25 17:19:17 +02:00
Jailson Dias
5646f65195 add received and delivered timestamps on json message 2024-10-25 17:19:17 +02:00
AsamK
fab1b96c21 Upload text attachment also if there no other attachments
Fixes #1598
2024-09-29 09:43:51 +02:00
AsamK
91eacc18c2 Refactor attachment upload 2024-09-29 09:43:26 +02:00
AsamK
7f1fc932ad Add constant for MAX_MESSAGE_BODY_SIZE 2024-09-29 09:43:05 +02:00
AsamK
304c44064d Prepare next release 2024-09-28 22:14:26 +02:00
AsamK
946c483f35 Bump version to 0.13.7 2024-09-28 22:13:24 +02:00
AsamK
69d691f416 Update graalvm reflect-config
Fixes #1590
2024-09-28 20:41:58 +02:00
AsamK
a6ab8f7e80 Remove v2 suffix from secure value recovery 2024-09-28 20:41:58 +02:00
AsamK
19dc2d446b Update libsignal-service 2024-09-28 20:41:58 +02:00
AsamK
8524037900 Update gradle 2024-09-28 12:13:49 +02:00
AsamK
dbbc3fbd71 Remove unnecessary proto conversion 2024-09-24 17:28:24 +02:00
AsamK
c6e93126fa Fix truncating cdsi table
Fixes #1587
2024-09-12 23:09:20 +02:00
AsamK
352699c4b6 Update artifact CI actions 2024-09-11 17:50:01 +02:00
AsamK
bea2772491 Update graalvm buildtools 2024-09-10 19:16:42 +02:00
AsamK
bab8ddf35a Update slf4j 2024-09-09 18:31:47 +02:00
AsamK
1d5d16f57e Adapt signal_jni file names in graalvm config 2024-09-09 18:31:34 +02:00
AsamK
6f9e9e9302 Prepare next release 2024-09-08 19:24:18 +02:00
AsamK
20add0f27b Downgrade dbus-java 2024-09-08 19:24:18 +02:00
AsamK
eca3c6fa30 Replace deprecated DBusMap 2024-09-08 19:24:18 +02:00
AsamK
a0d1b081ff Bump version to 0.13.6 2024-09-08 18:59:22 +02:00
AsamK
d852c60c37 Update dependencies 2024-09-08 18:55:23 +02:00
AsamK
2b5451f74a Update client dependencies 2024-09-08 18:51:56 +02:00
AsamK
b322716215 Update CHANGELOG.md 2024-09-08 10:18:26 +02:00
AsamK
41726d339f Update libsignal-service-java 2024-09-08 10:18:17 +02:00
AsamK
52bb92dce4 Call getter instead of using cached instance variable 2024-09-08 10:15:02 +02:00
AsamK
65adfdd6f5 Add signal CDN 3 2024-09-08 10:15:02 +02:00
AsamK
d8b1a2fffe Fix stripping the correct identifiers when merging recipients 2024-09-08 09:24:51 +02:00
AsamK
ea436ecb64 Fix possible db dead lock
Fixes #1483
2024-09-08 09:22:52 +02:00
AsamK
82fbde4f19 Adapt code style 2024-09-08 09:00:39 +02:00
AsamK
19b15e68e4 Improve addDevice error message
Fixes #1573
2024-09-08 09:00:34 +02:00
AsamK
b51f849fe6 Send sync message for read/viewed receipt messages
Fixes #1570
2024-09-08 08:51:33 +02:00
AsamK
7cc0ef1c70 Improve error message and log output for failed jsonrpc commands 2024-09-08 08:30:31 +02:00
AsamK
485c4fd467 Improve daemon deprecation message 2024-09-08 08:30:08 +02:00
AsamK
bda395191b Don't set previousE164s param if cdsi token is empty
Fixes #1576
2024-09-08 08:29:54 +02:00
AsamK
cb129db95e Update libsignal-service 2024-08-20 17:39:39 +02:00
AsamK
130cee9906 Fix sending to groups with non sender key capable members
Regression in 0.13.5

Fixes #1568
2024-08-20 17:13:57 +02:00
AsamK
6bdc9a4b16 Update gradle 2024-08-20 17:10:03 +02:00
AsamK
a69a9b7b0e Add method to update group endorsements 2024-08-20 17:10:03 +02:00
AsamK
6ea373fbd5 Prepare next release 2024-08-20 17:10:03 +02:00
Ingmar Lippert
62da514850
Add more specificity to addDevice (#1561) 2024-08-20 16:59:52 +02:00
AsamK
fe3934171d Bump version to 0.13.5 2024-07-25 22:47:16 +02:00
AsamK
3d11221732 Update jackson 2024-07-25 22:43:55 +02:00
AsamK
e961488b1a Update man page 2024-07-25 22:40:46 +02:00
AsamK
2db3d3259e Use UploadSpec for attachment uploads 2024-07-25 22:31:35 +02:00
AsamK
e51b1ee23a Update libsignal-service 2024-07-25 16:27:24 +02:00
AsamK
5ff66728e3 Update libsignal-service 2024-06-26 15:38:55 +02:00
AsamK
baf7b74a61 Prepare next release 2024-06-06 11:15:02 +02:00
AsamK
c716f94649 Bump version to 0.13.4 2024-06-06 10:47:42 +02:00
AsamK
8b355918e8 Use jvm running gradle if it's compatible with targetCompatibility 2024-06-06 10:40:21 +02:00
AsamK
67012b40b1 Update user agent 2024-06-06 10:16:53 +02:00
AsamK
fd402b52c8 Update doc 2024-06-06 10:16:40 +02:00
AsamK
5a97b9e134 Update groups when using listGroups command
Fixes #1517
2024-06-06 10:07:20 +02:00
AsamK
17596795c2 Only store profile keys for group history if none is known yet 2024-06-06 10:07:20 +02:00
AsamK
10b9c264fd Update libsignal-service
Fixes #1530
2024-06-06 10:07:20 +02:00
AsamK
a2b002ac5e Add java 22 to CI 2024-06-06 09:12:40 +02:00
AsamK
6d764db114 Update dependencies 2024-06-02 13:26:17 +02:00
AsamK
777cfbb69f Update gradle wrapper 2024-06-02 13:25:09 +02:00
AsamK
9781c56571 Improve username update error message
Fixes #1535
2024-05-24 16:09:07 +02:00
AsamK
04cf54263e Fix getUserStatus command with only username parameter
Related #1535
2024-05-23 12:46:15 +02:00
AsamK
6baf0eac13 Fix type parsing in JSON RPC mode
Fixes #1533
2024-05-21 20:28:16 +02:00
AsamK
fb21a42cce Update graalvm build tools 2024-05-18 22:13:29 +02:00
AsamK
53d7e0f08b Handle all possible identifiers of a RecipientAddress
Fixes #1516
2024-05-17 18:02:05 +02:00
AsamK
8f756cd90c Update libsignal-service 2024-05-09 21:32:34 +02:00
Sebastian Scheibner
fb81bf1d05
Update README.md 2024-05-09 21:20:38 +02:00
AsamK
04726f005c Save account file after setting username link
Fixes #1515
2024-05-01 09:08:00 +02:00
AsamK
09e3e7f335 Rotate storageId after setting username 2024-05-01 09:07:39 +02:00
AsamK
90b1e4bc02 Delete username link when deleting username 2024-05-01 09:04:03 +02:00
AsamK
3f31f1a8a6 Update metainfo 2024-04-21 09:34:06 +02:00
dependabot[bot]
3cd8e323c9
Bump rustls from 0.21.10 to 0.21.11 in /client (#1511)
Bumps [rustls](https://github.com/rustls/rustls) from 0.21.10 to 0.21.11.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.21.10...v/0.21.11)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-20 13:53:36 +02:00
AsamK
7060faf5d3 Prepare next release 2024-04-19 19:25:27 +02:00
AsamK
c9f2cca024 Bump version to 0.13.3 2024-04-19 19:21:07 +02:00
AsamK
f0054372b8 Add timestamp to account file
Closes #1498
2024-04-19 17:25:43 +02:00
AsamK
0a82c51b79 Update dependencies 2024-04-19 17:17:32 +02:00
AsamK
cef83d962c Fix missing null check 2024-04-19 17:07:29 +02:00
AsamK
c1775913b9 Implement dbus support for listIdentities
Fixes #195
2024-04-19 17:07:18 +02:00
AsamK
e4c5144fbf Add more details to listContacts command
Fixes #1502
2024-04-17 21:26:16 +02:00
AsamK
8aeaf927e6 Add missing parts for new nick name and note columns 2024-04-17 21:21:28 +02:00
AsamK
7e0d4c9b89 Store profile phone number sharing mode and discoverable state 2024-04-17 21:08:09 +02:00
AsamK
71de8e63cc Cache newly created session record
Fixes #1481
2024-04-15 19:23:50 +02:00
AsamK
e456d06cb0 Add aci,pni to API RecipientAddress 2024-04-15 19:23:50 +02:00
AsamK
e0cd5b987e Add handling for new nickname and note fields 2024-04-15 19:23:50 +02:00
Stephen Brennan
e5ebb732cb
Document the unit of "start" and "length" for mentions and text styles (#1505)
The unit of UTF-16 code units is not necessarily obvious for users of
languages that index strings by Unicode code points. Provide a pointer
to an FAQ entry as well:

https://github.com/AsamK/signal-cli/wiki/FAQ#string-indexing-units

Closes #1504

Signed-off-by: Stephen Brennan <stephen@brennan.io>
2024-04-13 20:26:15 +02:00
AsamK
419beee29a Update libsignal-service-java 2024-04-06 14:04:56 +02:00
AsamK
d4e1f9b7f1 Remove unnecessary config field 2024-04-06 14:04:56 +02:00
AsamK
edce33ae15 Disable java 22 until gradle supports it 2024-04-06 14:04:56 +02:00
dependabot[bot]
95f9e18de2
Bump h2 from 0.3.24 to 0.3.26 in /client (#1501)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-06 14:04:48 +02:00
AsamK
95d4b7d012 Update gradle wrapper 2024-04-06 12:34:55 +02:00
AsamK
17c24b3ff2 Update libsignal-service-java 2024-03-27 22:58:13 +01:00
AsamK
be0e8ddd8a Add reregister to tests 2024-03-27 22:58:13 +01:00
AsamK
49cc9cd9f8 Update CHANGELOG.md 2024-03-23 15:00:02 +01:00
AsamK
c85c995fef Prepare next release 2024-03-23 10:34:45 +01:00
AsamK
dda23e76ac Bump version to 0.13.2 2024-03-23 09:57:15 +01:00
AsamK
95e70b9d15 Add Java 22 to CI 2024-03-23 09:50:59 +01:00
AsamK
abddf24752 Update user agent 2024-03-23 09:50:59 +01:00
AsamK
d356d92b5e Extend getUserStatus command for usernames 2024-03-22 10:54:42 +01:00
AsamK
8b4f377cf1 Update dependencies 2024-03-22 10:04:57 +01:00
dependabot[bot]
323a801600
Bump mio from 0.8.10 to 0.8.11 in /client (#1488)
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.10 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.10...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-09 13:08:46 +01:00
AsamK
3372992dc2 Add appstream metainfo file 2024-03-09 11:35:36 +01:00
AsamK
ee39733978 Update libsignal-service 2024-03-07 17:44:19 +01:00
AsamK
5e17fe8414 Fix typo 2024-02-27 19:35:38 +01:00
AsamK
aebe64571d Prepare next release 2024-02-27 18:20:21 +01:00
AsamK
0cc2da690a Bump version to 0.13.1 2024-02-27 18:18:30 +01:00
AsamK
2424fc1f53 Add register parameter to force reregistration 2024-02-27 18:12:43 +01:00
AsamK
2e4cd0eddc Only retry messages after identity was trusted
Fixes #1477
2024-02-27 17:52:21 +01:00
AsamK
189b21dbde Improve error message if captcha is rejected by server
Fixes #1328
2024-02-26 22:13:57 +01:00
AsamK
e77d9e3d60 Update libsignal-service 2024-02-26 22:07:36 +01:00
AsamK
df76aa9919 Default number sharing to NOBODY
Matches the official apps behavior.

Closes #1472
2024-02-26 18:27:09 +01:00
AsamK
378ac23c6c Update account attributes after setting a pin
Ensures that the recovery password gets set immediately.

Related to #1447
2024-02-26 18:23:37 +01:00
AsamK
fc2ae856d2 Adapt account record processor for linked devices 2024-02-25 19:41:34 +01:00
AsamK
57164ad7fb Prevent crash when receiving already migrated group v1 from storage
Fixes #1471
2024-02-25 19:41:10 +01:00
AsamK
6c44662496 Allow overriding user agent string
Not recommended, as it could lead to issues with newer Signal protocol changes.

Fixes #1476
2024-02-25 18:27:20 +01:00
AsamK
22ac3cb50f Removing linked devices only works on the primary device 2024-02-25 18:12:36 +01:00
AsamK
b76964f219 Improve warning message 2024-02-25 17:54:18 +01:00
AsamK
2f3c064462 Update documentation 2024-02-25 17:46:27 +01:00
AsamK
c9002d9481 Ignore failure when uploading PNI prekeys
Can happen if PNI identity key hasn't been sent to the server yet.
2024-02-25 17:46:09 +01:00
AsamK
83d471818d Allow setting a username with explicit descriminator
Fixes #1469
2024-02-22 20:00:00 +01:00
AsamK
0bb2a64781 Add missing field handling in account record processor 2024-02-22 20:00:00 +01:00
AsamK
08ba774b71 Update graalvm buildtools 2024-02-20 17:37:59 +01:00
AsamK
59c1f4eed2 Show information when requesting voice verification without SMS verification
Fixes #1373
2024-02-20 17:37:31 +01:00
AsamK
f1e3b5c9cc Add uniqueness check to db migration 2024-02-20 17:05:56 +01:00
AsamK
7bc7242f08 Add uniqueness check to db migration 2024-02-20 11:03:32 +01:00
Viktor Szépe
6f407ab509
Fix typos (#1459) 2024-02-19 08:20:24 +01:00
AsamK
db9acaf4ff Set snapshot version 2024-02-18 22:14:09 +01:00
AsamK
7821815429 Bump version to 0.13.0 2024-02-18 22:01:41 +01:00
AsamK
cd88e896fa Fix missing unique constraint on storage_id table 2024-02-18 22:01:41 +01:00
AsamK
67fd10b978 Update dependencies 2024-02-18 21:43:02 +01:00
AsamK
9051f68ba6 Enable profile sharing when accepting message request 2024-02-18 21:26:36 +01:00
AsamK
3b3377e6e9 Update CHANGELOG 2024-02-18 21:06:17 +01:00
AsamK
fb99ff5284 Document creating and sending to usernames 2024-02-18 21:04:50 +01:00
AsamK
ed40d116b7 Fix sending to username 2024-02-18 20:58:55 +01:00
AsamK
d84362eb0f Add sendMessageRequestResponse command 2024-02-18 20:48:16 +01:00
AsamK
2c0ad7feb7 Fix issue with prekey update 2024-02-18 20:48:16 +01:00
AsamK
7206b4da25 Fix use of aci instead of serviceId
Fixes #1455
2024-02-18 20:48:01 +01:00
AsamK
fd671576a4 Fix issues with receiving message to PNI address 2024-02-18 20:46:07 +01:00
AsamK
6cd57312a1 Add parameter to configure phone number privacy 2024-02-18 20:42:41 +01:00
AsamK
25258db55d Restructure pre key refresh to be more robust 2024-02-18 16:32:50 +01:00
AsamK
91ed49e019 Includ pni signatures if necessary 2024-02-18 16:31:34 +01:00
AsamK
bd792f5b7f Update libsignal-service 2024-02-18 15:16:41 +01:00
AsamK
ed4b1e8f02 Improve sql query 2024-02-10 12:24:13 +01:00
AsamK
e78146eb67 Fix saving username link components 2024-02-10 11:54:32 +01:00
AsamK
9f6b6cb657 Implement account reregistration with recovery password 2024-02-10 11:54:06 +01:00
AsamK
fb85e1a068 Update dependencies 2024-02-09 22:51:03 +01:00
AsamK
c78299e7bc Update gradle wrapper 2024-02-09 22:11:50 +01:00
AsamK
9a5e29e015 Update CHANGELOG.md 2024-02-09 22:11:01 +01:00
AsamK
7cf3a989bf Add command to retrieve avatars and stickers
Fixes #1125
2024-02-09 22:10:46 +01:00
AsamK
d486563099 Ignore invalid ACI/PNI in recipient table 2024-02-09 18:34:32 +01:00
AsamK
be699cbd85 Fix potential dead lock in recipient store 2024-02-09 18:07:30 +01:00
AsamK
4e61f2b2e5 Use app id as dbus name for flatpak version 2024-02-09 18:07:30 +01:00
AsamK
1bf703b012 Add --bus-name option to use different D-Bus bus name 2024-02-09 18:07:30 +01:00
AsamK
a48fdaf8b7 Bump version to 0.12.8 2024-02-09 18:07:30 +01:00
AsamK
a0f33ac942 Update reflect config 2024-02-09 18:07:30 +01:00
AsamK
bea659f948 Add icon 2024-02-05 16:58:24 +01:00
AsamK
7fb263fdf0 Update dependencies 2024-02-01 17:54:26 +01:00
AsamK
3be1c24b1b Update client dependencies 2024-01-31 21:17:23 +01:00
AsamK
0b33cb55b8 Adapt listContacts to only show registered users
Fixes #1395
2024-01-31 20:49:12 +01:00
AsamK
00e71ed0fc Improve scrubbing of sensitive identifiers
Fixes #1189
2024-01-31 20:33:01 +01:00
AsamK
a40810e33e Update CHANGELOG.md 2024-01-30 19:00:33 +01:00
AsamK
15c9d04703 Add missing command to json-rpc client 2024-01-30 18:58:27 +01:00
AsamK
e13dcdc85a Prevent empty username 2024-01-30 17:59:59 +01:00
AsamK
2ab5b2817e Deprecate daemon mode without explicit mode flag 2024-01-30 17:44:51 +01:00
AsamK
080c14d111 Clean old prekeys only after server message queue is empty 2024-01-30 17:21:29 +01:00
AsamK
d1e32f2b11 Improve some log output 2024-01-29 18:21:44 +01:00
AsamK
0840e0dedb Add CDSI recipients refresh job 2024-01-28 23:04:34 +01:00
AsamK
888d6bf091 Split unregistered recipients 2024-01-28 23:04:34 +01:00
AsamK
e2f308a57a Store profile sharing for group v2 2024-01-28 22:38:41 +01:00
AsamK
76fe6ad799 Recreate recipient database with aci column 2024-01-28 22:38:41 +01:00
AsamK
90df256e85 Fix storage sync issues 2024-01-28 22:38:41 +01:00
AsamK
f92466f6be Store recipient unregistered state 2024-01-28 22:38:41 +01:00
AsamK
6a5dcd00b2 Implement remote storage sync
Closes #604
2024-01-28 22:38:41 +01:00
AsamK
6c65de8ddf Update libsignal-service 2024-01-26 23:32:29 +01:00
AsamK
f696097301 Improve JobExecutor 2024-01-25 21:47:40 +01:00
AsamK
7d3db03d4a Update dbusjava 2024-01-25 21:03:25 +01:00
AsamK
78c87e501e Improve log level 2024-01-25 21:00:16 +01:00
AsamK
2974b466aa Update sqlite 2024-01-22 18:40:46 +01:00
AsamK
ca1a852b44 Handle mentions when receiving from dbus 2024-01-22 18:40:39 +01:00
dependabot[bot]
ac2343b142
Bump h2 from 0.3.21 to 0.3.24 in /client (#1418)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.21 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.21...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-20 09:18:20 +01:00
AsamK
be9efb9a25 Update libsignal-service-java 2024-01-14 11:42:48 +01:00
AsamK
30e8e36635 Add --unrestricted-unidentified-sender to updateAccount command 2024-01-13 16:00:55 +01:00
AsamK
3290a5bf4d Add --notify-self parmeter
Fixes #1087
2024-01-12 18:09:42 +01:00
AsamK
c07ba14fc6 Update dependencies 2024-01-04 18:09:36 +01:00
AsamK
c6e9e19858 Update libsignal-service-java 2024-01-04 18:00:19 +01:00
morph027
375bdb7948
add file.encoding=UTF-8 to native builds, refs #1105 (#1404)
Signed-off-by: morph027 <stefan.heitmueller@gmx.com>
2023-12-19 18:48:54 +01:00
AsamK
5e7899675e Update CHANGELOG.md 2023-12-15 19:53:35 +01:00
AsamK
328ba0202f Bump version to 0.12.7 2023-12-15 19:10:56 +01:00
AsamK
59cbbde835 Update CHANGELOG.md 2023-12-15 19:05:13 +01:00
AsamK
caa4fa0180 Fix inspection issues 2023-12-15 17:32:30 +01:00
AsamK
fc6a4b78eb Update libsignal-service-java
Uses new device link endpoint

Fixes #1399
2023-12-14 18:07:13 +01:00
AsamK
2729772adb Fix release pipeline 2023-12-11 19:11:26 +01:00
AsamK
70e0f7027e Bump version to 0.12.6 2023-12-11 18:32:25 +01:00
AsamK
4dcdffda0a Use a new SVR2 enclave. 2023-12-10 19:30:36 +01:00
AsamK
2535cd9c2c Update libsignal-service-java 2023-12-10 18:57:05 +01:00
AsamK
d3eef6c820 Update dependencies 2023-12-06 18:12:17 +01:00
AsamK
35fb1d60af Update gradle wrapper 2023-12-06 18:11:27 +01:00
AsamK
089ec22666 Fix overlapping sqlite sessions in migration
Fixes #1394
2023-12-06 18:09:02 +01:00
AsamK
66077f317b Add missing capabilities for linking account
Fixes #1386
2023-11-28 19:12:53 +01:00
AsamK
5904bfa92b Update CHANGELOG 2023-11-21 21:14:16 +01:00
AsamK
2c1d7a803f Set snapshot version 2023-11-21 21:10:44 +01:00
AsamK
6d5b8b6a7b Bump version to 0.12.5 2023-11-21 21:07:54 +01:00
AsamK
b88f8c205e Update dependencies 2023-11-21 21:02:47 +01:00
AsamK
ce4afbe4c2 Update libsignal-service-java
Fixes #1384
2023-11-21 21:02:47 +01:00
AsamK
fcaecef961 Improve source serviceId handling 2023-11-21 19:59:06 +01:00
AsamK
81d2e315e3 Prevent groupV2Operations from being null 2023-11-21 17:17:26 +01:00
AsamK
fddf982832 Fix receiving contact address
Fixes #1385
2023-11-21 17:09:21 +01:00
AsamK
3602ef9be9 Add -u flag to send to username 2023-11-21 17:09:21 +01:00
AsamK
37c65ca6b4 Implement username links 2023-11-21 17:09:21 +01:00
AsamK
77f284661b Fix storing of username 2023-11-12 19:33:40 +01:00
AsamK
0d60c4d464 Refactor group v2 migration 2023-11-12 19:33:40 +01:00
AsamK
f06eeb01b9 Refactor getGroup method 2023-11-12 15:21:29 +01:00
AsamK
fbcc1cfb50 Refactor KeyValueStore 2023-11-12 14:24:26 +01:00
AsamK
26cef99cdf Don't store self profile key in recipient store 2023-11-12 13:13:58 +01:00
AsamK
5cac7feabe Add who am I job 2023-11-12 12:12:02 +01:00
AsamK
d844e0f396 Add download profile avatar job 2023-11-12 11:59:48 +01:00
AsamK
ed11bf6368 Make JobExecutor execute jobs asynchronously 2023-11-12 11:51:38 +01:00
AsamK
eaa6b7cf57 Check whoAmI after regular CDSI sync 2023-11-11 18:12:22 +01:00
AsamK
f8ea631b03 Refactor trusted recipient resolver 2023-11-11 17:24:45 +01:00
AsamK
7b0744ec75 Add --hide parameter to removeContact command 2023-11-11 11:39:33 +01:00
AsamK
9f4a2b3e26 Convert Contact to record 2023-11-11 11:09:24 +01:00
AsamK
9741c93ce9 Move all dbus interaction to dbus package 2023-11-10 15:44:42 +01:00
AsamK
7e9940be4a Refactor DaemonCommand 2023-11-10 15:32:45 +01:00
AsamK
c0aa338d7c Reorder static final modifier 2023-11-10 14:03:13 +01:00
AsamK
b20978e08e Remove now unnecessary SuccessExitStatus from systemd units 2023-11-09 23:26:38 +01:00
AsamK
5d33f71d4d Use improved shutdown for receive command 2023-11-09 19:23:11 +01:00
AsamK
1058e33f12 Use improved shutdown for daemon command 2023-11-09 19:23:11 +01:00
AsamK
b7fedff511 Use improved shutdown for jsonRpc command 2023-11-09 19:23:11 +01:00
AsamK
f252597002 Add infrastructure for better shutdown handling 2023-11-09 19:23:11 +01:00
AsamK
cbbfc4ea6e Rename exportManager method 2023-11-09 18:54:17 +01:00
AsamK
6b04197eaa Refactor getManagerFromQuery method 2023-11-09 18:54:17 +01:00
AsamK
3027ba2cf1 Refactor log config 2023-11-09 15:55:43 +01:00
AsamK
19e9f31afd Remove now unnecessary try/catch 2023-11-09 09:22:46 +01:00
AsamK
5a1d2580cb Use a single PushServiceSocket 2023-11-09 09:22:46 +01:00
AsamK
cb5cace8da Update libsignal-service 2023-11-09 09:22:46 +01:00
AsamK
e734c125ad Ignore quotes without author
Fixes #1369
2023-11-08 12:40:34 +01:00
AsamK
d351f64fb1 Configure log LevelChangePropagator 2023-11-08 12:05:55 +01:00
AsamK
e61f587bfc Reduce use of printStackTrace 2023-11-07 09:28:29 +01:00
AsamK
4e8f0a41c7 Update dependencies 2023-11-07 09:11:36 +01:00
AsamK
4fbc97c92a Register java exit handlers in graalvm native build 2023-11-07 08:54:05 +01:00
AsamK
1e35ac380e Refactor DaemonCommand 2023-11-07 08:54:05 +01:00
AsamK
85b0647a3e Extract getReceiveConfig to utils 2023-11-07 08:54:05 +01:00
spezialist1
7b899d1853
Add example syntax for a mixed text style (#1367) 2023-11-06 11:58:11 +01:00
AsamK
699b21f066 Handle rate limit error in JSON-RPC mode 2023-11-05 16:20:33 +01:00
AsamK
8d423adb4d Use safety numbers with ACI by default 2023-11-05 11:59:09 +01:00
AsamK
506dcfa6c0 Update README.md 2023-11-04 11:32:42 +01:00
AsamK
91e0d5164b Ensure profile key is stored in profileKeyStore
Fixes #1362
2023-11-03 20:31:35 +01:00
AsamK
67d8ffcde5 Initialize database before registering 2023-11-03 20:19:41 +01:00
AsamK
44c9aded65 Don't check self number in recipients refresh 2023-11-03 20:01:05 +01:00
AsamK
f1ccfc0361 Initialize pre key offsets when creating new account 2023-11-03 20:00:26 +01:00
AsamK
cdef9c435c Check if account is already registered before attempting verification 2023-11-03 19:59:39 +01:00
AsamK
ed8ac5b84c Use new threads API 2023-10-24 17:36:32 +02:00
AsamK
8d55dfb66b Use pattern matching switch cases 2023-10-24 17:36:32 +02:00
AsamK
80c1a6d2af Switch to Java 21 2023-10-24 17:36:32 +02:00
AsamK
895740755d Remove included by default native-image component 2023-10-23 18:01:49 +02:00
AsamK
90601b7b6b Fix man page generation 2023-10-22 21:33:15 +02:00
AsamK
cc2ffbb4da Bump version to 0.12.4 2023-10-22 20:49:22 +02:00
AsamK
0c24064939 Bump version to 0.12.4 2023-10-22 20:42:25 +02:00
AsamK
7859084be8 Fix tests 2023-10-22 20:42:25 +02:00
AsamK
101c217ef8 Update captcha help text 2023-10-22 20:42:25 +02:00
AsamK
7887a5a613 Prevent unnecessary compile warnings 2023-10-22 20:42:25 +02:00
AsamK
800ff09a37 Update dependencies 2023-10-22 20:42:25 +02:00
AsamK
20f8fa2ebd Prevent ConcurrentModificationException
Fixes #1351
2023-10-22 20:42:25 +02:00
AsamK
314159c273 Update libsignal-service-java 2023-10-22 20:42:25 +02:00
Vladimir Glinskikh
bf16885278
Bump upgrade gradle from 8.3 to 8.4 (#1353) 2023-10-21 12:49:28 +02:00
AsamK
3ae6f7ab7c Update graalvm reflect-config 2023-10-19 23:31:41 +02:00
AsamK
e829dd8b5c Update graalvm reflect-config
Fixes #1352
2023-10-19 23:18:07 +02:00
dependabot[bot]
62c71eaafa
Bump rustix from 0.37.23 to 0.37.25 in /client (#1350)
Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.37.23 to 0.37.25.
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.37.23...v0.37.25)

---
updated-dependencies:
- dependency-name: rustix
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 08:01:22 +02:00
AsamK
24ec25ac62 Fix typo 2023-10-18 22:57:51 +02:00
Pēteris Caune
19b5c76154
Fix typo (#1348) 2023-10-18 07:47:10 +02:00
AsamK
2852b2ea6d Extend log scrubber for dbus serviceIds 2023-10-17 23:05:45 +02:00
AsamK
fc2e9bbfae Fix inspections 2023-10-17 20:34:21 +02:00
AsamK
d51dd7ae57 Use .isEmpty() for checking lists and strings 2023-10-17 20:34:21 +02:00
AsamK
9ba70c1808 Update bouncycastle dependency 2023-10-17 19:48:22 +02:00
AsamK
9ec942ea1d Update workflow permission 2023-10-17 19:34:42 +02:00
AsamK
b9e644269b Use gradle-build-action for caching and dependency submission 2023-10-17 19:22:09 +02:00
AsamK
5b56445741 Update github checkout action 2023-10-17 19:21:41 +02:00
AsamK
1ed5148624 Bump version to 0.12.3 2023-10-17 15:24:56 +02:00
AsamK
fb7c63c507 Reduce sqlite logging 2023-10-17 15:21:18 +02:00
AsamK
2c5edbc981 Add cache for serviceId to recipient id/address mapping 2023-10-17 15:20:14 +02:00
AsamK
1addffe622 Store username aci link in recipient store 2023-10-17 14:58:15 +02:00
AsamK
733c14bbc8 Ignore invalid recipient numbers 2023-10-17 14:19:32 +02:00
AsamK
ca2e6adedb Update dependencies 2023-10-17 14:18:35 +02:00
AsamK
5cc20ace1f Ignore failures from SVR v1 pin 2023-10-17 13:34:09 +02:00
AsamK
400dcf2899 Refactor creating linked account files 2023-10-16 19:01:31 +02:00
AsamK
dd3326f038 Do a full recipients refresh every day 2023-10-16 19:01:31 +02:00
AsamK
505de39d2a Use partial cdsi request only for a maximum of 3 numbers
Fixes #1239
2023-10-16 19:01:08 +02:00
AsamK
24069c8277 Update documentation 2023-10-15 22:37:46 +02:00
AsamK
bb78e9aaeb Update libsignal-service-java 2023-10-15 22:37:01 +02:00
AsamK
5c39344cff Implement full CDSI refresh 2023-10-15 22:36:45 +02:00
AsamK
7cd24a74af Improve handling of CDSI resource exhaustion 2023-10-15 19:02:59 +02:00
AsamK
a675631965 Resolve self address after setting new PNI 2023-10-15 18:53:24 +02:00
AsamK
33c4e17c0d Implement change phone number
Closes #1240
2023-10-14 23:37:23 +02:00
AsamK
56ee173d03 Implement exit code for rate limiting error
Closes #1240
2023-10-13 19:36:58 +02:00
AsamK
af4709255a Fix creation of key_value table for new accounts 2023-10-13 12:54:30 +02:00
AsamK
527e1aefc9 Refactor generateSignedPreKeyRecord 2023-10-12 21:15:19 +02:00
AsamK
a66dd0dc79 Implement EditMessageReceived signal for dbus 2023-10-10 20:21:40 +02:00
AsamK
7b5b5776f0 Add --quote-attachment paramter to send command
Fixes #1318
2023-10-10 19:54:45 +02:00
AsamK
91ab0b12b0 Rename pre key id offset field 2023-10-09 19:13:45 +02:00
AsamK
c2ea7045f5 Prevent deleting self recipient 2023-10-09 19:13:45 +02:00
AsamK
af3cae5f3d Restructure account save file 2023-10-09 19:13:45 +02:00
AsamK
54c3b19052 Support serviceId based safety number for scannable safety numbers 2023-10-06 21:42:28 +02:00
AsamK
04de0010b5 Update libsignal-service-java 2023-10-06 19:58:09 +02:00
AsamK
66161b74a6 Move aci/pni to account data object 2023-10-06 19:53:32 +02:00
AsamK
889ef66784 Update release pipeline
The default libsignal-client jar now contains Linux/macOS/Window native library
2023-10-06 18:55:13 +02:00
AsamK
fae83f8d22 Extend json-rpc man page 2023-10-05 22:18:36 +02:00
AsamK
6f4d538832 Execute JSON-RPC requests in parallel 2023-10-05 22:18:13 +02:00
AsamK
a0c345185b Move metadata to db 2023-10-04 20:19:07 +02:00
AsamK
c0f771684d Move configuration store to db 2023-10-04 20:19:07 +02:00
AsamK
90ec01bfbf Only output username if set 2023-10-04 17:10:16 +02:00
AsamK
7f83bfefd6 Reorganize SignalAccount 2023-10-02 12:09:41 +02:00
AsamK
310aadbdc0 Fix handling edit message 2023-10-01 17:06:07 +02:00
AsamK
6b2de3a24d Reduce hikari logging noise 2023-10-01 16:30:14 +02:00
AsamK
002a87d3ba Add pre key cleanup and improve refresh 2023-10-01 16:29:41 +02:00
AsamK
6f63346905 Fix log statement 2023-09-30 22:32:45 +02:00
AsamK
735766669e Convert classes to records 2023-09-30 22:26:08 +02:00
418 changed files with 37219 additions and 10209 deletions

57
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: build
on:
push:
branches:
- "**"
pull_request:
workflow_call:
permissions: {}
jobs:
build:
strategy:
matrix:
# java="25" is the LTS Java version used in reproducible builds script (default in Containerfile).
# More Java versions can be added to test compatibility, eg. "26".
java: ["25", "26"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build
run: |
if [ "${{ matrix.java }}" != "25" ]; then
export OVERRIDE_JAVA_VERSION="${{ matrix.java }}"
fi
./reproducible-builds/build.sh
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-archive-${{ matrix.java }}
path: dist/*
build-client:
strategy:
matrix:
os:
- ubuntu
- macos
- windows
runs-on: ${{ matrix.os }}-latest
defaults:
run:
working-directory: ./client
steps:
- uses: actions/checkout@v6
- name: Install rust
run: rustup default stable
- name: Build client
run: cargo build --release --verbose
- name: Archive production artifacts
uses: actions/upload-artifact@v7
with:
name: signal-cli-client-${{ matrix.os }}
path: |
client/target/release/signal-cli-client
client/target/release/signal-cli-client.exe

View File

@ -1,58 +0,0 @@
name: signal-cli CI
on:
push:
branches:
- '**'
pull_request:
workflow_call:
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17', '21' ]
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: ${{ matrix.java }}
cache: 'gradle'
- name: Build with Gradle
run: ./gradlew --no-daemon build
- name: Compress archive
run: gzip -n -9 build/distributions/signal-cli-*.tar
- name: Archive production artifacts
uses: actions/upload-artifact@v3
with:
name: signal-cli-archive-${{ matrix.java }}
path: build/distributions/signal-cli-*.tar.gz
build-graalvm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: graalvm/setup-graalvm@v1
with:
version: 'latest'
java-version: '17'
cache: 'gradle'
components: 'native-image'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build with Gradle
run: ./gradlew --no-daemon nativeCompile
- name: Archive production artifacts
uses: actions/upload-artifact@v3
with:
name: signal-cli-native
path: build/native/nativeCompile/signal-cli

View File

@ -21,13 +21,13 @@ jobs:
steps: steps:
- name: Setup Java JDK - name: Setup Java JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v5
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 17 java-version: 25
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v6
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@ -35,7 +35,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v4
# Override language selection by uncommenting this and choosing your languages # Override language selection by uncommenting this and choosing your languages
# with: # with:
# languages: go, javascript, csharp, python, cpp, java # languages: go, javascript, csharp, python, cpp, java
@ -43,7 +43,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v4
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v4

27
.github/workflows/dependency-graph.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: dependency-graph
on:
push:
branches:
- "**"
permissions: {}
jobs:
submit-dependency-graph:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '25'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
dependency-graph: generate-and-submit
- name: Generate dependency graph
run: ./gradlew --no-daemon dependencies

View File

@ -5,185 +5,78 @@ on:
tags: tags:
- v* - v*
permissions: permissions: {}
contents: read # to fetch code (actions/checkout)
env: env:
IMAGE_NAME: signal-cli IMAGE_NAME: signal-cli
IMAGE_REGISTRY: ghcr.io/asamk IMAGE_REGISTRY: ghcr.io/asamk
REGISTRY_USER: ${{ github.actor }} REGISTRY_USER: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ github.token }} REGISTRY_PASSWORD: ${{ github.token }}
ARCHIVE_JAVA_VERSION: 25
jobs: jobs:
build:
uses: ./.github/workflows/build.yml
ci_wf: release:
uses: AsamK/signal-cli/.github/workflows/ci.yml@master needs: build
# ${{ github.repository }} not accepted here
lib_to_jar:
needs: ci_wf
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
outputs: outputs:
signal_cli_version: ${{ steps.cli_ver.outputs.version }} version: ${{ steps.version.outputs.version }}
release_id: ${{ steps.create_release.outputs.id }}
steps: steps:
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v3 uses: actions/download-artifact@v8
- name: Get signal-cli version - name: Get signal-cli version
id: cli_ver id: version
run: | run: |
ver="${GITHUB_REF_NAME#v}" mv ./signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/* .
echo "version=${ver}" >> $GITHUB_OUTPUT echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Extract archive - name: Create draft release and upload assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
tree . gh release create "v${{ steps.version.outputs.version }}" \
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1) --repo "${{ github.repository }}" \
tar -xzf ./"${ARCHIVE_DIR}"/*.tar.gz --title "v${{ steps.version.outputs.version }}" \
mv ./"${ARCHIVE_DIR}"/*.tar.gz signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz --draft \
rm -rf signal-cli-archive-*/ --verify-tag \
"signal-cli-${{ steps.version.outputs.version }}.tar.gz" \
- name: Get signal-client jar version "signal-cli-${{ steps.version.outputs.version }}-Linux-native.tar.gz" \
id: lib_ver "signal-cli-${{ steps.version.outputs.version }}-Linux-client.tar.gz" \
run: | "signal-cli-${{ steps.version.outputs.version }}-json-schemas.tar.gz"
JAR_PREFIX=libsignal-client-
jar_file=$(find ./signal-cli-*/lib/ -name "$JAR_PREFIX*.jar")
jar_version=$(echo "$jar_file" | xargs basename | sed "s/$JAR_PREFIX//; s/.jar//")
echo "$jar_version"
echo "signal_client_version=${jar_version}" >> $GITHUB_OUTPUT
- name: Download signal-client builds
env:
RELEASES_URL: https://github.com/signalapp/libsignal/releases/download/
FILE_NAMES: signal_jni.dll libsignal_jni.dylib
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
run: |
for file_name in $FILE_NAMES; do
curl -sOL "${RELEASES_URL}/v${SIGNAL_CLIENT_VER}/${file_name}" # note: added v
done
tree .
- name: Compress native app
env:
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
run: |
chmod +x signal-cli-native/signal-cli
tar -czf signal-cli-${SIGNAL_CLI_VER}-Linux-native.tar.gz -C signal-cli-native signal-cli
rm -rf signal-cli-native/
- name: Replace Windows lib
env:
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
run: |
mv signal_jni.dll libsignal_jni.so
zip -u ./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar ./libsignal_jni.so
tar -czf signal-cli-${SIGNAL_CLI_VER}-Windows.tar.gz signal-cli-*/
- name: Replace macOS lib
env:
SIGNAL_CLI_VER: ${{ steps.cli_ver.outputs.version }}
SIGNAL_CLIENT_VER: ${{ steps.lib_ver.outputs.signal_client_version }}
run: |
jar_file=./signal-cli-*/lib/libsignal-client-${SIGNAL_CLIENT_VER}.jar
zip -d $jar_file libsignal_jni.so
zip $jar_file libsignal_jni.dylib
tar -czf signal-cli-${SIGNAL_CLI_VER}-macOS.tar.gz signal-cli-*/
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
release_name: v${{ steps.cli_ver.outputs.version }} # note: added `v`
draft: true
- name: Upload Linux archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
- name: Upload Linux native archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Linux-native.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
- name: Upload windows archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-Windows.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
- name: Upload macos archive
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
asset_name: signal-cli-${{ steps.cli_ver.outputs.version }}-macOS.tar.gz
asset_content_type: application/x-compressed-tar # .tar.gz
build-container: build-container:
needs: ci_wf needs: release
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read
packages: write packages: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v6
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v3 uses: actions/download-artifact@v8
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file - name: Move archive file
run: | run: |
ARCHIVE_DIR=$(ls signal-cli-archive-*/ -d | tail -n1) tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}.tar.gz
tar xf ./"${ARCHIVE_DIR}"/*.tar.gz
rm -r signal-cli-archive-* signal-cli-native
mkdir -p build/install/ mkdir -p build/install/
mv ./signal-cli-*/ build/install/signal-cli mv ./signal-cli-"${{ needs.release.outputs.version }}"/ build/install/signal-cli
- name: Build Image - name: Build Image
id: build_image id: build_image
uses: redhat-actions/buildah-build@v2 uses: redhat-actions/buildah-build@v3
with: with:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tags: latest ${{ github.sha }} ${{ steps.cli_ver.outputs.version }} tags: latest ${{ github.sha }} ${{ needs.release.outputs.version }}
containerfiles: containerfiles: ./Containerfile
./Containerfile
oci: true oci: true
- name: Push To GHCR - name: Push To GHCR
uses: redhat-actions/push-to-registry@v2 uses: redhat-actions/push-to-registry@v3
id: push id: push
with: with:
image: ${{ steps.build_image.outputs.image }} image: ${{ steps.build_image.outputs.image }}
@ -197,41 +90,75 @@ jobs:
echo "${{ toJSON(steps.push.outputs) }}" echo "${{ toJSON(steps.push.outputs) }}"
build-container-native: build-container-native:
needs: ci_wf needs: release
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read
packages: write packages: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v6
- name: Download signal-cli build from CI workflow - name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v3 uses: actions/download-artifact@v8
- name: Get signal-cli version
id: cli_ver
run: |
ver="${GITHUB_REF_NAME#v}"
echo "version=${ver}" >> $GITHUB_OUTPUT
- name: Move archive file - name: Move archive file
run: | run: |
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-native.tar.gz
mkdir -p build/native/nativeCompile/ mkdir -p build/native/nativeCompile/
chmod +x ./signal-cli-native/signal-cli mv signal-cli build/native/nativeCompile/
mv ./signal-cli-native/signal-cli build/native/nativeCompile/ chmod +x build/native/nativeCompile/signal-cli
- name: Build Image - name: Build Image
id: build_image id: build_image
uses: redhat-actions/buildah-build@v2 uses: redhat-actions/buildah-build@v3
with: with:
image: ${{ env.IMAGE_NAME }} image: ${{ env.IMAGE_NAME }}
tags: latest-native ${{ github.sha }}-native ${{ steps.cli_ver.outputs.version }}-native tags: latest-native ${{ github.sha }}-native ${{ needs.release.outputs.version }}-native
containerfiles: containerfiles: ./native.Containerfile
./native.Containerfile
oci: true oci: true
- name: Push To GHCR - name: Push To GHCR
uses: redhat-actions/push-to-registry@v2 uses: redhat-actions/push-to-registry@v3
id: push
with:
image: ${{ steps.build_image.outputs.image }}
tags: ${{ steps.build_image.outputs.tags }}
registry: ${{ env.IMAGE_REGISTRY }}
username: ${{ env.REGISTRY_USER }}
password: ${{ env.REGISTRY_PASSWORD }}
- name: Echo outputs
run: |
echo "${{ toJSON(steps.push.outputs) }}"
build-container-client:
needs: release
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v6
- name: Download signal-cli build from CI workflow
uses: actions/download-artifact@v8
- name: Move archive file
run: |
tar xf signal-cli-archive-${{ env.ARCHIVE_JAVA_VERSION }}/signal-cli-${{ needs.release.outputs.version }}-Linux-client.tar.gz
mkdir -p client/target/release/
mv signal-cli-client client/target/release/
chmod +x client/target/release/signal-cli-client
- name: Build Image
id: build_image
uses: redhat-actions/buildah-build@v3
with:
image: ${{ env.IMAGE_NAME }}
tags: latest-client ${{ github.sha }}-client ${{ needs.release.outputs.version }}-client
containerfiles: ./client.Containerfile
oci: true
- name: Push To GHCR
uses: redhat-actions/push-to-registry@v3
id: push id: push
with: with:
image: ${{ steps.build_image.outputs.image }} image: ${{ steps.build_image.outputs.image }}

8
.gitignore vendored
View File

@ -1,4 +1,5 @@
.gradle/ .gradle/
.kotlin/
.idea/* .idea/*
!.idea/codeStyles/ !.idea/codeStyles/
build/ build/
@ -13,3 +14,10 @@ out/
.DS_Store .DS_Store
/bin/ /bin/
/test-config/ /test-config/
/dist/
/github/
man/*.1
man/*.5
man/man1
man/man5
.superpowers/

View File

@ -5,8 +5,8 @@
<option name="GENERATE_FINAL_LOCALS" value="true" /> <option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="GENERATE_FINAL_PARAMETERS" value="true" /> <option name="GENERATE_FINAL_PARAMETERS" value="true" />
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" /> <option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" /> <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="50" /> <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="IMPORT_LAYOUT_TABLE"> <option name="IMPORT_LAYOUT_TABLE">
<value> <value>
<package name="com" withSubpackages="true" static="false" /> <package name="com" withSubpackages="true" static="false" />

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ If you have a question you can ask it in the [GitHub discussions page](https://g
- Be sure to include a **title and clear description**, as much relevant information as possible. - Be sure to include a **title and clear description**, as much relevant information as possible.
- Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using - Specify the versions of signal-cli, libsignal-client (if self-compiled), JDK and OS you're using
- Specify if it's the normal java or the graalvm native version. - Specify if it's the normal java or the graalvm native version.
- Run the failing command with `--verbose` flag to get a more detailed log output and include that in the bug report - Run the failing command with `-vv --scrub-log` flags to get a more detailed log output and include that in the bug report
# Pull request # Pull request

View File

@ -1,4 +1,4 @@
FROM docker.io/eclipse-temurin:17-jre FROM docker.io/azul/zulu-openjdk:25-jre-headless
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger." LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."

View File

@ -3,7 +3,7 @@
signal-cli is a commandline interface for the [Signal messenger](https://signal.org/). signal-cli is a commandline interface for the [Signal messenger](https://signal.org/).
It supports registering, verifying, sending and receiving messages. It supports registering, verifying, sending and receiving messages.
signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java), signal-cli uses a [patched libsignal-service-java](https://github.com/Turasa/libsignal-service-java),
extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/libsignal/service). extracted from the [Signal-Android source code](https://github.com/signalapp/Signal-Android/tree/main/lib/libsignal-service).
For registering you need a phone number where you can receive SMS or incoming calls. For registering you need a phone number where you can receive SMS or incoming calls.
signal-cli is primarily intended to be used on servers to notify admins of important events. signal-cli is primarily intended to be used on servers to notify admins of important events.
@ -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)). 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. 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 ## Installation
You can [build signal-cli](#building) yourself or use You can [build signal-cli](#building) yourself or use
@ -19,24 +23,33 @@ Windows. There's also a [docker image and some Linux packages](https://github.co
System requirements: System requirements:
- at least Java Runtime Environment (JRE) 17 - at least Java Runtime Environment (JRE) 25
- native library: libsignal-client - native library: libsignal-client
The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other The native libs are bundled for x86_64 Linux (with recent enough glibc), Windows and MacOS. For other
systems/architectures systems/architectures
see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal) see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal)
### Install system-wide on Linux ### Install system-wide on Linux [ JVM build ]
See [latest version](https://github.com/AsamK/signal-cli/releases). See [latest version](https://github.com/AsamK/signal-cli/releases).
```sh ```sh
export VERSION=<latest version, format "x.y.z"> VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux.tar.gz curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz
sudo tar xf signal-cli-"${VERSION}"-Linux.tar.gz -C /opt sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt
sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/
``` ```
### Install system-wide on Linux [ GraalVM native build ]
```sh
VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//')
curl -L -O https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}"-Linux-native.tar.gz
sudo tar xf signal-cli-"${VERSION}"-Linux-native.tar.gz -C /opt
sudo ln -sf /opt/signal-cli /usr/local/bin/
```
You can find further instructions on the Wiki: You can find further instructions on the Wiki:
- [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart) - [Quickstart](https://github.com/AsamK/signal-cli/wiki/Quickstart)
@ -51,12 +64,31 @@ Important: The ACCOUNT is your phone number in international format and must inc
should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list should start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list
of all country codes.) of all country codes.)
* Link to an existing account
If you have an existing Signal account associated
with a number, you can link signal-cli to it with:
signal-cli link
* Register a number (with SMS verification) * Register a number (with SMS verification)
Alternatively, if you don't have an existing Signal
account, you can register one from signal-cli. Note
that this will unregister any existing client
associated with the same number.
signal-cli -a ACCOUNT register 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 You can register Signal using a landline number. In this case, you need to follow the procedure below:
to the voice call verification by adding the `--voice` switch at the end of above register command. * 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 Registering may require solving a CAPTCHA
challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha) challenge: [Registration with captcha](https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha)
@ -72,6 +104,12 @@ of all country codes.)
signal-cli -a ACCOUNT send -m "This is a message" RECIPIENT 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. * Pipe the message content from another process.
uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT uname -a | signal-cli -a ACCOUNT send --message-from-stdin RECIPIENT
@ -122,14 +160,23 @@ version installed, you can replace `./gradlew` with `gradle` in the following st
./gradlew run --args="--help" ./gradlew run --args="--help"
``` ```
### JSON Schemas for the JSON-RPC mode
1. Generate [JSON Schema](https://json-schema.org/) files for all the JSON-RPC data classes (`src/main/java/org/asamk/signal/json`):
```sh
./gradlew jsonSchemas
```
2. The generated files can be found in the `build/generated/META-INF/schemas` folder.
### Building a native binary with GraalVM (EXPERIMENTAL) ### Building a native binary with GraalVM (EXPERIMENTAL)
It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not It is possible to build a native binary with [GraalVM](https://www.graalvm.org). This is still experimental and will not
work in all situations. work in all situations.
1. [Install GraalVM and setup the enviroment](https://www.graalvm.org/docs/getting-started/#install-graalvm) 1. [Install GraalVM and setup the environment](https://www.graalvm.org/docs/getting-started/#install-graalvm)
2. [Install prerequisites](https://www.graalvm.org/reference-manual/native-image/#prerequisites) 2. Execute Gradle:
3. Execute Gradle:
./gradlew nativeCompile ./gradlew nativeCompile

View File

@ -1,31 +1,46 @@
import groovy.json.JsonOutput
plugins { plugins {
java java
application application
eclipse eclipse
`check-lib-versions` `check-lib-versions`
id("org.graalvm.buildtools.native") version "0.9.27" id("org.graalvm.buildtools.native") version "1.1.9"
} }
version = "0.12.2" allprojects {
group = "org.asamk"
version = "0.14.8"
}
java { java {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_25
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
toolchain {
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
}
}
} }
application { application {
mainClass.set("org.asamk.signal.Main") mainClass.set("org.asamk.signal.Main")
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")
} }
graalvmNative { graalvmNative {
binaries { binaries {
this["main"].run { this["main"].run {
buildArgs.add("-Dfile.encoding=UTF-8")
buildArgs.add("-J-Dfile.encoding=UTF-8")
buildArgs.add("-march=compatibility")
buildArgs.add("--enable-native-access=ALL-UNNAMED")
resources.autodetect() resources.autodetect()
configurationFileDirectories.from(file("graalvm-config-dir"))
if (System.getenv("GRAALVM_HOME") == null) { if (System.getenv("GRAALVM_HOME") == null) {
toolchainDetection.set(true) toolchainDetection.set(true)
javaLauncher.set(javaToolchains.launcherFor { javaLauncher.set(javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(17)) languageVersion.set(JavaLanguageVersion.of(25))
}) })
} else { } else {
toolchainDetection.set(false) toolchainDetection.set(false)
@ -34,7 +49,48 @@ graalvmNative {
} }
} }
val artifactType = Attribute.of("artifactType", String::class.java)
val minified = Attribute.of("minified", Boolean::class.javaObjectType)
dependencies { 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",
)
)
val schemaAnnotationProcessor = configurations.create("schemaAnnotationProcessor") {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
registerTransform(JarFileExcluder::class) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
parameters {
excludeFilesByArtifact = excludePatterns
}
}
schemaAnnotationProcessor(libs.micronaut.json.schema.processor)
schemaAnnotationProcessor(libs.micronaut.inject.java)
implementation(libs.bouncycastle) implementation(libs.bouncycastle)
implementation(libs.jackson.databind) implementation(libs.jackson.databind)
implementation(libs.argparse4j) implementation(libs.argparse4j)
@ -42,7 +98,20 @@ dependencies {
implementation(libs.slf4j.api) implementation(libs.slf4j.api)
implementation(libs.slf4j.jul) implementation(libs.slf4j.jul)
implementation(libs.logback) implementation(libs.logback)
implementation(project(":lib")) implementation(libs.zxing)
implementation(libs.micronaut.json.schema.annotations)
if (gradle.startParameter.taskNames.any { it.contains("jsonSchemas") }) {
implementation(libs.micronaut.json.schema.generator)
}
implementation(project(":libsignal-cli"))
testImplementation(libs.junit.jupiter)
testImplementation(platform(libs.junit.jupiter.bom))
testRuntimeOnly(libs.junit.launcher)
}
tasks.named<Test>("test") {
useJUnitPlatform()
} }
configurations { configurations {
@ -66,23 +135,68 @@ tasks.withType<Jar> {
attributes( attributes(
"Implementation-Title" to project.name, "Implementation-Title" to project.name,
"Implementation-Version" to project.version, "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") archiveBaseName.set("${project.name}-fat")
exclude( exclude(
"META-INF/*.SF", "META-INF/*.SF",
"META-INF/**/*.MF",
"META-INF/*.DSA", "META-INF/*.DSA",
"META-INF/*.RSA", "META-INF/*.RSA",
"META-INF/NOTICE*", "META-INF/NOTICE*",
"META-INF/LICENSE*", "META-INF/LICENSE*",
"META-INF/INDEX.LIST", "META-INF/INDEX.LIST",
"**/module-info.class" "**/module-info.class",
) )
duplicatesStrategy = DuplicatesStrategy.WARN duplicatesStrategy = DuplicatesStrategy.WARN
doFirst {
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
}
with(tasks.jar.get()) with(tasks.jar.get())
} }
tasks.register("writeLibsignalVersion") {
doLast {
val resolutionResult = configurations.runtimeClasspath.get().incoming.resolutionResult
val libsignalDep =
resolutionResult.allDependencies.find { dep -> dep.requested is ModuleComponentSelector && (dep.requested as ModuleComponentSelector).group == "org.signal" && (dep.requested as ModuleComponentSelector).moduleIdentifier.name == "libsignal-client" }
if (libsignalDep != null) {
val version = (libsignalDep.requested as ModuleComponentSelector).version
file("libsignal-version").writeText(version + "\n")
} else {
throw GradleException("Could not find libsignal-client dependency")
}
}
}
tasks.register<JavaCompile>("jsonSchemas") {
dependsOn(tasks.compileJava)
val schemaBaseUri = "http://localhost:8080/schemas/"
source = sourceSets.main.get().java
include("org/asamk/signal/json/**/*.java")
classpath = sourceSets.main.get().compileClasspath + files(sourceSets.main.get().java.destinationDirectory)
destinationDirectory.set(layout.buildDirectory.dir("generated"))
options.annotationProcessorPath = schemaAnnotationProcessor
options.compilerArgs.addAll(
listOf(
"-Amicronaut.processing.group=org.asamk",
"-Amicronaut.processing.module=signal-cli",
"-Amicronaut.processing.annotations=org.asamk.signal.json.*",
"-Amicronaut.jsonschema.baseUri=$schemaBaseUri",
)
)
doLast {
fileTree(destinationDirectory.get().dir("META-INF/schemas").asFile) {
include("*.schema.json")
}.forEach { schemaFile ->
val normalized = schemaFile.readText().replace("\"$schemaBaseUri/", "\"")
val prettyJson = JsonOutput.prettyPrint(normalized)
schemaFile.writeText("$prettyJson\n")
}
}
}

View File

@ -7,11 +7,11 @@ plugins {
} }
tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure { tasks.named<KotlinCompilationTask<KotlinJvmCompilerOptions>>("compileKotlin").configure {
compilerOptions.jvmTarget.set(JvmTarget.JVM_17) compilerOptions.jvmTarget.set(JvmTarget.JVM_25)
} }
java { java {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_25
} }
repositories { repositories {

View File

@ -1,16 +1,12 @@
@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.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.Dependency
import javax.xml.parsers.DocumentBuilderFactory
class CheckLibVersionsPlugin : Plugin<Project> { class CheckLibVersionsPlugin : Plugin<Project> {
override fun apply(project: Project) { override fun apply(project: Project) {
project.task("checkLibVersions") { project.tasks.register("checkLibVersions") {
description = description =
"Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded." "Find any 3rd party libraries which have released new versions to the central Maven repo since we last upgraded."
doLast { doLast {
@ -28,15 +24,15 @@ class CheckLibVersionsPlugin : Plugin<Project> {
val name = dependency.name val name = dependency.name
val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" val metaDataUrl = "https://repo1.maven.org/maven2/$path/$name/maven-metadata.xml"
try { try {
val url = ResourceGroovyMethods.toURL(metaDataUrl) val dbf = DocumentBuilderFactory.newInstance()
val metaDataText = ResourceGroovyMethods.getText(url) val db = dbf.newDocumentBuilder()
val metadata = XmlSlurper().parseText(metaDataText) val doc = db.parse(metaDataUrl)
val newest = (metadata.getProperty("versioning") as GPathResult).getProperty("latest") val newest = doc.getElementsByTagName("latest").item(0).textContent
if (version != newest.toString()) { if (version != newest.toString()) {
println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}") println("UPGRADE {\"group\": \"$group\", \"name\": \"$name\", \"current\": \"$version\", \"latest\": \"$newest\"}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logger.debug("Unable to download or parse $metaDataUrl: $e.message") logger.debug("Unable to download or parse {}: {}", metaDataUrl, e.message)
} }
} }
} }

View 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
}
}
}
}
}

11
client.Containerfile Normal file
View File

@ -0,0 +1,11 @@
FROM docker.io/debian:testing-slim
LABEL org.opencontainers.image.source=https://github.com/AsamK/signal-cli
LABEL org.opencontainers.image.description="signal-cli provides an unofficial commandline, dbus and JSON-RPC interface for the Signal messenger."
LABEL org.opencontainers.image.licenses=GPL-3.0-only
RUN useradd signal-cli --system
ADD client/target/release/signal-cli-client /usr/bin/signal-cli-client
USER signal-cli
ENTRYPOINT ["/usr/bin/signal-cli-client"]

1424
client/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,17 @@
[package] [package]
name = "signal-cli-client" name = "signal-cli-client"
version = "0.0.1" version = "0.0.1"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
clap = { version = "4", features = ["cargo", "derive", "wrap_help"] } clap = { version = "4", features = ["cargo", "derive", "wrap_help"] }
log = "0.4"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] } tokio = { version = "1", features = ["rt", "macros", "net", "rt-multi-thread"] }
jsonrpsee = { version = "0.20.0", features = [ jsonrpsee = { version = "0.26", features = [
"macros", "macros",
"async-client", "async-client",
"http-client", "http-client",
@ -20,4 +19,4 @@ jsonrpsee = { version = "0.20.0", features = [
bytes = "1" bytes = "1"
tokio-util = "0.7" tokio-util = "0.7"
futures-util = "0.3" futures-util = "0.3"
thiserror = "1" thiserror = "2"

View File

@ -1,6 +1,7 @@
use clap::{crate_version, Parser, Subcommand, ValueEnum};
use std::{ffi::OsString, net::SocketAddr}; use std::{ffi::OsString, net::SocketAddr};
use clap::{crate_version, Parser, Subcommand, ValueEnum};
/// JSON-RPC client for signal-cli /// JSON-RPC client for signal-cli
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(rename_all = "kebab-case", version = crate_version!())] #[command(rename_all = "kebab-case", version = crate_version!())]
@ -9,11 +10,15 @@ pub struct Cli {
#[arg(short = 'a', long)] #[arg(short = 'a', long)]
pub account: Option<String>, pub account: Option<String>,
#[arg(long)]
pub output: Option<String>,
/// TCP host and port of signal-cli daemon /// TCP host and port of signal-cli daemon
#[arg(long, conflicts_with = "json_rpc_http")] #[arg(long, conflicts_with = "json_rpc_http")]
pub json_rpc_tcp: Option<Option<SocketAddr>>, pub json_rpc_tcp: Option<Option<SocketAddr>>,
/// UNIX socket address and port of signal-cli daemon /// UNIX socket address and port of signal-cli daemon
#[cfg(unix)]
#[arg(long, conflicts_with = "json_rpc_tcp")] #[arg(long, conflicts_with = "json_rpc_tcp")]
pub json_rpc_socket: Option<Option<OsString>>, pub json_rpc_socket: Option<Option<OsString>>,
@ -21,9 +26,6 @@ pub struct Cli {
#[arg(long, conflicts_with = "json_rpc_socket")] #[arg(long, conflicts_with = "json_rpc_socket")]
pub json_rpc_http: Option<Option<String>>, pub json_rpc_http: Option<Option<String>>,
#[arg(value_enum, long, default_value_t = OutputTypes::Json)]
pub output: OutputTypes,
#[arg(long)] #[arg(long)]
pub verbose: bool, pub verbose: bool,
@ -31,13 +33,6 @@ pub struct Cli {
pub command: CliCommands, pub command: CliCommands,
} }
#[derive(ValueEnum, Clone, Debug)]
#[value(rename_all = "kebab-case")]
pub enum OutputTypes {
PlainText,
Json,
}
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
#[command(rename_all = "camelCase", version = crate_version!())] #[command(rename_all = "camelCase", version = crate_version!())]
@ -46,6 +41,10 @@ pub enum CliCommands {
#[arg(long)] #[arg(long)]
uri: String, uri: String,
}, },
AddStickerPack {
#[arg(long)]
uri: String,
},
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
Block { Block {
recipient: Vec<String>, recipient: Vec<String>,
@ -57,8 +56,40 @@ pub enum CliCommands {
#[arg(long = "ignore-registered")] #[arg(long = "ignore-registered")]
ignore_registered: Option<bool>, ignore_registered: Option<bool>,
}, },
FinishChangeNumber {
number: String,
#[arg(short = 'v', long = "verification-code")]
verification_code: String,
#[arg(short = 'p', long)]
pin: Option<String>,
},
GetAttachment {
#[arg(long)]
id: String,
#[arg(long)]
recipient: Option<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Option<String>,
},
GetAvatar {
#[arg(long)]
contact: Option<String>,
#[arg(long)]
profile: Option<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Option<String>,
},
GetSticker {
#[arg(long = "pack-id")]
pack_id: String,
#[arg(long = "sticker-id")]
sticker_id: u32,
},
GetUserStatus { GetUserStatus {
recipient: Vec<String>, recipient: Vec<String>,
#[arg(long)]
username: Vec<String>,
}, },
JoinGroup { JoinGroup {
#[arg(long)] #[arg(long)]
@ -66,7 +97,7 @@ pub enum CliCommands {
}, },
Link { Link {
#[arg(short = 'n', long)] #[arg(short = 'n', long)]
name: String, name: Option<String>,
}, },
ListAccounts, ListAccounts,
ListContacts { ListContacts {
@ -77,6 +108,10 @@ pub enum CliCommands {
blocked: Option<bool>, blocked: Option<bool>,
#[arg(long)] #[arg(long)]
name: Option<String>, name: Option<String>,
#[arg(long)]
detailed: bool,
#[arg(long)]
internal: bool,
}, },
ListDevices, ListDevices,
ListGroups { ListGroups {
@ -107,11 +142,15 @@ pub enum CliCommands {
voice: bool, voice: bool,
#[arg(long)] #[arg(long)]
captcha: Option<String>, captcha: Option<String>,
#[arg(long)]
reregister: bool,
}, },
RemoveContact { RemoveContact {
recipient: String, recipient: String,
#[arg(long)] #[arg(long)]
forget: bool, forget: bool,
#[arg(long)]
hide: bool,
}, },
RemoveDevice { RemoveDevice {
#[arg(short = 'd', long = "device-id")] #[arg(short = 'd', long = "device-id")]
@ -137,21 +176,36 @@ pub enum CliCommands {
#[arg(short = 'g', long)] #[arg(short = 'g', long)]
group_id: Vec<String>, group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(long)] #[arg(long)]
note_to_self: bool, note_to_self: bool,
#[arg(long)]
notify_self: bool,
#[arg(short = 'e', long)] #[arg(short = 'e', long)]
end_session: bool, end_session: bool,
#[arg(short = 'm', long)] #[arg(short = 'm', long)]
message: Option<String>, message: Option<String>,
#[arg(long)]
message_from_stdin: bool,
#[arg(short = 'a', long)] #[arg(short = 'a', long)]
attachment: Vec<String>, attachment: Vec<String>,
#[arg(long)]
view_once: bool,
#[arg(long)] #[arg(long)]
mention: Vec<String>, mention: Vec<String>,
#[arg(long)]
text_style: Vec<String>,
#[arg(long)] #[arg(long)]
quote_timestamp: Option<u64>, quote_timestamp: Option<u64>,
@ -164,6 +218,24 @@ pub enum CliCommands {
#[arg(long)] #[arg(long)]
quote_mention: Vec<String>, quote_mention: Vec<String>,
#[arg(long)]
quote_text_style: Vec<String>,
#[arg(long)]
quote_attachment: Vec<String>,
#[arg(long)]
preview_url: Option<String>,
#[arg(long)]
preview_title: Option<String>,
#[arg(long)]
preview_description: Option<String>,
#[arg(long)]
preview_image: Option<String>,
#[arg(long)] #[arg(long)]
sticker: Option<String>, sticker: Option<String>,
@ -172,6 +244,28 @@ pub enum CliCommands {
#[arg(long)] #[arg(long)]
story_author: Option<String>, story_author: Option<String>,
#[arg(long)]
edit_timestamp: Option<u64>,
#[arg(long = "no-urgent")]
no_urgent: bool,
},
SendAdminDelete {
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'a', long = "target-author")]
target_author: String,
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: u64,
#[arg(long)]
story: bool,
#[arg(long)]
notify_self: bool,
}, },
SendContacts, SendContacts,
SendPaymentNotification { SendPaymentNotification {
@ -183,15 +277,117 @@ pub enum CliCommands {
#[arg(long)] #[arg(long)]
note: String, note: String,
}, },
SendPinMessage {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(short = 'a', long = "target-author")]
target_author: String,
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: u64,
#[arg(short = 'd', long = "pin-duration")]
pin_duration: Option<i32>,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
#[arg(long)]
story: bool,
},
SendPollCreate {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(short = 'q', long = "question")]
question: String,
#[arg(short = 'o', long = "option")]
option: Vec<String>,
#[arg(long = "no-multi")]
no_multi: bool,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
},
SendPollTerminate {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(long = "poll-timestamp")]
poll_timestamp: u64,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
},
SendPollVote {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(long = "poll-author")]
poll_author: Option<String>,
#[arg(long = "poll-timestamp")]
poll_timestamp: u64,
#[arg(short = 'o', long = "option")]
option: Vec<i32>,
#[arg(long = "vote-count")]
vote_count: i32,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
},
SendReaction { SendReaction {
recipient: Vec<String>, recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")] #[arg(short = 'g', long = "group-id")]
group_id: Vec<String>, group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(long = "note-to-self")] #[arg(long = "note-to-self")]
note_to_self: bool, note_to_self: bool,
#[arg(long)]
notify_self: bool,
#[arg(short = 'e', long)] #[arg(short = 'e', long)]
emoji: String, emoji: String,
@ -210,6 +406,9 @@ pub enum CliCommands {
SendReceipt { SendReceipt {
recipient: String, recipient: String,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(short = 't', long = "target-timestamp")] #[arg(short = 't', long = "target-timestamp")]
target_timestamp: Vec<u64>, target_timestamp: Vec<u64>,
@ -226,9 +425,49 @@ pub enum CliCommands {
#[arg(short = 's', long)] #[arg(short = 's', long)]
stop: bool, stop: bool,
}, },
SendUnpinMessage {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(short = 'u', long = "username")]
username: Vec<String>,
#[arg(short = 'a', long = "target-author")]
target_author: String,
#[arg(short = 't', long = "target-timestamp")]
target_timestamp: u64,
#[arg(long = "note-to-self")]
note_to_self: bool,
#[arg(long)]
notify_self: bool,
#[arg(long)]
story: bool,
},
SendMessageRequestResponse {
recipient: Vec<String>,
#[arg(short = 'g', long = "group-id")]
group_id: Vec<String>,
#[arg(long)]
r#type: MessageRequestResponseType,
},
SetPin { SetPin {
pin: String, pin: String,
}, },
StartChangeNumber {
number: String,
#[arg(short = 'v', long)]
voice: bool,
#[arg(long)]
captcha: Option<String>,
},
SubmitRateLimitChallenge { SubmitRateLimitChallenge {
challenge: String, challenge: String,
captcha: String, captcha: String,
@ -256,6 +495,16 @@ pub enum CliCommands {
UpdateAccount { UpdateAccount {
#[arg(short = 'n', long = "device-name")] #[arg(short = 'n', long = "device-name")]
device_name: Option<String>, device_name: Option<String>,
#[arg(long = "unrestricted-unidentified-sender")]
unrestricted_unidentified_sender: Option<bool>,
#[arg(long = "discoverable-by-number")]
discoverable_by_number: Option<bool>,
#[arg(long = "number-sharing")]
number_sharing: Option<bool>,
#[arg(short = 'u', long = "username")]
username: Option<String>,
#[arg(long = "delete-username")]
delete_username: bool,
}, },
UpdateConfiguration { UpdateConfiguration {
#[arg(long = "read-receipts")] #[arg(long = "read-receipts")]
@ -278,6 +527,28 @@ pub enum CliCommands {
#[arg(short = 'n', long)] #[arg(short = 'n', long)]
name: Option<String>, name: Option<String>,
#[arg(long = "given-name")]
given_name: Option<String>,
#[arg(long = "family-name")]
family_name: Option<String>,
#[arg(long = "nick-given-name")]
nick_given_name: Option<String>,
#[arg(long = "nick-family-name")]
nick_family_name: Option<String>,
#[arg(long)]
note: Option<String>,
},
UpdateDevice {
#[arg(short = 'd', long = "device-id")]
device_id: u32,
#[arg(short = 'n', long = "device-name")]
device_name: String,
}, },
UpdateGroup { UpdateGroup {
#[arg(short = 'g', long = "group-id")] #[arg(short = 'g', long = "group-id")]
@ -327,6 +598,12 @@ pub enum CliCommands {
#[arg(short = 'e', long)] #[arg(short = 'e', long)]
expiration: Option<u32>, expiration: Option<u32>,
#[arg(long = "member-label-emoji")]
member_label_emoji: Option<String>,
#[arg(long = "member-label")]
member_label: Option<String>,
}, },
UpdateProfile { UpdateProfile {
#[arg(long = "given-name")] #[arg(long = "given-name")]
@ -341,7 +618,7 @@ pub enum CliCommands {
#[arg(long = "about-emoji")] #[arg(long = "about-emoji")]
about_emoji: Option<String>, about_emoji: Option<String>,
#[arg(long = "mobile-coin-address")] #[arg(long = "mobile-coin-address", visible_alias = "mobilecoin-address")]
mobile_coin_address: Option<String>, mobile_coin_address: Option<String>,
#[arg(long)] #[arg(long)]
@ -383,3 +660,10 @@ pub enum GroupPermission {
EveryMember, EveryMember,
OnlyAdmins, OnlyAdmins,
} }
#[derive(ValueEnum, Clone, Debug)]
#[value(rename_all = "kebab-case")]
pub enum MessageRequestResponseType {
Accept,
Delete,
}

View File

@ -1,8 +1,7 @@
use std::path::Path; use std::path::Path;
use jsonrpsee::async_client::ClientBuilder; use jsonrpsee::async_client::ClientBuilder;
use jsonrpsee::core::client::SubscriptionClientT; use jsonrpsee::core::client::{Error, SubscriptionClientT};
use jsonrpsee::core::Error;
use jsonrpsee::http_client::HttpClientBuilder; use jsonrpsee::http_client::HttpClientBuilder;
use jsonrpsee::proc_macros::rpc; use jsonrpsee::proc_macros::rpc;
use serde::Deserialize; use serde::Deserialize;
@ -18,6 +17,13 @@ pub trait Rpc {
uri: String, uri: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "addStickerPack", param_kind = map)]
async fn add_sticker_pack(
&self,
account: Option<String>,
uri: String,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "block", param_kind = map)] #[method(name = "block", param_kind = map)]
fn block( fn block(
&self, &self,
@ -33,21 +39,58 @@ pub trait Rpc {
#[allow(non_snake_case)] ignoreRegistered: Option<bool>, #[allow(non_snake_case)] ignoreRegistered: Option<bool>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "getAttachment", param_kind = map)]
fn get_attachment(
&self,
account: Option<String>,
id: String,
recipient: Option<String>,
#[allow(non_snake_case)] groupId: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "getAvatar", param_kind = map)]
fn get_avatar(
&self,
account: Option<String>,
contact: Option<String>,
profile: Option<String>,
#[allow(non_snake_case)] groupId: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "getSticker", param_kind = map)]
fn get_sticker(
&self,
account: Option<String>,
#[allow(non_snake_case)] packId: String,
#[allow(non_snake_case)] stickerId: u32,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "getUserStatus", param_kind = map)] #[method(name = "getUserStatus", param_kind = map)]
fn get_user_status( fn get_user_status(
&self, &self,
account: Option<String>, account: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
usernames: Vec<String>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "joinGroup", param_kind = map)] #[method(name = "joinGroup", param_kind = map)]
fn join_group(&self, account: Option<String>, uri: String) -> Result<Value, ErrorObjectOwned>; fn join_group(&self, account: Option<String>, uri: String) -> Result<Value, ErrorObjectOwned>;
#[allow(non_snake_case)]
#[method(name = "finishChangeNumber", param_kind = map)]
fn finish_change_number(
&self,
account: Option<String>,
number: String,
verificationCode: String,
pin: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "finishLink", param_kind = map)] #[method(name = "finishLink", param_kind = map)]
fn finish_link( fn finish_link(
&self, &self,
#[allow(non_snake_case)] deviceLinkUri: String, #[allow(non_snake_case)] deviceLinkUri: String,
#[allow(non_snake_case)] deviceName: String, #[allow(non_snake_case)] deviceName: Option<String>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listAccounts", param_kind = map)] #[method(name = "listAccounts", param_kind = map)]
@ -61,6 +104,8 @@ pub trait Rpc {
#[allow(non_snake_case)] allRecipients: bool, #[allow(non_snake_case)] allRecipients: bool,
blocked: Option<bool>, blocked: Option<bool>,
name: Option<String>, name: Option<String>,
detailed: bool,
internal: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "listDevices", param_kind = map)] #[method(name = "listDevices", param_kind = map)]
@ -98,6 +143,7 @@ pub trait Rpc {
account: Option<String>, account: Option<String>,
voice: bool, voice: bool,
captcha: Option<String>, captcha: Option<String>,
reregister: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "removeContact", param_kind = map)] #[method(name = "removeContact", param_kind = map)]
@ -106,6 +152,7 @@ pub trait Rpc {
account: Option<String>, account: Option<String>,
recipient: String, recipient: String,
forget: bool, forget: bool,
hide: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "removeDevice", param_kind = map)] #[method(name = "removeDevice", param_kind = map)]
@ -128,29 +175,123 @@ pub trait Rpc {
#[allow(non_snake_case)] noteToSelf: bool, #[allow(non_snake_case)] noteToSelf: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[allow(non_snake_case)]
#[method(name = "send", param_kind = map)] #[method(name = "send", param_kind = map)]
fn send( fn send(
&self, &self,
account: Option<String>, account: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>, groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] notifySelf: bool,
#[allow(non_snake_case)] noteToSelf: bool, #[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] endSession: bool, #[allow(non_snake_case)] endSession: bool,
message: String, message: String,
attachments: Vec<String>, attachments: Vec<String>,
#[allow(non_snake_case)] viewOnce: bool,
mentions: Vec<String>, mentions: Vec<String>,
#[allow(non_snake_case)] textStyle: Vec<String>,
#[allow(non_snake_case)] quoteTimestamp: Option<u64>, #[allow(non_snake_case)] quoteTimestamp: Option<u64>,
#[allow(non_snake_case)] quoteAuthor: Option<String>, #[allow(non_snake_case)] quoteAuthor: Option<String>,
#[allow(non_snake_case)] quoteMessage: Option<String>, #[allow(non_snake_case)] quoteMessage: Option<String>,
#[allow(non_snake_case)] quoteMention: Vec<String>, #[allow(non_snake_case)] quoteMention: Vec<String>,
#[allow(non_snake_case)] quoteTextStyle: Vec<String>,
#[allow(non_snake_case)] quoteAttachment: Vec<String>,
#[allow(non_snake_case)] previewUrl: Option<String>,
#[allow(non_snake_case)] previewTitle: Option<String>,
#[allow(non_snake_case)] previewDescription: Option<String>,
#[allow(non_snake_case)] previewImage: Option<String>,
sticker: Option<String>, sticker: Option<String>,
#[allow(non_snake_case)] storyTimestamp: Option<u64>, #[allow(non_snake_case)] storyTimestamp: Option<u64>,
#[allow(non_snake_case)] storyAuthor: Option<String>, #[allow(non_snake_case)] storyAuthor: Option<String>,
#[allow(non_snake_case)] editTimestamp: Option<u64>,
#[allow(non_snake_case)] noUrgent: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendContacts", param_kind = map)] #[method(name = "sendContacts", param_kind = map)]
fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>; fn send_contacts(&self, account: Option<String>) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendAdminDelete", param_kind = map)]
fn send_admin_delete(
&self,
account: Option<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
#[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64,
story: bool,
#[allow(non_snake_case)] notifySelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPinMessage", param_kind = map)]
fn send_pin_message(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64,
#[allow(non_snake_case)] pinDuration: Option<i32>,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
story: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPollCreate", param_kind = map)]
fn send_poll_create(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
question: String,
option: Vec<String>,
#[allow(non_snake_case)] noMulti: bool,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPollVote", param_kind = map)]
fn send_poll_vote(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] pollAuthor: Option<String>,
#[allow(non_snake_case)] pollTimestamp: u64,
option: Vec<i32>,
#[allow(non_snake_case)] voteCount: i32,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPollTerminate", param_kind = map)]
fn send_poll_terminate(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] pollTimestamp: u64,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendUnpinMessage", param_kind = map)]
fn send_unpin_message(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64,
#[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
story: bool,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendPaymentNotification", param_kind = map)] #[method(name = "sendPaymentNotification", param_kind = map)]
fn send_payment_notification( fn send_payment_notification(
&self, &self,
@ -166,7 +307,9 @@ pub trait Rpc {
account: Option<String>, account: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>, #[allow(non_snake_case)] groupIds: Vec<String>,
usernames: Vec<String>,
#[allow(non_snake_case)] noteToSelf: bool, #[allow(non_snake_case)] noteToSelf: bool,
#[allow(non_snake_case)] notifySelf: bool,
emoji: String, emoji: String,
#[allow(non_snake_case)] targetAuthor: String, #[allow(non_snake_case)] targetAuthor: String,
#[allow(non_snake_case)] targetTimestamp: u64, #[allow(non_snake_case)] targetTimestamp: u64,
@ -179,6 +322,7 @@ pub trait Rpc {
&self, &self,
account: Option<String>, account: Option<String>,
recipient: String, recipient: String,
usernames: Vec<String>,
#[allow(non_snake_case)] targetTimestamps: Vec<u64>, #[allow(non_snake_case)] targetTimestamps: Vec<u64>,
r#type: String, r#type: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
@ -195,6 +339,15 @@ pub trait Rpc {
stop: bool, stop: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "sendMessageRequestResponse", param_kind = map)]
fn send_message_request_response(
&self,
account: Option<String>,
recipients: Vec<String>,
#[allow(non_snake_case)] groupIds: Vec<String>,
r#type: String,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "setPin", param_kind = map)] #[method(name = "setPin", param_kind = map)]
fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>; fn set_pin(&self, account: Option<String>, pin: String) -> Result<Value, ErrorObjectOwned>;
@ -206,6 +359,15 @@ pub trait Rpc {
captcha: String, captcha: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "startChangeNumber", param_kind = map)]
fn start_change_number(
&self,
account: Option<String>,
number: String,
voice: bool,
captcha: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "startLink", param_kind = map)] #[method(name = "startLink", param_kind = map)]
fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>; fn start_link(&self, account: Option<String>) -> Result<JsonLink, ErrorObjectOwned>;
@ -233,18 +395,24 @@ pub trait Rpc {
#[allow(non_snake_case)] deleteAccount: bool, #[allow(non_snake_case)] deleteAccount: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[allow(non_snake_case)]
#[method(name = "updateAccount", param_kind = map)] #[method(name = "updateAccount", param_kind = map)]
fn update_account( fn update_account(
&self, &self,
account: Option<String>, account: Option<String>,
#[allow(non_snake_case)] deviceName: Option<String>, deviceName: Option<String>,
unrestrictedUnidentifiedSender: Option<bool>,
discoverableByNumber: Option<bool>,
numberSharing: Option<bool>,
username: Option<String>,
deleteUsername: bool,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateConfiguration", param_kind = map)] #[method(name = "updateConfiguration", param_kind = map)]
fn update_configuration( fn update_configuration(
&self, &self,
account: Option<String>, account: Option<String>,
#[allow(non_snake_case)] readReceiptes: Option<bool>, #[allow(non_snake_case)] readReceipts: Option<bool>,
#[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>, #[allow(non_snake_case)] unidentifiedDeliveryIndicators: Option<bool>,
#[allow(non_snake_case)] typingIndicators: Option<bool>, #[allow(non_snake_case)] typingIndicators: Option<bool>,
#[allow(non_snake_case)] linkPreviews: Option<bool>, #[allow(non_snake_case)] linkPreviews: Option<bool>,
@ -257,6 +425,19 @@ pub trait Rpc {
recipient: String, recipient: String,
name: Option<String>, name: Option<String>,
expiration: Option<u32>, expiration: Option<u32>,
#[allow(non_snake_case)] givenName: Option<String>,
#[allow(non_snake_case)] familyName: Option<String>,
#[allow(non_snake_case)] nickGivenName: Option<String>,
#[allow(non_snake_case)] nickFamilyName: Option<String>,
note: Option<String>,
) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateDevice", param_kind = map)]
fn update_device(
&self,
account: Option<String>,
#[allow(non_snake_case)] deviceId: u32,
#[allow(non_snake_case)] deviceName: String,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateGroup", param_kind = map)] #[method(name = "updateGroup", param_kind = map)]
@ -279,6 +460,8 @@ pub trait Rpc {
#[allow(non_snake_case)] setPermissionEditDetails: Option<String>, #[allow(non_snake_case)] setPermissionEditDetails: Option<String>,
#[allow(non_snake_case)] setPermissionSendMessages: Option<String>, #[allow(non_snake_case)] setPermissionSendMessages: Option<String>,
expiration: Option<u32>, expiration: Option<u32>,
#[allow(non_snake_case)] memberLabelEmoji: Option<String>,
#[allow(non_snake_case)] memberLabel: Option<String>,
) -> Result<Value, ErrorObjectOwned>; ) -> Result<Value, ErrorObjectOwned>;
#[method(name = "updateProfile", param_kind = map)] #[method(name = "updateProfile", param_kind = map)]
@ -327,20 +510,23 @@ pub struct JsonLink {
pub device_link_uri: String, pub device_link_uri: String,
} }
pub async fn connect_tcp(tcp: impl ToSocketAddrs) -> Result<impl SubscriptionClientT, Error> { pub async fn connect_tcp(
tcp: impl ToSocketAddrs,
) -> Result<impl SubscriptionClientT, std::io::Error> {
let (sender, receiver) = super::transports::tcp::connect(tcp).await?; let (sender, receiver) = super::transports::tcp::connect(tcp).await?;
Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) Ok(ClientBuilder::default().build_with_tokio(sender, receiver))
} }
#[cfg(unix)]
pub async fn connect_unix( pub async fn connect_unix(
socket_path: impl AsRef<Path>, socket_path: impl AsRef<Path>,
) -> Result<impl SubscriptionClientT, Error> { ) -> Result<impl SubscriptionClientT, std::io::Error> {
let (sender, receiver) = super::transports::ipc::connect(socket_path).await?; let (sender, receiver) = super::transports::ipc::connect(socket_path).await?;
Ok(ClientBuilder::default().build_with_tokio(sender, receiver)) 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) HttpClientBuilder::default().build(uri)
} }

View File

@ -1,13 +1,13 @@
use std::{path::PathBuf, time::Duration}; use std::{path::PathBuf, time::Duration};
use clap::Parser; use clap::Parser;
use cli::Cli; use jsonrpsee::core::client::{Error as RpcError, Subscription, SubscriptionClientT};
use jsonrpsee::core::client::{Subscription, SubscriptionClientT}; use serde_json::{Error, Value};
use jsonrpsee::core::Error as RpcError;
use serde_json::Value;
use tokio::{select, time::sleep}; use tokio::{select, time::sleep};
use crate::cli::{GroupPermission, LinkState}; use cli::Cli;
use crate::cli::{CliCommands, GroupPermission, LinkState};
use crate::jsonrpc::RpcClient; use crate::jsonrpc::RpcClient;
mod cli; mod cli;
@ -38,7 +38,7 @@ async fn handle_command(
client: impl SubscriptionClientT + Sync, client: impl SubscriptionClientT + Sync,
) -> Result<Value, RpcError> { ) -> Result<Value, RpcError> {
match cli.command { match cli.command {
cli::CliCommands::Receive { timeout } => { CliCommands::Receive { timeout } => {
let mut stream = client.subscribe_receive(cli.account).await?; let mut stream = client.subscribe_receive(cli.account).await?;
{ {
@ -50,50 +50,63 @@ async fn handle_command(
stream.unsubscribe().await?; stream.unsubscribe().await?;
Ok(Value::Null) Ok(Value::Null)
} }
cli::CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await, CliCommands::AddDevice { uri } => client.add_device(cli.account, uri).await,
cli::CliCommands::Block { CliCommands::Block {
recipient, recipient,
group_id, group_id,
} => client.block(cli.account, recipient, group_id).await, } => client.block(cli.account, recipient, group_id).await,
cli::CliCommands::DeleteLocalAccountData { ignore_registered } => { CliCommands::DeleteLocalAccountData { ignore_registered } => {
client client
.delete_local_account_data(cli.account, ignore_registered) .delete_local_account_data(cli.account, ignore_registered)
.await .await
} }
cli::CliCommands::GetUserStatus { recipient } => { CliCommands::GetUserStatus {
client.get_user_status(cli.account, recipient).await recipient,
username,
} => {
client
.get_user_status(cli.account, recipient, username)
.await
} }
cli::CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await, CliCommands::JoinGroup { uri } => client.join_group(cli.account, uri).await,
cli::CliCommands::Link { name } => { CliCommands::Link { name } => {
let url = client let url = client
.start_link(cli.account) .start_link(cli.account)
.await .await
.map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))? .map_err(|e| RpcError::Custom(format!("JSON-RPC command startLink failed: {e:?}")))?
.device_link_uri; .device_link_uri;
println!("{}", url); println!("{url}");
client.finish_link(url, name).await client.finish_link(url, name).await
} }
cli::CliCommands::ListAccounts => client.list_accounts().await, CliCommands::ListAccounts => client.list_accounts().await,
cli::CliCommands::ListContacts { CliCommands::ListContacts {
recipient, recipient,
all_recipients, all_recipients,
blocked, blocked,
name, name,
detailed,
internal,
} => { } => {
client client
.list_contacts(cli.account, recipient, all_recipients, blocked, name) .list_contacts(
cli.account,
recipient,
all_recipients,
blocked,
name,
detailed,
internal,
)
.await .await
} }
cli::CliCommands::ListDevices => client.list_devices(cli.account).await, CliCommands::ListDevices => client.list_devices(cli.account).await,
cli::CliCommands::ListGroups { CliCommands::ListGroups {
detailed: _, detailed: _,
group_id, group_id,
} => client.list_groups(cli.account, group_id).await, } => client.list_groups(cli.account, group_id).await,
cli::CliCommands::ListIdentities { number } => { CliCommands::ListIdentities { number } => client.list_identities(cli.account, number).await,
client.list_identities(cli.account, number).await CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
} CliCommands::QuitGroup {
cli::CliCommands::ListStickerPacks => client.list_sticker_packs(cli.account).await,
cli::CliCommands::QuitGroup {
group_id, group_id,
delete, delete,
admin, admin,
@ -102,17 +115,29 @@ async fn handle_command(
.quit_group(cli.account, group_id, delete, admin) .quit_group(cli.account, group_id, delete, admin)
.await .await
} }
cli::CliCommands::Register { voice, captcha } => { CliCommands::Register {
client.register(cli.account, voice, captcha).await voice,
captcha,
reregister,
} => {
client
.register(cli.account, voice, captcha, reregister)
.await
} }
cli::CliCommands::RemoveContact { recipient, forget } => { CliCommands::RemoveContact {
client.remove_contact(cli.account, recipient, forget).await recipient,
forget,
hide,
} => {
client
.remove_contact(cli.account, recipient, forget, hide)
.await
} }
cli::CliCommands::RemoveDevice { device_id } => { CliCommands::RemoveDevice { device_id } => {
client.remove_device(cli.account, device_id).await client.remove_device(cli.account, device_id).await
} }
cli::CliCommands::RemovePin => client.remove_pin(cli.account).await, CliCommands::RemovePin => client.remove_pin(cli.account).await,
cli::CliCommands::RemoteDelete { CliCommands::RemoteDelete {
target_timestamp, target_timestamp,
recipient, recipient,
group_id, group_id,
@ -128,44 +153,91 @@ async fn handle_command(
) )
.await .await
} }
cli::CliCommands::Send { CliCommands::Send {
recipient, recipient,
group_id, group_id,
username,
notify_self,
note_to_self, note_to_self,
end_session, end_session,
message, message,
message_from_stdin,
attachment, attachment,
view_once,
mention, mention,
text_style,
quote_timestamp, quote_timestamp,
quote_author, quote_author,
quote_message, quote_message,
quote_mention, quote_mention,
quote_text_style,
quote_attachment,
preview_url,
preview_title,
preview_description,
preview_image,
sticker, sticker,
story_timestamp, story_timestamp,
story_author, story_author,
edit_timestamp,
no_urgent,
} => { } => {
client client
.send( .send(
cli.account, cli.account,
recipient, recipient,
group_id, group_id,
username,
notify_self,
note_to_self, note_to_self,
end_session, end_session,
message.unwrap_or_default(), if message_from_stdin {
std::io::read_to_string(std::io::stdin()).unwrap()
} else {
message.unwrap_or_default()
},
attachment, attachment,
view_once,
mention, mention,
text_style,
quote_timestamp, quote_timestamp,
quote_author, quote_author,
quote_message, quote_message,
quote_mention, quote_mention,
quote_text_style,
quote_attachment,
preview_url,
preview_title,
preview_description,
preview_image,
sticker, sticker,
story_timestamp, story_timestamp,
story_author, story_author,
edit_timestamp,
no_urgent,
) )
.await .await
} }
cli::CliCommands::SendContacts => client.send_contacts(cli.account).await, CliCommands::SendContacts => client.send_contacts(cli.account).await,
cli::CliCommands::SendPaymentNotification { CliCommands::SendAdminDelete {
group_id,
target_author,
target_timestamp,
story,
notify_self,
} => {
client
.send_admin_delete(
cli.account,
group_id,
target_author,
target_timestamp,
story,
notify_self,
)
.await
}
CliCommands::SendPaymentNotification {
recipient, recipient,
receipt, receipt,
note, note,
@ -174,10 +246,108 @@ async fn handle_command(
.send_payment_notification(cli.account, recipient, receipt, note) .send_payment_notification(cli.account, recipient, receipt, note)
.await .await
} }
cli::CliCommands::SendReaction { CliCommands::SendPinMessage {
recipient, recipient,
group_id, group_id,
username,
target_author,
target_timestamp,
pin_duration,
note_to_self, note_to_self,
notify_self,
story,
} => {
client
.send_pin_message(
cli.account,
recipient,
group_id,
username,
target_author,
target_timestamp,
pin_duration,
note_to_self,
notify_self,
story,
)
.await
}
CliCommands::SendPollCreate {
recipient,
group_id,
username,
question,
option,
no_multi,
note_to_self,
notify_self,
} => {
client
.send_poll_create(
cli.account,
recipient,
group_id,
username,
question,
option,
no_multi,
note_to_self,
notify_self,
)
.await
}
CliCommands::SendPollTerminate {
recipient,
group_id,
username,
poll_timestamp,
note_to_self,
notify_self,
} => {
client
.send_poll_terminate(
cli.account,
recipient,
group_id,
username,
poll_timestamp,
note_to_self,
notify_self,
)
.await
}
CliCommands::SendPollVote {
recipient,
group_id,
username,
poll_author,
poll_timestamp,
option,
vote_count,
note_to_self,
notify_self,
} => {
client
.send_poll_vote(
cli.account,
recipient,
group_id,
username,
poll_author,
poll_timestamp,
option,
vote_count,
note_to_self,
notify_self,
)
.await
}
CliCommands::SendReaction {
recipient,
group_id,
username,
note_to_self,
notify_self,
emoji, emoji,
target_author, target_author,
target_timestamp, target_timestamp,
@ -189,7 +359,9 @@ async fn handle_command(
cli.account, cli.account,
recipient, recipient,
group_id, group_id,
username,
note_to_self, note_to_self,
notify_self,
emoji, emoji,
target_author, target_author,
target_timestamp, target_timestamp,
@ -198,8 +370,9 @@ async fn handle_command(
) )
.await .await
} }
cli::CliCommands::SendReceipt { CliCommands::SendReceipt {
recipient, recipient,
username,
target_timestamp, target_timestamp,
r#type, r#type,
} => { } => {
@ -207,6 +380,7 @@ async fn handle_command(
.send_receipt( .send_receipt(
cli.account, cli.account,
recipient, recipient,
username,
target_timestamp, target_timestamp,
match r#type { match r#type {
cli::ReceiptType::Read => "read".to_owned(), cli::ReceiptType::Read => "read".to_owned(),
@ -215,8 +389,8 @@ async fn handle_command(
) )
.await .await
} }
cli::CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await, CliCommands::SendSyncRequest => client.send_sync_request(cli.account).await,
cli::CliCommands::SendTyping { CliCommands::SendTyping {
recipient, recipient,
group_id, group_id,
stop, stop,
@ -225,13 +399,37 @@ async fn handle_command(
.send_typing(cli.account, recipient, group_id, stop) .send_typing(cli.account, recipient, group_id, stop)
.await .await
} }
cli::CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await, CliCommands::SendUnpinMessage {
cli::CliCommands::SubmitRateLimitChallenge { challenge, captcha } => { recipient,
group_id,
username,
target_author,
target_timestamp,
note_to_self,
notify_self,
story,
} => {
client
.send_unpin_message(
cli.account,
recipient,
group_id,
username,
target_author,
target_timestamp,
note_to_self,
notify_self,
story,
)
.await
}
CliCommands::SetPin { pin } => client.set_pin(cli.account, pin).await,
CliCommands::SubmitRateLimitChallenge { challenge, captcha } => {
client client
.submit_rate_limit_challenge(cli.account, challenge, captcha) .submit_rate_limit_challenge(cli.account, challenge, captcha)
.await .await
} }
cli::CliCommands::Trust { CliCommands::Trust {
recipient, recipient,
trust_all_known_keys, trust_all_known_keys,
verified_safety_number, verified_safety_number,
@ -245,17 +443,34 @@ async fn handle_command(
) )
.await .await
} }
cli::CliCommands::Unblock { CliCommands::Unblock {
recipient, recipient,
group_id, group_id,
} => client.unblock(cli.account, recipient, group_id).await, } => client.unblock(cli.account, recipient, group_id).await,
cli::CliCommands::Unregister { delete_account } => { CliCommands::Unregister { delete_account } => {
client.unregister(cli.account, delete_account).await client.unregister(cli.account, delete_account).await
} }
cli::CliCommands::UpdateAccount { device_name } => { CliCommands::UpdateAccount {
client.update_account(cli.account, device_name).await device_name,
unrestricted_unidentified_sender,
discoverable_by_number,
number_sharing,
username,
delete_username,
} => {
client
.update_account(
cli.account,
device_name,
unrestricted_unidentified_sender,
discoverable_by_number,
number_sharing,
username,
delete_username,
)
.await
} }
cli::CliCommands::UpdateConfiguration { CliCommands::UpdateConfiguration {
read_receipts, read_receipts,
unidentified_delivery_indicators, unidentified_delivery_indicators,
typing_indicators, typing_indicators,
@ -271,16 +486,39 @@ async fn handle_command(
) )
.await .await
} }
cli::CliCommands::UpdateContact { CliCommands::UpdateContact {
recipient, recipient,
expiration, expiration,
name, name,
given_name,
family_name,
nick_given_name,
nick_family_name,
note,
} => { } => {
client client
.update_contact(cli.account, recipient, name, expiration) .update_contact(
cli.account,
recipient,
name,
expiration,
given_name,
family_name,
nick_given_name,
nick_family_name,
note,
)
.await .await
} }
cli::CliCommands::UpdateGroup { CliCommands::UpdateDevice {
device_id,
device_name,
} => {
client
.update_device(cli.account, device_id, device_name)
.await
}
CliCommands::UpdateGroup {
group_id, group_id,
name, name,
description, description,
@ -297,6 +535,8 @@ async fn handle_command(
set_permission_edit_details, set_permission_edit_details,
set_permission_send_messages, set_permission_send_messages,
expiration, expiration,
member_label_emoji,
member_label,
} => { } => {
client client
.update_group( .update_group(
@ -330,10 +570,12 @@ async fn handle_command(
GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(), GroupPermission::OnlyAdmins => "onlyAdmins".to_owned(),
}), }),
expiration, expiration,
member_label_emoji,
member_label,
) )
.await .await
} }
cli::CliCommands::UpdateProfile { CliCommands::UpdateProfile {
given_name, given_name,
family_name, family_name,
about, about,
@ -355,14 +597,72 @@ async fn handle_command(
) )
.await .await
} }
cli::CliCommands::UploadStickerPack { path } => { CliCommands::UploadStickerPack { path } => {
client.upload_sticker_pack(cli.account, path).await client.upload_sticker_pack(cli.account, path).await
} }
cli::CliCommands::Verify { CliCommands::Verify {
verification_code, verification_code,
pin, pin,
} => client.verify(cli.account, verification_code, pin).await, } => client.verify(cli.account, verification_code, pin).await,
cli::CliCommands::Version => client.version().await, CliCommands::Version => client.version().await,
CliCommands::AddStickerPack { uri } => client.add_sticker_pack(cli.account, uri).await,
CliCommands::FinishChangeNumber {
number,
verification_code,
pin,
} => {
client
.finish_change_number(cli.account, number, verification_code, pin)
.await
}
CliCommands::GetAttachment {
id,
recipient,
group_id,
} => {
client
.get_attachment(cli.account, id, recipient, group_id)
.await
}
CliCommands::GetAvatar {
contact,
profile,
group_id,
} => {
client
.get_avatar(cli.account, contact, profile, group_id)
.await
}
CliCommands::GetSticker {
pack_id,
sticker_id,
} => client.get_sticker(cli.account, pack_id, sticker_id).await,
CliCommands::StartChangeNumber {
number,
voice,
captcha,
} => {
client
.start_change_number(cli.account, number, voice, captcha)
.await
}
CliCommands::SendMessageRequestResponse {
recipient,
group_id,
r#type,
} => {
client
.send_message_request_response(
cli.account,
recipient,
group_id,
match r#type {
cli::MessageRequestResponseType::Accept => "accept".to_owned(),
cli::MessageRequestResponseType::Delete => "delete".to_owned(),
},
)
.await
}
} }
} }
@ -386,6 +686,12 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await handle_command(cli, client).await
} else { } else {
#[cfg(windows)]
{
Err(RpcError::Custom("Invalid socket".into()))
}
#[cfg(unix)]
{
let socket_path = cli let socket_path = cli
.json_rpc_socket .json_rpc_socket
.clone() .clone()
@ -405,11 +711,12 @@ async fn connect(cli: Cli) -> Result<Value, RpcError> {
handle_command(cli, client).await handle_command(cli, client).await
} }
} }
}
async fn stream_next( async fn stream_next(
timeout: f64, timeout: f64,
stream: &mut Subscription<Value>, stream: &mut Subscription<Value>,
) -> Option<Result<Value, RpcError>> { ) -> Option<Result<Value, Error>> {
if timeout < 0.0 { if timeout < 0.0 {
stream.next().await stream.next().await
} else { } else {

View File

@ -1,8 +1,8 @@
use std::io::Error;
use std::path::Path; use std::path::Path;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT}; use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
use jsonrpsee::core::Error;
use tokio::net::UnixStream; use tokio::net::UnixStream;
use tokio_util::codec::Decoder; use tokio_util::codec::Decoder;

View File

@ -1,10 +1,8 @@
use futures_util::{stream::StreamExt, Sink, SinkExt, Stream}; use futures_util::{stream::StreamExt, Sink, SinkExt, Stream};
use jsonrpsee::core::{ use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
async_trait,
client::{ReceivedMessage, TransportReceiverT, TransportSenderT},
};
use thiserror::Error; use thiserror::Error;
#[cfg(unix)]
pub mod ipc; pub mod ipc;
mod stream_codec; mod stream_codec;
pub mod tcp; pub mod tcp;
@ -21,7 +19,6 @@ struct Sender<T: Send + Sink<String>> {
inner: T, inner: T,
} }
#[async_trait]
impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> TransportSenderT
for Sender<T> for Sender<T>
{ {
@ -31,7 +28,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner self.inner
.send(body) .send(body)
.await .await
.map_err(|e| Errors::Other(format!("{:?}", e)))?; .map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(()) Ok(())
} }
@ -39,7 +36,7 @@ impl<T: Send + Sink<String, Error = impl std::error::Error> + Unpin + 'static> T
self.inner self.inner
.close() .close()
.await .await
.map_err(|e| Errors::Other(format!("{:?}", e)))?; .map_err(|e| Errors::Other(format!("{e:?}")))?;
Ok(()) Ok(())
} }
} }
@ -48,7 +45,6 @@ struct Receiver<T: Send + Stream> {
inner: T, inner: T,
} }
#[async_trait]
impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static> TransportReceiverT
for Receiver<T> for Receiver<T>
{ {
@ -58,7 +54,7 @@ impl<T: Send + Stream<Item = Result<String, std::io::Error>> + Unpin + 'static>
match self.inner.next().await { match self.inner.next().await {
None => Err(Errors::Closed), None => Err(Errors::Closed),
Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)), Some(Ok(msg)) => Ok(ReceivedMessage::Text(msg)),
Some(Err(e)) => Err(Errors::Other(format!("{:?}", e))), Some(Err(e)) => Err(Errors::Other(format!("{e:?}"))),
} }
} }
} }

View File

@ -41,7 +41,7 @@ impl Decoder for StreamCodec {
match str::from_utf8(line.as_ref()) { match str::from_utf8(line.as_ref()) {
Ok(s) => Ok(Some(s.to_string())), 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 { } else {
Ok(None) Ok(None)

View File

@ -1,6 +1,7 @@
use std::io::Error;
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT}; use jsonrpsee::core::client::{TransportReceiverT, TransportSenderT};
use jsonrpsee::core::Error;
use tokio::net::{TcpStream, ToSocketAddrs}; use tokio::net::{TcpStream, ToSocketAddrs};
use tokio_util::codec::Decoder; use tokio_util::codec::Decoder;

View File

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="console-application">
<id>org.asamk.SignalCli</id>
<name>signal-cli</name>
<summary>Use Signal messenger in terminal</summary>
<developer id="org.asamk">
<name>AsamK</name>
</developer>
<icon type="stock">org.asamk.SignalCli</icon>
<keywords>
<keyword>signal</keyword>
<keyword>signal-cli</keyword>
<keyword>messenger</keyword>
<keyword>messaging</keyword>
</keywords>
<url type="bugtracker">https://github.com/AsamK/signal-cli/issues</url>
<url type="homepage">https://github.com/AsamK/signal-cli</url>
<url type="donation">https://github.com/sponsors/AsamK</url>
<url type="faq">https://github.com/AsamK/signal-cli/discussions</url>
<url type="vcs-browser">https://github.com/AsamK/signal-cli</url>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<description>
<p>
signal-cli is an unofficial commandline interface for the Signal Messenger.
It supports many Signal functions, including registering, verifying, sending and receiving messages.
For registering you need a phone number where you can receive SMS or incoming calls.
Alternatively signal-cli can be linked to an existing App account.
</p>
</description>
<categories>
<category>Utility</category>
<category>Java</category>
</categories>
<provides>
<binary>signal-cli</binary>
</provides>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<releases>
<release version="0.14.7" date="2026-08-01">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.7</url>
</release>
<release version="0.14.6" date="2026-07-12">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.6</url>
</release>
<release version="0.14.5" date="2026-06-11">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.5</url>
</release>
<release version="0.14.4" date="2026-05-23">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.4</url>
</release>
<release version="0.14.3" date="2026-04-22">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.3</url>
</release>
<release version="0.14.2" date="2026-04-04">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.2</url>
</release>
<release version="0.14.1" date="2026-03-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.1</url>
</release>
<release version="0.14.0" date="2026-03-01">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.14.0</url>
</release>
<release version="0.13.24" date="2026-02-05">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.24</url>
</release>
<release version="0.13.23" date="2026-01-24">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.23</url>
</release>
<release version="0.13.22" date="2025-11-14">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.22</url>
</release>
<release version="0.13.21" date="2025-10-25">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.21</url>
</release>
<release version="0.13.20" date="2025-09-23">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.20</url>
</release>
<release version="0.13.19" date="2025-09-15">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.19</url>
</release>
<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>
<release version="0.13.8" date="2024-10-26">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.8</url>
</release>
<release version="0.13.7" date="2024-09-28">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.7</url>
</release>
<release version="0.13.6" date="2024-09-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.6</url>
</release>
<release version="0.13.5" date="2024-07-25">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.5</url>
</release>
<release version="0.13.4" date="2024-06-06">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.4</url>
</release>
<release version="0.13.3" date="2024-04-19">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.3</url>
</release>
<release version="0.13.2" date="2024-03-23">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.2</url>
</release>
<release version="0.13.1" date="2024-02-27">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.1</url>
</release>
<release version="0.13.0" date="2024-02-18">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.13.0</url>
</release>
<release version="0.12.8" date="2024-02-08">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.8</url>
</release>
<release version="0.12.7" date="2023-12-15">
<url type="details">https://github.com/AsamK/signal-cli/releases/tag/v0.12.7</url>
</release>
</releases>
</component>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" version="1.1" viewBox="0 0 33.867 33.867" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-32.279 -138.64)">
<g transform="matrix(.45526 0 0 .45526 33.984 140.17)">
<path d="m33.468 66.938c-18.454 0-33.468-15.014-33.468-33.469s15.014-33.469 33.468-33.469c18.455 0 33.469 15.014 33.469 33.469 0 5.621-1.421 11.161-4.116 16.076l4.608 17.2-16.849-4.516c-5.172 3.084-11.069 4.709-17.112 4.709z" fill="#fff"/>
<path d="m33.468 67.184c-18.454 0-33.468-15.014-33.468-33.469s15.014-33.469 33.468-33.469c18.455 0 33.469 15.014 33.469 33.469 0 5.621-1.421 11.161-4.116 16.076l4.608 17.2-16.849-4.516c-5.172 3.084-11.069 4.709-17.112 4.709zm0-62.938c-16.249 0-29.468 13.22-29.468 29.469s13.219 29.469 29.468 29.469c5.582 0 11.021-1.574 15.729-4.554l0.74-0.468 11.835 3.171-3.243-12.1 0.419-0.72c2.609-4.484 3.988-9.602 3.988-14.799 0-16.248-13.219-29.468-29.468-29.468z"/>
<path d="m25.515 45.296q-2.3937 0-4.2817-0.97772-1.8543-0.97772-2.9332-3.0343-1.0451-2.0566-1.0451-5.2595 0-3.3377 1.1126-5.428 1.1126-2.0903 3.0006-3.068 1.9217-0.97772 4.3492-0.97772 1.3823 0 2.6634 0.30343 1.2812 0.26972 2.0903 0.67429l-0.91029 2.4612q-0.80915-0.30343-1.888-0.57315t-2.0229-0.26972q-5.3269 0-5.3269 6.844 0 3.2703 1.2812 5.0235 1.3149 1.7194 3.8772 1.7194 1.4834 0 2.596-0.30343 1.1463-0.30343 2.0903-0.74172v2.6297q-0.91029 0.472-2.0229 0.708-1.0789 0.26972-2.6297 0.26972zm11.901-0.33714h-2.9669v-25.623h2.9669zm7.2486-24.848q0.67429 0 1.18 0.472 0.53943 0.43829 0.53943 1.416 0 0.94401-0.53943 1.416-0.50572 0.472-1.18 0.472-0.74172 0-1.2474-0.472-0.50572-0.472-0.50572-1.416 0-0.97772 0.50572-1.416 0.50572-0.472 1.2474-0.472zm1.4497 6.7766v18.071h-2.9669v-18.071z" aria-label="cli"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -32,8 +32,6 @@ RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true RestrictNamespaces=true
RestrictRealtime=true RestrictRealtime=true
RestrictSUIDSGID=true RestrictSUIDSGID=true
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
StandardInput=socket StandardInput=socket
StandardOutput=journal StandardOutput=journal
StandardError=journal StandardError=journal

View File

@ -8,11 +8,9 @@ After=network-online.target
[Service] [Service]
Type=dbus Type=dbus
Environment="SIGNAL_CLI_OPTS=-Xms2m" Environment="SIGNAL_CLI_OPTS=-Xms2m"
ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon --system ExecStart=%dir%/bin/signal-cli --config /var/lib/signal-cli daemon --dbus-system
User=signal-cli User=signal-cli
BusName=org.asamk.Signal BusName=org.asamk.Signal
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
[Install] [Install]
Alias=dbus-org.asamk.Signal.service Alias=dbus-org.asamk.Signal.service

View File

@ -8,11 +8,9 @@ After=network-online.target
[Service] [Service]
Type=dbus Type=dbus
Environment="SIGNAL_CLI_OPTS=-Xms2m" Environment="SIGNAL_CLI_OPTS=-Xms2m"
ExecStart=%dir%/bin/signal-cli -a %I --config /var/lib/signal-cli daemon --system ExecStart=%dir%/bin/signal-cli -a %I --config /var/lib/signal-cli daemon --dbus-system
User=signal-cli User=signal-cli
BusName=org.asamk.Signal BusName=org.asamk.Signal
# JVM always exits with 143 in reaction to SIGTERM signal
SuccessExitStatus=143
[Install] [Install]
Alias=dbus-org.asamk.Signal.service Alias=dbus-org.asamk.Signal.service

359
docs/CALL_TUNNEL.md Normal file
View File

@ -0,0 +1,359 @@
# Voice Call Support
## Overview
signal-cli supports voice calls by spawning a subprocess called
`signal-call-tunnel` for each call. The tunnel handles WebRTC negotiation and
audio transport. signal-cli communicates with the tunnel over its stdin/stdout
using newline-delimited JSON messages, relaying signaling between the tunnel
and the Signal protocol.
```
signal-cli signal-call-tunnel
| |
|-- spawn --------------------------->|
|-- config JSON on stdin ------------>|
| |
|-- commands on stdin --------------->|
|<-- events on stdout ----------------|
| | WebRTC
| signaling relay | audio I/O
| |
| (stderr: tunnel logging) -------->| (captured by signal-cli)
```
Each call gets its own tunnel process. When the call ends, signal-cli closes
stdin and destroys the process.
Audio device names (`inputDeviceName`, `outputDeviceName`) are opaque strings
returned by the tunnel in its `ready` message. signal-cli passes them through
to JSON-RPC clients, which use them to connect audio via platform APIs.
---
## Spawning the Tunnel
For each call, signal-cli:
1. Spawns `signal-call-tunnel`
2. Writes config JSON followed by a newline to stdin
3. Keeps stdin open for subsequent control messages
4. Reads control events from stdout
5. Captures stderr for logging
The `signal-call-tunnel` binary is located by searching (in order):
1. `SIGNAL_CALL_TUNNEL_BIN` environment variable
2. `<signal-cli install dir>/bin/signal-call-tunnel` (detected from jar location)
3. `signal-call-tunnel` on `PATH`
### Config JSON
The first line written to the tunnel's stdin:
```json
{
"call_id": 12345,
"is_outgoing": true,
"local_device_id": 1,
"input_device_name": "signal_input",
"output_device_name": "signal_output"
}
```
| Field | Type | Description |
|----------------------|-------------------------|-----------------------------------------------|
| `call_id` | unsigned 64-bit integer | Call identifier (use unsigned representation) |
| `is_outgoing` | boolean | Whether this is an outgoing call |
| `local_device_id` | integer | Signal device ID |
| `input_device_name` | string (optional) | Requested input audio device name |
| `output_device_name` | string (optional) | Requested output audio device name |
If `input_device_name` or `output_device_name` are omitted, the tunnel
chooses default names. On Linux, these are per-call unique names (e.g.,
`signal_input_<call_id>`). On macOS, these are the fixed names `signal_input`
and `signal_output`, which must match the pre-installed BlackHole drivers.
---
## Control Protocol
Newline-delimited JSON messages over stdin (signal-cli to tunnel) and stdout
(tunnel to signal-cli). The first line on stdin is the config JSON. Subsequent
lines are control messages.
### signal-cli -> Tunnel (stdin)
| Type | When | Fields |
|----------------------|----------------------------|---------------------------------------------------------------------------------------------------|
| `createOutgoingCall` | Outgoing call setup | `callId`, `peerId` |
| `proceed` | After offer/receivedOffer | `callId`, `hideIp`, `iceServers` |
| `receivedOffer` | Incoming call | `callId`, `peerId`, `opaque`, `age`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
| `receivedAnswer` | Outgoing call answered | `opaque`, `senderDeviceId`, `senderIdentityKey`, `receiverIdentityKey` |
| `receivedIce` | ICE candidates arrive | `candidates` (array of base64 opaque blobs) |
| `accept` | User accepts incoming call | *(none)* |
| `hangup` | End the call | *(none)* |
### Tunnel -> signal-cli (stdout)
| Type | When | Fields |
|---------------|---------------------------------------------|------------------------------------------------------|
| `ready` | Control socket bound, audio devices created | `inputDeviceName`, `outputDeviceName` |
| `sendOffer` | Tunnel generated an offer | `callId`, `opaque`, `callMediaType` |
| `sendAnswer` | Tunnel generated an answer | `callId`, `opaque` |
| `sendIce` | ICE candidates gathered | `callId`, `candidates` (array of `{"opaque":"..."}`) |
| `sendHangup` | Tunnel wants to hang up | `callId`, `hangupType` |
| `sendBusy` | Line is busy | `callId` |
| `stateChange` | Call state transition | `state`, `reason` (optional) |
| `error` | Something went wrong | `message` |
Opaque blobs and identity keys are base64-encoded. ICE servers use the format:
```json
{
"urls": [
"turn:example.com"
],
"username": "u",
"password": "p"
}
```
---
## Startup Sequence
```
signal-cli signal-call-tunnel
| |
|-- spawn process ------------------> |
|-- config JSON + newline on stdin ---->|
| | parse config
| | initialize audio
| |
|<-------- ready (on stdout) -----------|
| {"type":"ready", |
| "inputDeviceName":"...", |
| "outputDeviceName":"..."} |
| |
|-- control messages on stdin --------->|
|<-- control events on stdout ----------|
```
---
## Call Flows
### Outgoing call
```
signal-cli signal-call-tunnel Remote Phone
| | |
|-- spawn + config ------->| |
|<-- ready ----------------| |
|-- createOutgoingCall --->| |
|-- proceed (TURN) ------->| |
| | create offer |
|<-- sendOffer ------------| |
|-- offer via Signal -------------------------------->|
|<-- answer via Signal -------------------------------|
|-- receivedAnswer ------->| (+ identity keys) |
|<-- sendIce --------------| |
|-- ICE via Signal -------------------------------> |
|<-- ICE via Signal -------------------------------- |
|-- receivedIce ---------->| |
| | ICE connects |
|<-- stateChange:Connected | |
```
### Incoming call
```
signal-cli signal-call-tunnel Remote Phone
| | |
|<-- offer via Signal --------------------------------|
|-- spawn + config ------->| |
|<-- ready ----------------| |
|-- receivedOffer -------->| (+ identity keys) |
|-- proceed (TURN) ------->| |
| | process offer |
|<-- sendAnswer -----------| |
|-- answer via Signal -------------------------------->|
|<-- sendIce --------------| |
|-- ICE via Signal ------------------------------> |
|<-- ICE via Signal -------------------------------- |
|-- receivedIce ---------->| |
| | ICE connecting... |
| | |
| (user accepts call) | |
| Java defers accept | |
| | |
|<-- stateChange:Ringing --| (tunnel ready to accept)|
|-- accept --------------->| (deferred accept sent) |
| | accept |
|<-- stateChange:Connected | |
```
### JSON-RPC client perspective
An external application (bot, UI, test script) interacts via JSON-RPC only.
**Important:** Call event notifications are not sent by default. Clients must
call `subscribeCallEvents` before initiating or receiving calls. Without this,
incoming calls are silently ignored (no tunnel is spawned).
```
JSON-RPC Client signal-cli daemon
| |
|-- subscribeCallEvents() ------------>| (required: enables call support)
| |
|-- startCall(recipient) ------------->|
|<-- {callId, state, -|
| inputDeviceName, |
| outputDeviceName} |
| |
|<-- callEvent: RINGING_OUTGOING ------|
| ... remote answers ... |
|<-- callEvent: CONNECTED -------------|
| |
| connect to audio devices |
| (via platform audio APIs) |
| |
|-- hangupCall(callId) --------------->| (or: receive callEvent ENDED)
|<-- callEvent: ENDED -----------------|
| disconnect from audio devices |
```
For incoming calls:
```
JSON-RPC Client signal-cli daemon
| |
|-- subscribeCallEvents() ------------>| (if not already subscribed)
| |
|<-- callEvent: RINGING_INCOMING ------| (includes callId, device names)
| |
|-- acceptCall(callId) --------------->|
|<-- {callId, state, -|
| inputDeviceName, |
| outputDeviceName} |
| |
|<-- callEvent: CONNECTING ------------|
|<-- callEvent: CONNECTED -------------|
| |
| connect to audio devices |
| (via platform audio APIs) |
```
To stop receiving call events, call `unsubscribeCallEvents`.
---
## State Machine
Call states as seen by JSON-RPC clients:
```
startCall()
|
v
+----- RINGING_OUTGOING ----+ RINGING_INCOMING -----+
| | | | |
| (timeout | (answered) | (rejected) | acceptCall() | (timeout
| ~60s) | | | | ~60s)
v v v v v
ENDED CONNECTED ENDED CONNECTING ENDED
| |
| v
| CONNECTED
| |
| (hangup/error) | (hangup/error)
v v
ENDED ENDED
```
For outgoing calls, `CONNECTED` fires directly when the tunnel reports
`Connected` state -- there is no intermediate `CONNECTING` event.
For incoming calls, `CONNECTING` is set by Java when the user calls
`acceptCall()`, before the tunnel completes ICE negotiation.
Both directions have a 60-second ring timeout.
Reconnection (ICE restart):
```
CONNECTED --> RECONNECTING --> CONNECTED (ICE restart succeeded)
|
v
ENDED (ICE restart failed)
```
`RECONNECTING` maps from the tunnel's `Connecting` state, which is emitted
during ICE restarts (not during initial connection).
---
## CallManager.java
`lib/src/main/java/org/asamk/signal/manager/helper/CallManager.java`
Manages the call lifecycle from the Java side:
1. Spawns `signal-call-tunnel` and writes config JSON to stdin
2. Keeps stdin open as the control write channel; reads stdout for control events
3. Captures stderr for tunnel logging
4. Parses `inputDeviceName` and `outputDeviceName` from the tunnel's `ready`
message and includes them in `CallInfo`
5. Translates tunnel state changes into `CallInfo.State` values and fires
`callEvent` JSON-RPC notifications to connected clients
6. Defers the `accept` message for incoming calls until the tunnel reports
`Ringing` state (sending earlier causes the tunnel to drop it)
7. Schedules a 60-second ring timeout for both incoming and outgoing calls
8. On hangup: sends hangup message, closes stdin, and destroys the process
---
## Implementation Notes
### Peer ID consistency
The `peerId` field in `createOutgoingCall` and `receivedOffer` must be the actual
remote peer UUID (e.g., `senderAddress.toString()`). The tunnel rejects ICE
candidates if the peer ID doesn't match across calls, causing "Ignoring
peer-reflexive ICE candidate because the ufrag is unknown."
### sendHangup semantics
`sendHangup` from the tunnel is a request to send a hangup message via Signal
protocol. It is **not** a local state change -- local state transitions come
exclusively from `stateChange` events. For single-device clients, ignore
`AcceptedOnAnotherDevice`, `DeclinedOnAnotherDevice`, and
`BusyOnAnotherDevice` hangup types in the `hangupType` field -- sending these to
the remote peer causes it to terminate the call prematurely.
### Call ID serialization
Call IDs can exceed `Long.MAX_VALUE` in Java. Use `Long.toUnsignedString()` when
serializing to JSON for the tunnel (which expects unsigned 64-bit integers). In
the config JSON, `call_id` should also use unsigned representation.
### Incoming hangup filtering
When receiving hangup messages via Signal protocol, only honor `NORMAL` type
hangups. `ACCEPTED`, `DECLINED`, and `BUSY` types are multi-device coordination
messages and should be ignored by single-device clients.
### JSON-RPC call ID types
JSON-RPC clients may send call IDs as various numeric types (Long, BigInteger,
Integer). Use `Number.longValue()` rather than direct casting when extracting
call IDs from JSON-RPC parameters.
### Identity key format
Identity keys in `senderIdentityKey` and `receiverIdentityKey` must be **raw
32-byte Curve25519 public keys** (without the 0x05 DJB type prefix). If the
33-byte serialized form is used instead, SRTP key derivation produces different
keys on each side, causing authentication failures.

View File

@ -1,226 +0,0 @@
[
{
"name":"[Z"
},
{
"name":"com.sun.security.auth.module.UnixSystem",
"fields":[{"name":"gid"}, {"name":"groups"}, {"name":"uid"}, {"name":"username"}]
},
{
"name":"java.lang.Boolean",
"methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.Class",
"methods":[{"name":"getCanonicalName","parameterTypes":[] }]
},
{
"name":"java.lang.ClassLoader",
"methods":[{"name":"getPlatformClassLoader","parameterTypes":[] }, {"name":"loadClass","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.IllegalArgumentException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.IllegalStateException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.lang.NoSuchMethodError"
},
{
"name":"java.lang.String"
},
{
"name":"java.lang.Throwable",
"methods":[{"name":"getMessage","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }]
},
{
"name":"java.lang.UnsatisfiedLinkError",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"java.util.UUID",
"methods":[{"name":"<init>","parameterTypes":["long","long"] }, {"name":"getLeastSignificantBits","parameterTypes":[] }, {"name":"getMostSignificantBits","parameterTypes":[] }]
},
{
"name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"
},
{
"name":"org.asamk.signal.manager.storage.protocol.SignalProtocolStore",
"methods":[{"name":"getIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress"] }, {"name":"getIdentityKeyPair","parameterTypes":[] }, {"name":"getLocalRegistrationId","parameterTypes":[] }, {"name":"isTrustedIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.IdentityKey","org.signal.libsignal.protocol.state.IdentityKeyStore$Direction"] }, {"name":"loadKyberPreKey","parameterTypes":["int"] }, {"name":"loadPreKey","parameterTypes":["int"] }, {"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] }, {"name":"loadSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress"] }, {"name":"loadSignedPreKey","parameterTypes":["int"] }, {"name":"markKyberPreKeyUsed","parameterTypes":["int"] }, {"name":"removePreKey","parameterTypes":["int"] }, {"name":"saveIdentity","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.IdentityKey"] }, {"name":"storeSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID","org.signal.libsignal.protocol.groups.state.SenderKeyRecord"] }, {"name":"storeSession","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","org.signal.libsignal.protocol.state.SessionRecord"] }]
},
{
"name":"org.asamk.signal.manager.storage.senderKeys.SenderKeyStore",
"methods":[{"name":"loadSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID"] }, {"name":"storeSenderKey","parameterTypes":["org.signal.libsignal.protocol.SignalProtocolAddress","java.util.UUID","org.signal.libsignal.protocol.groups.state.SenderKeyRecord"] }]
},
{
"name":"org.graalvm.jniutils.JNIExceptionWrapperEntryPoints",
"methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]
},
{
"name":"org.signal.libsignal.protocol.DuplicateMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.IdentityKey",
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["byte[]"] }, {"name":"serialize","parameterTypes":[] }]
},
{
"name":"org.signal.libsignal.protocol.IdentityKeyPair",
"methods":[{"name":"serialize","parameterTypes":[] }]
},
{
"name":"org.signal.libsignal.protocol.InvalidKeyException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.InvalidKeyIdException"
},
{
"name":"org.signal.libsignal.protocol.InvalidMessageException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.NoSessionException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.SignalProtocolAddress",
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["java.lang.String","int"] }]
},
{
"name":"org.signal.libsignal.protocol.UntrustedIdentityException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.fingerprint.FingerprintParsingException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyRecord",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.protocol.groups.state.SenderKeyStore"
},
{
"name":"org.signal.libsignal.protocol.logging.Log",
"methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]
},
{
"name":"org.signal.libsignal.protocol.message.PlaintextContent",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.message.PreKeySignalMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.protocol.message.SenderKeyMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.protocol.message.SignalMessage",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }]
},
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore"
},
{
"name":"org.signal.libsignal.protocol.state.IdentityKeyStore$Direction",
"fields":[{"name":"RECEIVING"}, {"name":"SENDING"}]
},
{
"name":"org.signal.libsignal.protocol.state.KyberPreKeyRecord",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.state.KyberPreKeyStore"
},
{
"name":"org.signal.libsignal.protocol.state.PreKeyRecord",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.state.PreKeyStore"
},
{
"name":"org.signal.libsignal.protocol.state.SessionRecord",
"fields":[{"name":"unsafeHandle"}],
"methods":[{"name":"<init>","parameterTypes":["long"] }, {"name":"<init>","parameterTypes":["byte[]"] }]
},
{
"name":"org.signal.libsignal.protocol.state.SessionStore"
},
{
"name":"org.signal.libsignal.protocol.state.SignedPreKeyRecord",
"fields":[{"name":"unsafeHandle"}]
},
{
"name":"org.signal.libsignal.protocol.state.SignedPreKeyStore"
},
{
"name":"org.signal.libsignal.usernames.BadNicknameCharacterException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.CannotBeEmptyException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.NicknameTooLongException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.usernames.NicknameTooShortException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.signal.libsignal.zkgroup.InvalidInputException",
"methods":[{"name":"<init>","parameterTypes":["java.lang.String"] }]
},
{
"name":"org.sqlite.BusyHandler",
"methods":[{"name":"callback","parameterTypes":["int"] }]
},
{
"name":"org.sqlite.Collation",
"methods":[{"name":"xCompare","parameterTypes":["java.lang.String","java.lang.String"] }]
},
{
"name":"org.sqlite.Function",
"fields":[{"name":"args"}, {"name":"context"}, {"name":"value"}],
"methods":[{"name":"xFunc","parameterTypes":[] }]
},
{
"name":"org.sqlite.Function$Aggregate",
"methods":[{"name":"clone","parameterTypes":[] }, {"name":"xFinal","parameterTypes":[] }, {"name":"xStep","parameterTypes":[] }]
},
{
"name":"org.sqlite.Function$Window",
"methods":[{"name":"xInverse","parameterTypes":[] }, {"name":"xValue","parameterTypes":[] }]
},
{
"name":"org.sqlite.ProgressHandler",
"methods":[{"name":"progress","parameterTypes":[] }]
},
{
"name":"org.sqlite.core.DB",
"methods":[{"name":"onCommit","parameterTypes":["boolean"] }, {"name":"onUpdate","parameterTypes":["int","java.lang.String","java.lang.String","long"] }, {"name":"throwex","parameterTypes":[] }, {"name":"throwex","parameterTypes":["int"] }]
},
{
"name":"org.sqlite.core.DB$ProgressObserver",
"methods":[{"name":"progress","parameterTypes":["int","int"] }]
},
{
"name":"org.sqlite.core.NativeDB",
"fields":[{"name":"busyHandler"}, {"name":"commitListener"}, {"name":"pointer"}, {"name":"progressHandler"}, {"name":"updateListener"}],
"methods":[{"name":"stringToUtf8ByteArray","parameterTypes":["java.lang.String"] }, {"name":"throwex","parameterTypes":["java.lang.String"] }]
}
]

View File

@ -1,8 +0,0 @@
[
{
"type":"agent-extracted",
"classes":[
]
}
]

View File

@ -1,23 +0,0 @@
[
{
"interfaces":["java.sql.Connection"]
},
{
"interfaces":["org.asamk.Signal"]
},
{
"interfaces":["org.asamk.Signal$Configuration"]
},
{
"interfaces":["org.asamk.Signal$Device"]
},
{
"interfaces":["org.asamk.Signal$Group"]
},
{
"interfaces":["org.asamk.SignalControl"]
},
{
"interfaces":["org.freedesktop.dbus.interfaces.DBus"]
}
]

File diff suppressed because it is too large Load Diff

View File

@ -1,194 +0,0 @@
{
"resources":{
"includes":[{
"pattern":"\\QMETA-INF/maven/org.xerial/sqlite-jdbc/pom.properties\\E"
}, {
"pattern":"\\QMETA-INF/services/ch.qos.logback.classic.spi.Configurator\\E"
}, {
"pattern":"\\QMETA-INF/services/java.sql.Driver\\E"
}, {
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoader\\E"
}, {
"pattern":"\\QMETA-INF/services/kotlin.reflect.jvm.internal.impl.resolve.ExternalOverridabilityCondition\\E"
}, {
"pattern":"\\QMETA-INF/services/org.freedesktop.dbus.spi.transport.ITransportProvider\\E"
}, {
"pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AG\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AI\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AS\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AT\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AU\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AZ\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BB\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BM\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BO\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BS\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CA\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CH\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CI\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CL\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CN\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CO\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DK\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EC\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ES\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FI\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GB\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HK\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_HU\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ID\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IN\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IT\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_JP\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_LV\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MM\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MO\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MX\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_MY\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NG\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NL\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PA\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PH\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PL\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RO\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RU\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SA\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SI\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_SK\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TH\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TR\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UA\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UG\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_VE\\E"
}, {
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_XK\\E"
}, {
"pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\E"
}, {
"pattern":"\\Qkotlin/annotation/annotation.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/collections/collections.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/coroutines/coroutines.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/internal/internal.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/kotlin.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/ranges/ranges.kotlin_builtins\\E"
}, {
"pattern":"\\Qkotlin/reflect/reflect.kotlin_builtins\\E"
}, {
"pattern":"\\Qlibsignal_jni.dylib\\E"
}, {
"pattern":"\\Qlibsignal_jni.so\\E"
}, {
"pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"
}, {
"pattern":"\\Qorg/asamk/signal/manager/config/whisper.store\\E"
}, {
"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"
}, {
"pattern":"\\Qorg/sqlite/native/Linux/x86_64/libsqlitejdbc.so\\E"
}, {
"pattern":"\\Qsignal_jni.dll\\E"
}, {
"pattern":"\\Qsqlite-jdbc.properties\\E"
}, {
"pattern":"com/google/i18n/phonenumbers/data/.*"
}, {
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/nfc.nrm\\E"
}, {
"pattern":"java.base:\\Qjdk/internal/icu/impl/data/icudt67b/uprops.icu\\E"
}, {
"pattern":"java.base:\\Qsun/net/idn/uidna.spp\\E"
}, {
"pattern":"java.base:\\Qsun/net/www/content-types.properties\\E"
}, {
"pattern":"java.base:\\Qsun/text/resources/LineBreakIteratorData\\E"
}]},
"bundles":[{
"name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl",
"locales":["", "en", "und"]
}]
}

View File

@ -1,8 +0,0 @@
{
"types":[
],
"lambdaCapturingTypes":[
],
"proxies":[
]
}

29
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,29 @@
[versions]
slf4j = "2.0.18"
coroutines = "1.10.2"
junit = "6.1.2"
micronaut-json-schema = "2.1.0"
micronaut-core = "5.1.10"
signal-service = "2.15.3_unofficial_152"
[libraries]
bouncycastle = "org.bouncycastle:bcprov-jdk18on:1.85"
jackson-databind = "com.fasterxml.jackson.core:jackson-databind:2.22.1"
argparse4j = "net.sourceforge.argparse4j:argparse4j:0.9.0"
dbusjava = "com.github.hypfvieh:dbus-java-transport-native-unixsocket:5.0.0"
zxing = "com.google.zxing:core:3.5.4"
micronaut-json-schema-annotations = { module = "io.micronaut.jsonschema:micronaut-json-schema-annotations", version.ref = "micronaut-json-schema" }
micronaut-json-schema-processor = { module = "io.micronaut.jsonschema:micronaut-json-schema-processor", version.ref = "micronaut-json-schema" }
micronaut-json-schema-generator = { module = "io.micronaut.jsonschema:micronaut-json-schema-generator", version.ref = "micronaut-json-schema" }
micronaut-inject-java = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-core" }
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.6.1"
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
signalnetwork = { module = "com.github.turasa:signal-network", version.ref = "signal-service" }
sqlite = "org.xerial:sqlite-jdbc:3.53.2.1"
hikari = "com.zaxxer:HikariCP:7.1.0"
junit-jupiter-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }

Binary file not shown.

View File

@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

31
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -15,10 +15,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
# Gradle start up script for POSIX generated by Gradle. # gradlew start up script for POSIX generated by Gradle.
# #
# Important for running: # Important for running:
# #
@ -27,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole # bash, then to run this script, type that shell name before the whole
# command line, like: # command line, like:
# #
# ksh Gradle # ksh gradlew
# #
# Busybox and similar reduced shells will NOT work, because this script # Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features: # requires all of these POSIX shell features:
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/<unknown>/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -112,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@ -145,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -153,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -170,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@ -202,16 +202,15 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

58
gradlew.bat vendored
View File

@ -13,16 +13,18 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem gradlew startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@ -43,13 +45,13 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute gradlew
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* @rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end :exitWithErrorLevel
@rem End local scope for the variables with windows NT shell @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
if %ERRORLEVEL% equ 0 goto mainEnd "%COMSPEC%" /c exit %ERRORLEVEL%
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -4,17 +4,23 @@ plugins {
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_25
if (!JavaVersion.current().isCompatibleWith(targetCompatibility)) {
toolchain {
languageVersion.set(JavaLanguageVersion.of(targetCompatibility.majorVersion))
}
}
} }
val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString() val libsignalClientPath = project.findProperty("libsignal_client_path")?.toString()
dependencies { dependencies {
if (libsignalClientPath == null) { if (libsignalClientPath == null) {
implementation(libs.signalservice) implementation(libs.signalnetwork)
} else { } else {
implementation(libs.signalservice) { implementation(libs.signalnetwork) {
exclude(group = "org.signal", module = "libsignal-client") exclude(group = "org.signal", module = "libsignal-client")
} }
implementation(files(libsignalClientPath)) implementation(files(libsignalClientPath))
@ -24,8 +30,11 @@ dependencies {
implementation(libs.slf4j.api) implementation(libs.slf4j.api)
implementation(libs.sqlite) implementation(libs.sqlite)
implementation(libs.hikari) implementation(libs.hikari)
compileOnly(libs.kotlinx.coroutines.core)
testImplementation(libs.junit) testImplementation(libs.junit.jupiter)
testImplementation(platform(libs.junit.jupiter.bom))
testRuntimeOnly(libs.junit.launcher)
} }
tasks.named<Test>("test") { tasks.named<Test>("test") {

View File

@ -1,9 +1,16 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.asamk.signal.manager.api.AlreadyReceivingException; import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException; import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.CallInfo;
import org.asamk.signal.manager.api.CallOffer;
import org.asamk.signal.manager.api.CaptchaRejectedException;
import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.Configuration; import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.DeviceLimitExceededException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.Group; import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupId;
@ -13,32 +20,43 @@ import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.IdentityVerificationCode; import org.asamk.signal.manager.api.IdentityVerificationCode;
import org.asamk.signal.manager.api.InactiveGroupLinkException; 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.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.InvalidStickerException; import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.InvalidUsernameException; import org.asamk.signal.manager.api.InvalidUsernameException;
import org.asamk.signal.manager.api.LastGroupAdminException; import org.asamk.signal.manager.api.LastGroupAdminException;
import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.NotAGroupMemberException; import org.asamk.signal.manager.api.NotAGroupMemberException;
import org.asamk.signal.manager.api.NotPrimaryDeviceException; import org.asamk.signal.manager.api.NotPrimaryDeviceException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.PendingAdminApprovalException; 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; import org.asamk.signal.manager.api.ReceiveConfig;
import org.asamk.signal.manager.api.Recipient; import org.asamk.signal.manager.api.Recipient;
import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResult;
import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.StickerPack; import org.asamk.signal.manager.api.StickerPack;
import org.asamk.signal.manager.api.StickerPackId;
import org.asamk.signal.manager.api.StickerPackInvalidException; import org.asamk.signal.manager.api.StickerPackInvalidException;
import org.asamk.signal.manager.api.StickerPackUrl; import org.asamk.signal.manager.api.StickerPackUrl;
import org.asamk.signal.manager.api.TurnServer;
import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.api.UpdateProfile; import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus; 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.signal.core.util.UuidUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
@ -54,7 +72,11 @@ import java.util.Set;
public interface Manager extends Closeable { public interface Manager extends Closeable {
static boolean isValidNumber(final String e164Number, final String countryCode) { static boolean isValidNumber(final String e164Number, final String countryCode) {
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode); return PhoneNumberUtil.getInstance().isPossibleNumber(e164Number, countryCode);
}
static boolean isValidAci(final String aci) {
return UuidUtil.INSTANCE.isUuid(aci);
} }
static boolean isSignalClientAvailable() { static boolean isSignalClientAvailable() {
@ -74,6 +96,8 @@ public interface Manager extends Closeable {
String getSelfNumber(); String getSelfNumber();
String getSelfACI();
/** /**
* This is used for checking a set of phone numbers for registration on Signal * This is used for checking a set of phone numbers for registration on Signal
* *
@ -81,13 +105,20 @@ public interface Manager extends Closeable {
* @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null.
* @throws IOException if it's unable to get the contacts to check if they're registered * @throws IOException if it's unable to get the contacts to check if they're registered
*/ */
Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException; Map<String, UserStatus> getUserStatus(Set<String> numbers) throws IOException, RateLimitException;
void updateAccountAttributes(String deviceName) throws IOException; Map<String, UsernameStatus> getUsernameStatus(Set<String> usernames) throws IOException;
void updateAccountAttributes(
String deviceName,
Boolean unrestrictedUnidentifiedSender,
final Boolean discoverableByNumber,
final Boolean numberSharing
) throws IOException;
Configuration getConfiguration(); Configuration getConfiguration();
void updateConfiguration(Configuration configuration) throws IOException, NotPrimaryDeviceException; void updateConfiguration(Configuration configuration) throws NotPrimaryDeviceException;
/** /**
* Update the user's profile. * Update the user's profile.
@ -95,11 +126,15 @@ public interface Manager extends Closeable {
*/ */
void updateProfile(UpdateProfile updateProfile) throws IOException; void updateProfile(UpdateProfile updateProfile) throws IOException;
String getUsername();
UsernameLinkUrl getUsernameLink();
/** /**
* Set a username for the account. * Set a username for the account.
* If the username is null, it will be deleted. * If the username is null, it will be deleted.
*/ */
String setUsername(String username) throws IOException, InvalidUsernameException; void setUsername(String username) throws IOException, InvalidUsernameException;
/** /**
* Set a username for the account. * Set a username for the account.
@ -107,34 +142,61 @@ public interface Manager extends Closeable {
*/ */
void deleteUsername() throws IOException; void deleteUsername() throws IOException;
void startChangeNumber(
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, PinLockMissingException;
void unregister() throws IOException; void unregister() throws IOException;
void deleteAccount() throws IOException; void deleteAccount() throws IOException;
void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException; void submitRateLimitRecaptchaChallenge(
String challenge,
String captcha
) throws IOException, CaptchaRejectedException;
List<Device> getLinkedDevices() throws IOException; List<Device> getLinkedDevices() throws IOException;
void removeLinkedDevices(int deviceId) throws IOException; void updateLinkedDevice(int deviceId, String name) throws IOException, NotPrimaryDeviceException;
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException; void removeLinkedDevices(int deviceId) throws IOException, NotPrimaryDeviceException;
void addDeviceLink(DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException, NotPrimaryDeviceException, DeviceLimitExceededException;
void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException; void setRegistrationLockPin(Optional<String> pin) throws IOException, NotPrimaryDeviceException;
List<Group> getGroups(); List<Group> getGroups();
List<Group> getGroups(Collection<GroupId> groupIds);
SendGroupMessageResults quitGroup( SendGroupMessageResults quitGroup(
GroupId groupId, Set<RecipientIdentifier.Single> groupAdmins GroupId groupId,
Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException; ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException, UnregisteredRecipientException;
void deleteGroup(GroupId groupId) throws IOException; void deleteGroup(GroupId groupId) throws IOException;
SendGroupMessageResults terminateGroup(
GroupId groupId
) throws IOException, GroupNotFoundException, NotAGroupMemberException;
Pair<GroupId, SendGroupMessageResults> createGroup( Pair<GroupId, SendGroupMessageResults> createGroup(
String name, Set<RecipientIdentifier.Single> members, String avatarFile String name,
Set<RecipientIdentifier.Single> members,
String avatarFile
) throws IOException, AttachmentInvalidException, UnregisteredRecipientException; ) throws IOException, AttachmentInvalidException, UnregisteredRecipientException;
SendGroupMessageResults updateGroup( SendGroupMessageResults updateGroup(
final GroupId groupId, final UpdateGroup updateGroup final GroupId groupId,
final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException; ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException, UnregisteredRecipientException;
Pair<GroupId, SendGroupMessageResults> joinGroup( Pair<GroupId, SendGroupMessageResults> joinGroup(
@ -142,27 +204,42 @@ public interface Manager extends Closeable {
) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException; ) throws IOException, InactiveGroupLinkException, PendingAdminApprovalException;
SendMessageResults sendTypingMessage( SendMessageResults sendTypingMessage(
TypingAction action, Set<RecipientIdentifier> recipients TypingAction action,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendReadReceipt( SendMessageResults sendReadReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
RecipientIdentifier.Single sender, List<Long> messageIds
) throws IOException;
SendMessageResults sendViewedReceipt( SendMessageResults sendViewedReceipt(RecipientIdentifier.Single sender, List<Long> messageIds);
RecipientIdentifier.Single sender, List<Long> messageIds
) throws IOException;
SendMessageResults sendMessage( SendMessageResults sendMessage(
Message message, Set<RecipientIdentifier> recipients Message message,
Set<RecipientIdentifier> recipients,
boolean notifySelf
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
SendMessageResults sendEditMessage( SendMessageResults sendEditMessage(
Message message, Set<RecipientIdentifier> recipients, long editTargetTimestamp Message message,
Set<RecipientIdentifier> recipients,
long editTargetTimestamp
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException; ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException;
/**
* Post a file attachment story to "My Story" or to a group.
*
* @param attachment path to the file to upload and post as a story
* @param allowsReplies whether other users are allowed to reply to this story
* @param groupId if present, post the story to this group instead of "My Story"
*/
SendMessageResults sendStory(
String attachment,
boolean allowsReplies,
Optional<GroupId> groupId
) throws IOException, AttachmentInvalidException, GroupNotFoundException, NotAGroupMemberException;
SendMessageResults sendRemoteDeleteMessage( SendMessageResults sendRemoteDeleteMessage(
long targetSentTimestamp, Set<RecipientIdentifier> recipients long targetSentTimestamp,
Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException;
SendMessageResults sendMessageReaction( SendMessageResults sendMessageReaction(
@ -171,36 +248,102 @@ public interface Manager extends Closeable {
RecipientIdentifier.Single targetAuthor, RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp, long targetSentTimestamp,
Set<RecipientIdentifier> recipients, Set<RecipientIdentifier> recipients,
final boolean notifySelf,
final boolean isStory final boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException; ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendAdminDelete(
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set<RecipientIdentifier.Group> recipients,
boolean notifySelf,
boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPinMessage(
int pinDuration,
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set<RecipientIdentifier> recipients,
boolean notifySelf,
boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendUnpinMessage(
RecipientIdentifier.Single targetAuthor,
long targetSentTimestamp,
Set<RecipientIdentifier> recipients,
boolean notifySelf,
boolean isStory
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPaymentNotificationMessage( SendMessageResults sendPaymentNotificationMessage(
byte[] receipt, String note, RecipientIdentifier.Single recipient byte[] receipt,
String note,
RecipientIdentifier.Single recipient
) throws IOException; ) throws IOException;
SendMessageResults sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException; void sendEndSessionMessage(Set<RecipientIdentifier.Single> recipients) throws IOException;
SendMessageResults sendMessageRequestResponse(
MessageEnvelope.Sync.MessageRequestResponse.Type type,
Set<RecipientIdentifier> recipientIdentifiers
);
SendMessageResults sendPollCreateMessage(
final String question,
final boolean allowMultiple,
final List<String> options,
final Set<RecipientIdentifier> recipients,
final boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPollVoteMessage(
final RecipientIdentifier.Single targetAuthor,
final long targetSentTimestamp,
final List<Integer> optionIndexes,
final int voteCount,
final Set<RecipientIdentifier> recipients,
final boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
SendMessageResults sendPollTerminateMessage(
final long targetSentTimestamp,
final Set<RecipientIdentifier> recipients,
final boolean notifySelf
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException;
void hideRecipient(RecipientIdentifier.Single recipient);
void deleteRecipient(RecipientIdentifier.Single recipient); void deleteRecipient(RecipientIdentifier.Single recipient);
void deleteContact(RecipientIdentifier.Single recipient); void deleteContact(RecipientIdentifier.Single recipient);
void setContactName( void setContactName(
RecipientIdentifier.Single recipient, String givenName, final String familyName final RecipientIdentifier.Single recipient,
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException; final String givenName,
final String familyName,
final String nickGivenName,
final String nickFamilyName,
final String note
) throws UnregisteredRecipientException;
void setContactsBlocked( void setContactsBlocked(
Collection<RecipientIdentifier.Single> recipient, boolean blocked Collection<RecipientIdentifier.Single> recipient,
boolean blocked
) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException; ) throws NotPrimaryDeviceException, IOException, UnregisteredRecipientException;
void setGroupsBlocked( void setGroupsBlocked(
Collection<GroupId> groupId, boolean blocked Collection<GroupId> groupId,
boolean blocked
) throws GroupNotFoundException, IOException, NotPrimaryDeviceException; ) throws GroupNotFoundException, IOException, NotPrimaryDeviceException;
/** /**
* Change the expiration timer for a contact * Change the expiration timer for a contact
*/ */
void setExpirationTimer( void setExpirationTimer(
RecipientIdentifier.Single recipient, int messageExpirationTimer RecipientIdentifier.Single recipient,
int messageExpirationTimer
) throws IOException, UnregisteredRecipientException; ) throws IOException, UnregisteredRecipientException;
/** /**
@ -239,9 +382,13 @@ public interface Manager extends Closeable {
* Receive new messages from server, returns if no new message arrive in a timespan of timeout. * Receive new messages from server, returns if no new message arrive in a timespan of timeout.
*/ */
void receiveMessages( void receiveMessages(
Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler Optional<Duration> timeout,
Optional<Integer> maxMessages,
ReceiveMessageHandler handler
) throws IOException, AlreadyReceivingException; ) throws IOException, AlreadyReceivingException;
void stopReceiveMessages();
void setReceiveConfig(ReceiveConfig receiveConfig); void setReceiveConfig(ReceiveConfig receiveConfig);
boolean isContactBlocked(RecipientIdentifier.Single recipient); boolean isContactBlocked(RecipientIdentifier.Single recipient);
@ -269,7 +416,8 @@ public interface Manager extends Closeable {
* @param recipient account of the identity * @param recipient account of the identity
*/ */
boolean trustIdentityVerified( boolean trustIdentityVerified(
RecipientIdentifier.Single recipient, IdentityVerificationCode verificationCode RecipientIdentifier.Single recipient,
IdentityVerificationCode verificationCode
) throws UnregisteredRecipientException; ) throws UnregisteredRecipientException;
/** /**
@ -285,8 +433,59 @@ public interface Manager extends Closeable {
InputStream retrieveAttachment(final String id) throws IOException; InputStream retrieveAttachment(final String id) throws IOException;
InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException;
InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException;
// --- Voice call methods ---
CallInfo startCall(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException;
CallInfo acceptCall(long callId) throws IOException;
void hangupCall(long callId) throws IOException;
SendMessageResult rejectCall(long callId) throws IOException;
List<CallInfo> listActiveCalls();
void sendCallOffer(
RecipientIdentifier.Single recipient,
CallOffer offer
) throws IOException, UnregisteredRecipientException;
void sendCallAnswer(
RecipientIdentifier.Single recipient,
long callId,
byte[] answerOpaque
) throws IOException, UnregisteredRecipientException;
void sendIceUpdate(
RecipientIdentifier.Single recipient,
long callId,
List<byte[]> iceCandidates
) throws IOException, UnregisteredRecipientException;
void sendHangup(
RecipientIdentifier.Single recipient,
long callId,
MessageEnvelope.Call.Hangup.Type type
) throws IOException, UnregisteredRecipientException;
void sendBusy(RecipientIdentifier.Single recipient, long callId) throws IOException, UnregisteredRecipientException;
List<TurnServer> getTurnServerInfo() throws IOException;
@Override @Override
void close() throws IOException; void close();
void addCallEventListener(CallEventListener listener);
void removeCallEventListener(CallEventListener listener);
interface ReceiveMessageHandler { interface ReceiveMessageHandler {
@ -295,4 +494,9 @@ public interface Manager extends Closeable {
void handleMessage(MessageEnvelope envelope, Throwable e); void handleMessage(MessageEnvelope envelope, Throwable e);
} }
interface CallEventListener {
void handleCallEvent(CallInfo callInfo, String reason);
}
} }

View File

@ -1,10 +1,12 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.asamk.signal.manager.internal.LibSignalLogger; import org.asamk.signal.manager.internal.LibSignalLogger;
import org.asamk.signal.manager.internal.SignalLogger;
public class ManagerLogger { public class ManagerLogger {
public static void initLogger() { public static void initLogger() {
LibSignalLogger.initLogger(); LibSignalLogger.initLogger();
SignalLogger.initLogger();
} }
} }

View File

@ -3,8 +3,10 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException; 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.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
@ -12,12 +14,15 @@ import java.io.IOException;
public interface RegistrationManager extends Closeable { public interface RegistrationManager extends Closeable {
void register( void register(
boolean voiceVerification, String captcha boolean voiceVerification,
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException; String captcha,
final boolean forceRegister
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException;
void verifyAccount( void verifyAccount(
String verificationCode, String pin String verificationCode,
) throws IOException, PinLockedException, IncorrectPinException; String pin
) throws IOException, PinLockedException, IncorrectPinException, PinLockMissingException;
void deleteLocalAccountData() throws IOException; void deleteLocalAccountData() throws IOException;

View File

@ -4,5 +4,5 @@ import org.asamk.signal.manager.api.TrustNewIdentity;
public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) { public record Settings(TrustNewIdentity trustNewIdentity, boolean disableMessageSendLog) {
public static Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false); public static final Settings DEFAULT = new Settings(TrustNewIdentity.ON_FIRST_USE, false);
} }

View File

@ -2,6 +2,7 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.AccountCheckException; import org.asamk.signal.manager.api.AccountCheckException;
import org.asamk.signal.manager.api.NotRegisteredException; 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.api.ServiceEnvironment;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
@ -14,7 +15,7 @@ import org.asamk.signal.manager.internal.RegistrationManagerImpl;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.accounts.AccountsStore; import org.asamk.signal.manager.storage.accounts.AccountsStore;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.signal.libsignal.protocol.util.KeyHelper; import org.signal.core.models.ServiceId.ACI;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
@ -65,29 +66,56 @@ public class SignalAccountFiles {
} }
public MultiAccountManager initMultiAccountManager() throws IOException { public MultiAccountManager initMultiAccountManager() throws IOException {
final var managers = accountsStore.getAllAccounts().parallelStream().map(a -> { final var managerPairs = accountsStore.getAllAccounts().parallelStream().map(a -> {
try { try {
return initManager(a.number(), a.path()); return new Pair<Manager, Throwable>(initManagerByNumber(a.number(), a.path()), null);
} catch (NotRegisteredException | IOException | AccountCheckException e) { } catch (NotRegisteredException e) {
logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); logger.warn("Ignoring {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName());
return null; return null;
} catch (Throwable e) { } catch (AccountCheckException | IOException e) {
logger.error("Failed to load {}: {} ({})", a.number(), e.getMessage(), e.getClass().getSimpleName()); 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(); }).filter(Objects::nonNull).toList();
for (final var pair : managerPairs) {
if (pair.second() instanceof IOException e) {
throw e;
}
}
final var managers = managerPairs.stream()
.filter(p -> p != null && p.first() != null)
.map(Pair::first)
.toList();
return new MultiAccountManagerImpl(managers, this); return new MultiAccountManagerImpl(managers, this);
} }
public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException { public Manager initManagerByNumber(String number) throws IOException, NotRegisteredException, AccountCheckException {
final var accountPath = accountsStore.getPathByNumber(number); final var accountPath = accountsStore.getPathByNumber(number);
return this.initManager(number, accountPath); return this.initManagerByNumber(number, accountPath);
} }
private Manager initManager( public Manager initManagerByAci(String aciStr) throws IOException, NotRegisteredException, AccountCheckException {
String number, String accountPath final var aci = ACI.parseOrThrow(aciStr);
final var accountPath = accountsStore.getPathByAci(aci);
return this.initManagerByAci(aci, accountPath);
}
private Manager initManagerByNumber(
String number,
String accountPath
) throws IOException, NotRegisteredException, AccountCheckException { ) throws IOException, NotRegisteredException, AccountCheckException {
final var account = loadAccount(accountPath);
if (!number.equals(account.getNumber())) {
account.close();
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
}
return initManagerFromAccount(number, accountPath, account);
}
private SignalAccount loadAccount(final String accountPath) throws NotRegisteredException, IOException {
if (accountPath == null) { if (accountPath == null) {
throw new NotRegisteredException(); throw new NotRegisteredException();
} }
@ -95,12 +123,27 @@ public class SignalAccountFiles {
throw new NotRegisteredException(); throw new NotRegisteredException();
} }
var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings); return SignalAccount.load(pathConfig.dataPath(), accountPath, true, settings);
if (!number.equals(account.getNumber())) {
account.close();
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
} }
private Manager initManagerByAci(
ACI aci,
String accountPath
) throws IOException, NotRegisteredException, AccountCheckException {
final var account = loadAccount(accountPath);
if (!aci.equals(account.getAci())) {
account.close();
throw new IOException("ACI in account file doesn't match expected ACI: " + account.getAci());
}
return initManagerFromAccount(aci.toString(), accountPath, account);
}
private ManagerImpl initManagerFromAccount(
final String identifier,
final String accountPath,
final SignalAccount account
) throws NotRegisteredException, IOException, AccountCheckException {
if (!account.isRegistered()) { if (!account.isRegistered()) {
account.close(); account.close();
throw new NotRegisteredException(); throw new NotRegisteredException();
@ -122,10 +165,10 @@ public class SignalAccountFiles {
manager.checkAccountState(); manager.checkAccountState();
} catch (DeprecatedVersionException e) { } catch (DeprecatedVersionException e) {
manager.close(); manager.close();
throw new AccountCheckException("signal-cli version is too old for the Signal-Server, please update."); throw new IOException("signal-cli version is too old for the Signal-Server, please update.");
} catch (IOException e) { } catch (IOException e) {
manager.close(); manager.close();
throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e); throw new AccountCheckException("Error while checking account " + identifier + ": " + e.getMessage(), e);
} }
if (account.getServiceEnvironment() == null) { if (account.getServiceEnvironment() == null) {
@ -153,15 +196,14 @@ public class SignalAccountFiles {
} }
public RegistrationManager initRegistrationManager( public RegistrationManager initRegistrationManager(
String number, Consumer<Manager> newManagerListener String number,
Consumer<Manager> newManagerListener
) throws IOException { ) throws IOException {
final var accountPath = accountsStore.getPathByNumber(number); final var accountPath = accountsStore.getPathByNumber(number);
if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) { if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath; final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
var aciIdentityKey = KeyUtils.generateIdentityKeyPair(); var aciIdentityKey = KeyUtils.generateIdentityKeyPair();
var pniIdentityKey = KeyUtils.generateIdentityKeyPair(); var pniIdentityKey = KeyUtils.generateIdentityKeyPair();
var registrationId = KeyHelper.generateRegistrationId(false);
var pniRegistrationId = KeyHelper.generateRegistrationId(false);
var profileKey = KeyUtils.createProfileKey(); var profileKey = KeyUtils.createProfileKey();
var account = SignalAccount.create(pathConfig.dataPath(), var account = SignalAccount.create(pathConfig.dataPath(),
@ -170,10 +212,9 @@ public class SignalAccountFiles {
serviceEnvironment, serviceEnvironment,
aciIdentityKey, aciIdentityKey,
pniIdentityKey, pniIdentityKey,
registrationId,
pniRegistrationId,
profileKey, profileKey,
settings); settings);
account.initDatabase();
return new RegistrationManagerImpl(account, return new RegistrationManagerImpl(account,
pathConfig, pathConfig,
@ -188,6 +229,7 @@ public class SignalAccountFiles {
account.close(); account.close();
throw new IOException("Number in account file doesn't match expected number: " + account.getNumber()); throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
} }
account.initDatabase();
return new RegistrationManagerImpl(account, return new RegistrationManagerImpl(account,
pathConfig, pathConfig,

View File

@ -2,7 +2,7 @@ package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context; import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.whispersystems.signalservice.api.push.ServiceId; import org.signal.core.models.ServiceId;
public class RenewSessionAction implements HandleAction { public class RenewSessionAction implements HandleAction {
@ -19,10 +19,8 @@ public class RenewSessionAction implements HandleAction {
@Override @Override
public void execute(Context context) throws Throwable { public void execute(Context context) throws Throwable {
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId); context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
if (!recipientId.equals(context.getAccount().getSelfRecipientId())) {
context.getSendHelper().sendNullMessage(recipientId); context.getSendHelper().sendNullMessage(recipientId);
} }
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {

View File

@ -13,7 +13,9 @@ public class ResendMessageAction implements HandleAction {
private final MessageSendLogEntry messageSendLogEntry; private final MessageSendLogEntry messageSendLogEntry;
public ResendMessageAction( public ResendMessageAction(
final RecipientId recipientId, final long timestamp, final MessageSendLogEntry messageSendLogEntry final RecipientId recipientId,
final long timestamp,
final MessageSendLogEntry messageSendLogEntry
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.timestamp = timestamp; this.timestamp = timestamp;

View File

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

View File

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

View File

@ -15,7 +15,9 @@ public class SendReceiptAction implements HandleAction {
private final List<Long> timestamps = new ArrayList<>(); private final List<Long> timestamps = new ArrayList<>();
public SendReceiptAction( 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.recipientId = recipientId;
this.type = type; this.type = type;

View File

@ -7,7 +7,6 @@ import org.signal.libsignal.metadata.ProtocolException;
import org.signal.libsignal.protocol.message.CiphertextMessage; import org.signal.libsignal.protocol.message.CiphertextMessage;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.internal.push.Envelope; import org.whispersystems.signalservice.internal.push.Envelope;
import java.util.Optional; import java.util.Optional;
@ -15,29 +14,21 @@ import java.util.Optional;
public class SendRetryMessageRequestAction implements HandleAction { public class SendRetryMessageRequestAction implements HandleAction {
private final RecipientId recipientId; private final RecipientId recipientId;
private final ServiceId serviceId;
private final ProtocolException protocolException; private final ProtocolException protocolException;
private final SignalServiceEnvelope envelope; private final SignalServiceEnvelope envelope;
private final ServiceId accountId;
public SendRetryMessageRequestAction( public SendRetryMessageRequestAction(
final RecipientId recipientId, final RecipientId recipientId,
final ServiceId serviceId,
final ProtocolException protocolException, final ProtocolException protocolException,
final SignalServiceEnvelope envelope, final SignalServiceEnvelope envelope
final ServiceId accountId
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.serviceId = serviceId;
this.protocolException = protocolException; this.protocolException = protocolException;
this.envelope = envelope; this.envelope = envelope;
this.accountId = accountId;
} }
@Override @Override
public void execute(Context context) throws Throwable { public void execute(Context context) throws Throwable {
context.getAccount().getAccountData(accountId).getSessionStore().archiveSessions(serviceId);
int senderDevice = protocolException.getSenderDevice(); int senderDevice = protocolException.getSenderDevice();
Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion( Optional<GroupId> groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion(
protocolException.getGroupId().get())) : Optional.empty(); protocolException.getGroupId().get())) : Optional.empty();
@ -50,7 +41,9 @@ public class SendRetryMessageRequestAction implements HandleAction {
envelopeType = messageContent.getType(); envelopeType = messageContent.getType();
} else { } else {
originalContent = envelope.getContent(); originalContent = envelope.getContent();
envelopeType = envelopeTypeToCiphertextMessageType(envelope.getType()); envelopeType = envelope.getType() == null
? CiphertextMessage.WHISPER_TYPE
: envelopeTypeToCiphertextMessageType(envelope.getType());
} }
DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent, DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent,
@ -67,7 +60,7 @@ public class SendRetryMessageRequestAction implements HandleAction {
return CiphertextMessage.WHISPER_TYPE; return CiphertextMessage.WHISPER_TYPE;
} }
return switch (type) { return switch (type) {
case PREKEY_BUNDLE -> CiphertextMessage.PREKEY_TYPE; case PREKEY_MESSAGE -> CiphertextMessage.PREKEY_TYPE;
case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE; case UNIDENTIFIED_SENDER -> CiphertextMessage.SENDERKEY_TYPE;
case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE; case PLAINTEXT_CONTENT -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE;
default -> CiphertextMessage.WHISPER_TYPE; default -> CiphertextMessage.WHISPER_TYPE;

View File

@ -0,0 +1,21 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.jobs.SyncStorageJob;
public class SyncStorageDataAction implements HandleAction {
private static final SyncStorageDataAction INSTANCE = new SyncStorageDataAction();
private SyncStorageDataAction() {
}
public static SyncStorageDataAction create() {
return INSTANCE;
}
@Override
public void execute(Context context) throws Throwable {
context.getJobExecutor().enqueueJob(new SyncStorageJob());
}
}

View File

@ -0,0 +1,21 @@
package org.asamk.signal.manager.api;
public record CallInfo(
long callId,
State state,
RecipientAddress recipient,
String inputDeviceName,
String outputDeviceName,
boolean isOutgoing
) {
public enum State {
IDLE,
RINGING_INCOMING,
RINGING_OUTGOING,
CONNECTING,
CONNECTED,
RECONNECTING,
ENDED
}
}

View File

@ -0,0 +1,11 @@
package org.asamk.signal.manager.api;
public record CallOffer(
long callId, Type type, byte[] opaque
) {
public enum Type {
AUDIO,
VIDEO
}
}

View File

@ -0,0 +1,16 @@
package org.asamk.signal.manager.api;
public class CaptchaRejectedException extends Exception {
public CaptchaRejectedException() {
super("Captcha rejected");
}
public CaptchaRejectedException(final String message) {
super(message);
}
public CaptchaRejectedException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -2,11 +2,11 @@ package org.asamk.signal.manager.api;
public class CaptchaRequiredException extends Exception { public class CaptchaRequiredException extends Exception {
private long nextAttemptTimestamp; private long nextVerificationAttemptMilliseconds;
public CaptchaRequiredException(final long nextAttemptTimestamp) { public CaptchaRequiredException(final long nextVerificationAttemptMilliseconds) {
super("Captcha required"); super("Captcha required");
this.nextAttemptTimestamp = nextAttemptTimestamp; this.nextVerificationAttemptMilliseconds = nextVerificationAttemptMilliseconds;
} }
public CaptchaRequiredException(final String message) { public CaptchaRequiredException(final String message) {
@ -17,7 +17,7 @@ public class CaptchaRequiredException extends Exception {
super(message, cause); super(message, cause);
} }
public long getNextAttemptTimestamp() { public long getNextVerificationAttemptMilliseconds() {
return nextAttemptTimestamp; return nextVerificationAttemptMilliseconds;
} }
} }

View File

@ -2,50 +2,44 @@ package org.asamk.signal.manager.api;
import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.signalservice.internal.util.Util;
import java.util.Objects; public record Contact(
String givenName,
public class Contact { String familyName,
String nickName,
private final String givenName; String nickNameGivenName,
String nickNameFamilyName,
private final String familyName; String note,
String color,
private final String color; int messageExpirationTime,
int messageExpirationTimeVersion,
private final int messageExpirationTime; long muteUntil,
boolean hideStory,
private final boolean blocked; boolean isBlocked,
long blockedAt,
private final boolean archived; boolean isArchived,
boolean isProfileSharingEnabled,
private final boolean profileSharingEnabled; boolean isHidden,
Long unregisteredTimestamp
public Contact(
final String givenName,
final String familyName,
final String color,
final int messageExpirationTime,
final boolean blocked,
final boolean archived,
final boolean profileSharingEnabled
) { ) {
this.givenName = givenName;
this.familyName = familyName;
this.color = color;
this.messageExpirationTime = messageExpirationTime;
this.blocked = blocked;
this.archived = archived;
this.profileSharingEnabled = profileSharingEnabled;
}
private Contact(final Builder builder) { private Contact(final Builder builder) {
givenName = builder.givenName; this(builder.givenName,
familyName = builder.familyName; builder.familyName,
color = builder.color; builder.nickName,
messageExpirationTime = builder.messageExpirationTime; builder.nickNameGivenName,
blocked = builder.blocked; builder.nickNameFamilyName,
archived = builder.archived; builder.note,
profileSharingEnabled = builder.profileSharingEnabled; builder.color,
builder.messageExpirationTime,
builder.messageExpirationTimeVersion,
builder.muteUntil,
builder.hideStory,
builder.isBlocked,
builder.blockedAt,
builder.isArchived,
builder.isProfileSharingEnabled,
builder.isHidden,
builder.unregisteredTimestamp);
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -54,13 +48,23 @@ public class Contact {
public static Builder newBuilder(final Contact copy) { public static Builder newBuilder(final Contact copy) {
Builder builder = new Builder(); Builder builder = new Builder();
builder.givenName = copy.getGivenName(); builder.givenName = copy.givenName();
builder.familyName = copy.getFamilyName(); builder.familyName = copy.familyName();
builder.color = copy.getColor(); builder.nickName = copy.nickName();
builder.messageExpirationTime = copy.getMessageExpirationTime(); builder.nickNameGivenName = copy.nickNameGivenName();
builder.blocked = copy.isBlocked(); builder.nickNameFamilyName = copy.nickNameFamilyName();
builder.archived = copy.isArchived(); builder.note = copy.note();
builder.profileSharingEnabled = copy.isProfileSharingEnabled(); builder.color = copy.color();
builder.messageExpirationTime = copy.messageExpirationTime();
builder.messageExpirationTimeVersion = copy.messageExpirationTimeVersion();
builder.muteUntil = copy.muteUntil();
builder.hideStory = copy.hideStory();
builder.isBlocked = copy.isBlocked();
builder.blockedAt = copy.blockedAt();
builder.isArchived = copy.isArchived();
builder.isProfileSharingEnabled = copy.isProfileSharingEnabled();
builder.isHidden = copy.isHidden();
builder.unregisteredTimestamp = copy.unregisteredTimestamp();
return builder; return builder;
} }
@ -79,72 +83,48 @@ public class Contact {
return givenName + " " + familyName; return givenName + " " + familyName;
} }
public String getGivenName() { public String getDisplayNickname() {
return givenName; final var noNickGivenName = Util.isEmpty(nickNameGivenName);
final var noNickFamilyName = Util.isEmpty(nickNameFamilyName);
if (noNickGivenName && noNickFamilyName) {
return null;
} else if (noNickGivenName) {
return nickNameFamilyName;
} else if (noNickFamilyName) {
return nickNameGivenName;
} }
public String getFamilyName() { return nickNameGivenName + " " + nickNameFamilyName;
return familyName;
}
public String getColor() {
return color;
}
public int getMessageExpirationTime() {
return messageExpirationTime;
}
public boolean isBlocked() {
return blocked;
}
public boolean isArchived() {
return archived;
}
public boolean isProfileSharingEnabled() {
return profileSharingEnabled;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Contact contact = (Contact) o;
return messageExpirationTime == contact.messageExpirationTime
&& blocked == contact.blocked
&& archived == contact.archived
&& profileSharingEnabled == contact.profileSharingEnabled
&& Objects.equals(givenName, contact.givenName)
&& Objects.equals(familyName, contact.familyName)
&& Objects.equals(color, contact.color);
}
@Override
public int hashCode() {
return Objects.hash(givenName,
familyName,
color,
messageExpirationTime,
blocked,
archived,
profileSharingEnabled);
} }
public static final class Builder { public static final class Builder {
private String givenName; private String givenName;
private String familyName; private String familyName;
private String nickName;
private String nickNameGivenName;
private String nickNameFamilyName;
private String note;
private String color; private String color;
private int messageExpirationTime; private int messageExpirationTime;
private boolean blocked; private int messageExpirationTimeVersion = 1;
private boolean archived; private long muteUntil;
private boolean profileSharingEnabled; private boolean hideStory;
private boolean isBlocked;
private long blockedAt;
private boolean isArchived;
private boolean isProfileSharingEnabled;
private boolean isHidden;
private Long unregisteredTimestamp;
private Builder() { private Builder() {
} }
public static Builder newBuilder() {
return new Builder();
}
public Builder withGivenName(final String val) { public Builder withGivenName(final String val) {
givenName = val; givenName = val;
return this; return this;
@ -155,6 +135,26 @@ public class Contact {
return this; return this;
} }
public Builder withNickName(final String val) {
nickName = val;
return this;
}
public Builder withNickNameGivenName(final String val) {
nickNameGivenName = val;
return this;
}
public Builder withNickNameFamilyName(final String val) {
nickNameFamilyName = val;
return this;
}
public Builder withNote(final String val) {
note = val;
return this;
}
public Builder withColor(final String val) { public Builder withColor(final String val) {
color = val; color = val;
return this; return this;
@ -165,18 +165,53 @@ public class Contact {
return this; return this;
} }
public Builder withBlocked(final boolean val) { public Builder withMessageExpirationTimeVersion(final int val) {
blocked = val; messageExpirationTimeVersion = val;
return this; return this;
} }
public Builder withArchived(final boolean val) { public Builder withMuteUntil(final long val) {
archived = val; muteUntil = val;
return this; return this;
} }
public Builder withProfileSharingEnabled(final boolean val) { public Builder withHideStory(final boolean val) {
profileSharingEnabled = val; hideStory = val;
return this;
}
public Builder withIsBlocked(final boolean val) {
if (val && !isBlocked) {
blockedAt = System.currentTimeMillis();
} else if (!val) {
blockedAt = 0;
}
isBlocked = val;
return this;
}
public Builder withBlockedAt(final long val) {
blockedAt = val;
return this;
}
public Builder withIsArchived(final boolean val) {
isArchived = val;
return this;
}
public Builder withIsProfileSharingEnabled(final boolean val) {
isProfileSharingEnabled = val;
return this;
}
public Builder withIsHidden(final boolean val) {
isHidden = val;
return this;
}
public Builder withUnregisteredTimestamp(final Long val) {
unregisteredTimestamp = val;
return this; return this;
} }

View File

@ -0,0 +1,12 @@
package org.asamk.signal.manager.api;
public class DeviceLimitExceededException extends Exception {
public DeviceLimitExceededException(final String message) {
super(message);
}
public DeviceLimitExceededException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -2,7 +2,6 @@ package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import java.net.URI; import java.net.URI;
@ -37,7 +36,7 @@ public record DeviceLinkUrl(String deviceIdentifier, ECPublicKey deviceKey) {
} }
ECPublicKey deviceKey; ECPublicKey deviceKey;
try { try {
deviceKey = Curve.decodePoint(publicKeyBytes, 0); deviceKey = new ECPublicKey(publicKeyBytes);
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new InvalidDeviceLinkException("Invalid device link", e); throw new InvalidDeviceLinkException("Invalid device link", e);
} }

View File

@ -12,10 +12,9 @@ public record Group(
String title, String title,
String description, String description,
GroupInviteLinkUrl groupInviteLinkUrl, GroupInviteLinkUrl groupInviteLinkUrl,
Set<RecipientAddress> members, Set<GroupMember> members,
Set<RecipientAddress> pendingMembers, Set<RecipientAddress> pendingMembers,
Set<RecipientAddress> requestingMembers, Set<RecipientAddress> requestingMembers,
Set<RecipientAddress> adminMembers,
Set<RecipientAddress> bannedMembers, Set<RecipientAddress> bannedMembers,
boolean isBlocked, boolean isBlocked,
int messageExpirationTimer, int messageExpirationTimer,
@ -23,11 +22,14 @@ public record Group(
GroupPermission permissionEditDetails, GroupPermission permissionEditDetails,
GroupPermission permissionSendMessage, GroupPermission permissionSendMessage,
boolean isMember, boolean isMember,
boolean isAdmin boolean isAdmin,
boolean isTerminated
) { ) {
public static Group from( public static Group from(
final GroupInfo groupInfo, final RecipientAddressResolver recipientStore, final RecipientId selfRecipientId final GroupInfo groupInfo,
final RecipientAddressResolver recipientStore,
final RecipientId selfRecipientId
) { ) {
return new Group(groupInfo.getGroupId(), return new Group(groupInfo.getGroupId(),
groupInfo.getTitle(), groupInfo.getTitle(),
@ -35,8 +37,7 @@ public record Group(
groupInfo.getGroupInviteLink(), groupInfo.getGroupInviteLink(),
groupInfo.getMembers() groupInfo.getMembers()
.stream() .stream()
.map(recipientStore::resolveRecipientAddress) .map(m -> org.asamk.signal.manager.api.GroupMember.from(m, recipientStore))
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()), .collect(Collectors.toSet()),
groupInfo.getPendingMembers() groupInfo.getPendingMembers()
.stream() .stream()
@ -48,11 +49,6 @@ public record Group(
.map(recipientStore::resolveRecipientAddress) .map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress) .map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()), .collect(Collectors.toSet()),
groupInfo.getAdminMembers()
.stream()
.map(recipientStore::resolveRecipientAddress)
.map(org.asamk.signal.manager.storage.recipients.RecipientAddress::toApiRecipientAddress)
.collect(Collectors.toSet()),
groupInfo.getBannedMembers() groupInfo.getBannedMembers()
.stream() .stream()
.map(recipientStore::resolveRecipientAddress) .map(recipientStore::resolveRecipientAddress)
@ -64,6 +60,7 @@ public record Group(
groupInfo.getPermissionEditDetails(), groupInfo.getPermissionEditDetails(),
groupInfo.getPermissionSendMessage(), groupInfo.getPermissionSendMessage(),
groupInfo.isMember(selfRecipientId), groupInfo.isMember(selfRecipientId),
groupInfo.isAdmin(selfRecipientId)); groupInfo.isAdmin(selfRecipientId),
groupInfo.isTerminated());
} }
} }

View File

@ -1,11 +1,11 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupLinkPassword; import org.asamk.signal.manager.groups.GroupLinkPassword;
import org.signal.core.util.Base64;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.storageservice.protos.groups.GroupInviteLink; import org.signal.storageservice.storage.protos.groups.GroupInviteLink;
import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.storage.protos.groups.local.DecryptedGroup;
import org.whispersystems.util.Base64UrlSafe;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -39,21 +39,21 @@ public final class GroupInviteLinkUrl {
} }
try { try {
if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) { if (!"/".equals(uri.getPath()) && !uri.getPath().isEmpty()) {
throw new InvalidGroupLinkException("No path was expected in uri"); throw new InvalidGroupLinkException("No path was expected in uri");
} }
var encoding = uri.getFragment(); var encoding = uri.getFragment();
if (encoding == null || encoding.length() == 0) { if (encoding == null || encoding.isEmpty()) {
throw new InvalidGroupLinkException("No reference was in the uri"); throw new InvalidGroupLinkException("No reference was in the uri");
} }
var bytes = Base64UrlSafe.decodePaddingAgnostic(encoding); var bytes = Base64.decode(encoding);
GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes); GroupInviteLink groupInviteLink = GroupInviteLink.ADAPTER.decode(bytes);
if (groupInviteLink.v1Contents != null) { if (groupInviteLink.contentsV1 != null) {
var groupInviteLinkContentsV1 = groupInviteLink.v1Contents; var groupInviteLinkContentsV1 = groupInviteLink.contentsV1;
var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray()); var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.groupMasterKey.toByteArray());
var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray()); var password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.inviteLinkPassword.toByteArray());
@ -90,12 +90,12 @@ public final class GroupInviteLinkUrl {
} }
private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) { private static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
var groupInviteLink = new GroupInviteLink.Builder().v1Contents(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey( var groupInviteLink = new GroupInviteLink.Builder().contentsV1(new GroupInviteLink.GroupInviteLinkContentsV1.Builder().groupMasterKey(
ByteString.of(groupMasterKey.serialize())) ByteString.of(groupMasterKey.serialize()))
.inviteLinkPassword(ByteString.of(password.serialize())) .inviteLinkPassword(ByteString.of(password.serialize()))
.build()).build(); .build()).build();
var encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.encode()); var encoding = Base64.encodeUrlSafeWithoutPadding(groupInviteLink.encode());
return GROUP_URL_PREFIX + encoding; return GROUP_URL_PREFIX + encoding;
} }
@ -112,7 +112,7 @@ public final class GroupInviteLinkUrl {
return password; return password;
} }
public final static class InvalidGroupLinkException extends Exception { public static final class InvalidGroupLinkException extends Exception {
public InvalidGroupLinkException(String message) { public InvalidGroupLinkException(String message) {
super(message); super(message);
@ -123,7 +123,7 @@ public final class GroupInviteLinkUrl {
} }
} }
public final static class UnknownGroupLinkVersionException extends Exception { public static final class UnknownGroupLinkVersionException extends Exception {
public UnknownGroupLinkVersionException(String message) { public UnknownGroupLinkVersionException(String message) {
super(message); super(message);

View File

@ -0,0 +1,14 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.groups.GroupMemberInfo;
public record GroupMember(
RecipientAddress recipientAddress, boolean isAdmin, String labelEmoji, String label
) {
public static GroupMember from(final GroupMemberInfo memberInfo, final RecipientAddressResolver recipientStore) {
return new GroupMember(recipientStore.resolveRecipientAddress(memberInfo.getRecipientId())
.toApiRecipientAddress(), memberInfo.isAdmin(), memberInfo.labelEmoji(), memberInfo.labelString());
}
}

View File

@ -1,17 +1,10 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.signal.libsignal.protocol.IdentityKey;
public record Identity( public record Identity(
RecipientAddress recipient, RecipientAddress recipient,
IdentityKey identityKey, byte[] fingerprint,
String safetyNumber, String safetyNumber,
byte[] scannableSafetyNumber, byte[] scannableSafetyNumber,
TrustLevel trustLevel, TrustLevel trustLevel,
long dateAddedTimestamp long dateAddedTimestamp
) { ) {}
public byte[] getFingerprint() {
return identityKey.getPublicKey().serialize();
}
}

View File

@ -2,6 +2,10 @@ package org.asamk.signal.manager.api;
public class InvalidNumberException extends Exception { public class InvalidNumberException extends Exception {
public InvalidNumberException(String message) {
super(message);
}
InvalidNumberException(String message, Throwable e) { InvalidNumberException(String message, Throwable e) {
super(message, e); super(message, e);
} }

View File

@ -6,12 +6,15 @@ import java.util.Optional;
public record Message( public record Message(
String messageText, String messageText,
List<String> attachments, List<String> attachments,
boolean viewOnce,
boolean voiceNote,
List<Mention> mentions, List<Mention> mentions,
Optional<Quote> quote, Optional<Quote> quote,
Optional<Sticker> sticker, Optional<Sticker> sticker,
List<Preview> previews, List<Preview> previews,
Optional<StoryReply> storyReply, Optional<StoryReply> storyReply,
List<TextStyle> textStyles List<TextStyle> textStyles,
boolean urgent
) { ) {
public record Mention(RecipientIdentifier.Single recipient, int start, int length) {} public record Mention(RecipientIdentifier.Single recipient, int start, int length) {}
@ -21,8 +24,12 @@ public record Message(
RecipientIdentifier.Single author, RecipientIdentifier.Single author,
String message, String message,
List<Mention> mentions, List<Mention> mentions,
List<TextStyle> textStyles List<TextStyle> textStyles,
) {} List<Attachment> attachments
) {
public record Attachment(String contentType, String filename, String preview) {}
}
public record Sticker(byte[] packId, int stickerId) {} public record Sticker(byte[] packId, int stickerId) {}

View File

@ -3,6 +3,7 @@ package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.helper.RecipientAddressResolver; import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.util.MimeUtils;
import org.signal.libsignal.metadata.ProtocolException; import org.signal.libsignal.metadata.ProtocolException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@ -34,7 +35,9 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -113,17 +116,35 @@ public record MessageEnvelope(
Optional<Long> remoteDeleteId, Optional<Long> remoteDeleteId,
Optional<Sticker> sticker, Optional<Sticker> sticker,
List<SharedContact> sharedContacts, List<SharedContact> sharedContacts,
Optional<PollCreate> pollCreate,
Optional<PollVote> pollVote,
Optional<PollTerminate> pollTerminate,
List<Mention> mentions, List<Mention> mentions,
List<Preview> previews, List<Preview> previews,
List<TextStyle> textStyles List<TextStyle> textStyles,
Optional<PinMessage> pinMessage,
Optional<UnpinMessage> unpinMessage,
Optional<AdminDelete> adminDelete
) { ) {
static Data from( static Data from(
final SignalServiceDataMessage dataMessage, final SignalServiceDataMessage dataMessage,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider final AttachmentFileProvider fileProvider
) { ) {
var body = dataMessage.getBody();
if (dataMessage.getAttachments().isPresent()) {
for (final var attachment : dataMessage.getAttachments().get()) {
if (MimeUtils.LONG_TEXT.equals(attachment.getContentType()) && attachment.isPointer()) {
final var longBody = longTexts.get(attachment.asPointer().getRemoteId().toString());
if (longBody != null) {
body = Optional.of(longBody);
}
}
}
}
return new Data(dataMessage.getTimestamp(), return new Data(dataMessage.getTimestamp(),
dataMessage.getGroupContext().map(GroupContext::from), dataMessage.getGroupContext().map(GroupContext::from),
dataMessage.getStoryContext() dataMessage.getStoryContext()
@ -131,15 +152,17 @@ public record MessageEnvelope(
recipientResolver, recipientResolver,
addressResolver)), addressResolver)),
dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from), dataMessage.getGroupCallUpdate().map(GroupCallUpdate::from),
dataMessage.getBody(), body,
dataMessage.getExpiresInSeconds(), dataMessage.getExpiresInSeconds(),
dataMessage.isExpirationUpdate(), dataMessage.isExpirationUpdate(),
dataMessage.isViewOnce(), dataMessage.isViewOnce(),
dataMessage.isEndSession(), false,
dataMessage.isProfileKeyUpdate(), dataMessage.isProfileKeyUpdate(),
dataMessage.getProfileKey().isPresent(), dataMessage.getProfileKey().isPresent(),
dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)), dataMessage.getReaction().map(r -> Reaction.from(r, recipientResolver, addressResolver)),
dataMessage.getQuote().map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)), dataMessage.getQuote()
.filter(q -> q.getAuthor() != null && q.getAuthor().isValid())
.map(q -> Quote.from(q, recipientResolver, addressResolver, fileProvider)),
dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null), dataMessage.getPayment().map(p -> p.getPaymentNotification().isPresent() ? Payment.from(p) : null),
dataMessage.getAttachments() dataMessage.getAttachments()
.map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList()) .map(a -> a.stream().map(as -> Attachment.from(as, fileProvider)).toList())
@ -151,6 +174,9 @@ public record MessageEnvelope(
.map(sharedContact -> SharedContact.from(sharedContact, fileProvider)) .map(sharedContact -> SharedContact.from(sharedContact, fileProvider))
.toList()) .toList())
.orElse(List.of()), .orElse(List.of()),
dataMessage.getPollCreate().map(PollCreate::from),
dataMessage.getPollVote().map(p -> PollVote.from(p, recipientResolver, addressResolver)),
dataMessage.getPollTerminate().map(PollTerminate::from),
dataMessage.getMentions() dataMessage.getMentions()
.map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList()) .map(a -> a.stream().map(m -> Mention.from(m, recipientResolver, addressResolver)).toList())
.orElse(List.of()), .orElse(List.of()),
@ -159,7 +185,10 @@ public record MessageEnvelope(
.orElse(List.of()), .orElse(List.of()),
dataMessage.getBodyRanges() dataMessage.getBodyRanges()
.map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList()) .map(a -> a.stream().filter(r -> r.style != null).map(TextStyle::from).toList())
.orElse(List.of())); .orElse(List.of()),
dataMessage.getPinnedMessage().map(p -> PinMessage.from(p, recipientResolver, addressResolver)),
dataMessage.getUnpinnedMessage().map(p -> UnpinMessage.from(p, recipientResolver, addressResolver)),
dataMessage.getAdminDelete().map(p -> AdminDelete.from(p, recipientResolver, addressResolver)));
} }
public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) { public record GroupContext(GroupId groupId, boolean isGroupUpdate, int revision) {
@ -234,7 +263,7 @@ public record MessageEnvelope(
return new Quote(quote.getId(), return new Quote(quote.getId(),
addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor())) addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(quote.getAuthor()))
.toApiRecipientAddress(), .toApiRecipientAddress(),
Optional.ofNullable(quote.getText()), Optional.of(quote.getText()),
quote.getMentions() == null quote.getMentions() == null
? List.of() ? List.of()
: quote.getMentions() : quote.getMentions()
@ -291,9 +320,9 @@ public record MessageEnvelope(
boolean isBorderless boolean isBorderless
) { ) {
static Attachment from(SignalServiceAttachment attachment, AttachmentFileProvider fileProvider) { static Attachment from(SignalServiceAttachment signalAttachment, AttachmentFileProvider fileProvider) {
if (attachment.isPointer()) { if (signalAttachment.isPointer()) {
final var a = attachment.asPointer(); final var a = signalAttachment.asPointer();
final var attachmentFile = fileProvider.getFile(a); final var attachmentFile = fileProvider.getFile(a);
return new Attachment(Optional.of(attachmentFile.getName()), return new Attachment(Optional.of(attachmentFile.getName()),
Optional.of(attachmentFile), Optional.of(attachmentFile),
@ -310,8 +339,9 @@ public record MessageEnvelope(
a.isGif(), a.isGif(),
a.isBorderless()); a.isBorderless());
} else { } else {
final var a = attachment.asStream(); Attachment attachment = null;
return new Attachment(Optional.empty(), try (final var a = signalAttachment.asStream()) {
attachment = new Attachment(Optional.empty(),
Optional.empty(), Optional.empty(),
a.getFileName(), a.getFileName(),
a.getContentType(), a.getContentType(),
@ -325,11 +355,16 @@ public record MessageEnvelope(
a.getVoiceNote(), a.getVoiceNote(),
a.isGif(), a.isGif(),
a.isBorderless()); a.isBorderless());
return attachment;
} catch (IOException e) {
return attachment;
}
} }
} }
static Attachment from( static Attachment from(
SignalServiceDataMessage.Quote.QuotedAttachment a, final AttachmentFileProvider fileProvider SignalServiceDataMessage.Quote.QuotedAttachment a,
final AttachmentFileProvider fileProvider
) { ) {
return new Attachment(Optional.empty(), return new Attachment(Optional.empty(),
Optional.empty(), Optional.empty(),
@ -381,7 +416,7 @@ public record MessageEnvelope(
} }
public record Name( public record Name(
Optional<String> display, Optional<String> nickname,
Optional<String> given, Optional<String> given,
Optional<String> family, Optional<String> family,
Optional<String> prefix, Optional<String> prefix,
@ -390,7 +425,7 @@ public record MessageEnvelope(
) { ) {
static Name from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Name name) { static Name from(org.whispersystems.signalservice.api.messages.shared.SharedContact.Name name) {
return new Name(name.getDisplay(), return new Name(name.getNickname(),
name.getGiven(), name.getGiven(),
name.getFamily(), name.getFamily(),
name.getPrefix(), name.getPrefix(),
@ -472,15 +507,15 @@ public record MessageEnvelope(
) { ) {
static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) { static Address from(org.whispersystems.signalservice.api.messages.shared.SharedContact.PostalAddress address) {
return new Address(Address.Type.from(address.getType()), return new Address(Type.from(address.getType()),
address.getLabel(), address.getLabel(),
address.getLabel(), address.getStreet(),
address.getLabel(), address.getPobox(),
address.getLabel(), address.getNeighborhood(),
address.getLabel(), address.getCity(),
address.getLabel(), address.getRegion(),
address.getLabel(), address.getPostcode(),
address.getLabel()); address.getCountry());
} }
public enum Type { public enum Type {
@ -499,11 +534,47 @@ public record MessageEnvelope(
} }
} }
public record PollCreate(
String question, boolean allowMultiple, List<String> options
) {
static PollCreate from(
SignalServiceDataMessage.PollCreate pollCreate
) {
return new PollCreate(pollCreate.getQuestion(), pollCreate.getAllowMultiple(), pollCreate.getOptions());
}
}
public record PollVote(
RecipientAddress targetAuthor, long targetSentTimestamp, List<Integer> optionIndexes, int voteCount
) {
static PollVote from(
SignalServiceDataMessage.PollVote pollVote,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new PollVote(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(pollVote.getTargetAuthor()))
.toApiRecipientAddress(),
pollVote.getTargetSentTimestamp(),
pollVote.getOptionIndexes(),
pollVote.getVoteCount());
}
}
public record PollTerminate(long targetSentTimestamp) {
static PollTerminate from(SignalServiceDataMessage.PollTerminate pollTerminate) {
return new PollTerminate(pollTerminate.getTargetSentTimestamp());
}
}
public record Preview(String title, String description, long date, String url, Optional<Attachment> image) { public record Preview(String title, String description, long date, String url, Optional<Attachment> image) {
static Preview from( static Preview from(SignalServicePreview preview, final AttachmentFileProvider fileProvider) {
SignalServicePreview preview, final AttachmentFileProvider fileProvider
) {
return new Preview(preview.getTitle(), return new Preview(preview.getTitle(),
preview.getDescription(), preview.getDescription(),
preview.getDate(), preview.getDate(),
@ -512,18 +583,68 @@ public record MessageEnvelope(
} }
} }
public record PinMessage(
RecipientAddress targetAuthor, long targetSentTimestamp, long pinDurationSeconds
) {
static PinMessage from(
SignalServiceDataMessage.PinnedMessage pinnedMessage,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new PinMessage(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
pinnedMessage.getTargetAuthor())).toApiRecipientAddress(),
pinnedMessage.getTargetSentTimestamp(),
Boolean.TRUE.equals(pinnedMessage.getForever())
? -1
: pinnedMessage.getPinDurationInSeconds() == null
? 0
: pinnedMessage.getPinDurationInSeconds());
}
}
public record UnpinMessage(RecipientAddress targetAuthor, long targetSentTimestamp) {
static UnpinMessage from(
SignalServiceDataMessage.UnpinnedMessage unpinnedMessage,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new UnpinMessage(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
unpinnedMessage.getTargetAuthor())).toApiRecipientAddress(),
unpinnedMessage.getTargetSentTimestamp());
}
}
public record AdminDelete(RecipientAddress targetAuthor, long targetSentTimestamp) {
static AdminDelete from(
SignalServiceDataMessage.AdminDelete adminDelete,
RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver
) {
return new AdminDelete(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
adminDelete.getTargetAuthor())).toApiRecipientAddress(), adminDelete.getTargetSentTimestamp());
}
}
} }
public record Edit(long targetSentTimestamp, Data dataMessage) { public record Edit(long targetSentTimestamp, Data dataMessage) {
public static Edit from( public static Edit from(
final SignalServiceEditMessage editMessage, final SignalServiceEditMessage editMessage,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider final AttachmentFileProvider fileProvider
) { ) {
return new Edit(editMessage.getTargetSentTimestamp(), return new Edit(editMessage.getTargetSentTimestamp(),
Data.from(editMessage.getDataMessage(), recipientResolver, addressResolver, fileProvider)); Data.from(editMessage.getDataMessage(),
longTexts,
recipientResolver,
addressResolver,
fileProvider));
} }
} }
@ -540,12 +661,13 @@ public record MessageEnvelope(
public static Sync from( public static Sync from(
final SignalServiceSyncMessage syncMessage, final SignalServiceSyncMessage syncMessage,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider final AttachmentFileProvider fileProvider
) { ) {
return new Sync(syncMessage.getSent() return new Sync(syncMessage.getSent()
.map(s -> Sent.from(s, recipientResolver, addressResolver, fileProvider)), .map(s -> Sent.from(s, longTexts, recipientResolver, addressResolver, fileProvider)),
syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)), syncMessage.getBlockedList().map(b -> Blocked.from(b, recipientResolver, addressResolver)),
syncMessage.getRead() syncMessage.getRead()
.map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList()) .map(r -> r.stream().map(rm -> Read.from(rm, recipientResolver, addressResolver)).toList())
@ -574,6 +696,7 @@ public record MessageEnvelope(
static Sent from( static Sent from(
SentTranscriptMessage sentMessage, SentTranscriptMessage sentMessage,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider final AttachmentFileProvider fileProvider
@ -589,9 +712,17 @@ public record MessageEnvelope(
.toApiRecipientAddress()) .toApiRecipientAddress())
.collect(Collectors.toSet()), .collect(Collectors.toSet()),
sentMessage.getDataMessage() sentMessage.getDataMessage()
.map(message -> Data.from(message, recipientResolver, addressResolver, fileProvider)), .map(message -> Data.from(message,
longTexts,
recipientResolver,
addressResolver,
fileProvider)),
sentMessage.getEditMessage() sentMessage.getEditMessage()
.map(message -> Edit.from(message, recipientResolver, addressResolver, fileProvider)), .map(message -> Edit.from(message,
longTexts,
recipientResolver,
addressResolver,
fileProvider)),
sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider))); sentMessage.getStoryMessage().map(s -> Story.from(s, fileProvider)));
} }
} }
@ -603,11 +734,15 @@ public record MessageEnvelope(
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver RecipientAddressResolver addressResolver
) { ) {
return new Blocked(blockedListMessage.getAddresses() return new Blocked(blockedListMessage.individuals.stream()
.stream() .map(d -> new RecipientAddress(d.getAci() == null ? null : d.getAci().toString(),
.map(d -> addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(d)) null,
.toApiRecipientAddress()) d.getE164(),
.toList(), blockedListMessage.getGroupIds().stream().map(GroupId::unknownVersion).toList()); null))
.toList(),
blockedListMessage.groups.stream()
.map(group -> GroupId.unknownVersion(group.getGroupId()))
.toList());
} }
} }
@ -618,7 +753,7 @@ public record MessageEnvelope(
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver RecipientAddressResolver addressResolver
) { ) {
return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSender())) return new Read(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(readMessage.getSenderAci()))
.toApiRecipientAddress(), readMessage.getTimestamp()); .toApiRecipientAddress(), readMessage.getTimestamp());
} }
} }
@ -681,7 +816,9 @@ public record MessageEnvelope(
DELETE, DELETE,
BLOCK, BLOCK,
BLOCK_AND_DELETE, BLOCK_AND_DELETE,
UNBLOCK_AND_ACCEPT; UNBLOCK_AND_ACCEPT,
SPAM,
BLOCK_AND_SPAM;
static Type from(MessageRequestResponseMessage.Type type) { static Type from(MessageRequestResponseMessage.Type type) {
return switch (type) { return switch (type) {
@ -691,6 +828,8 @@ public record MessageEnvelope(
case BLOCK -> BLOCK; case BLOCK -> BLOCK;
case BLOCK_AND_DELETE -> BLOCK_AND_DELETE; case BLOCK_AND_DELETE -> BLOCK_AND_DELETE;
case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT; case UNBLOCK_AND_ACCEPT -> UNBLOCK_AND_ACCEPT;
case SPAM -> SPAM;
case BLOCK_AND_SPAM -> BLOCK_AND_SPAM;
}; };
} }
} }
@ -707,7 +846,6 @@ public record MessageEnvelope(
Optional<Busy> busy, Optional<Busy> busy,
List<IceUpdate> iceUpdate, List<IceUpdate> iceUpdate,
Optional<Opaque> opaque, Optional<Opaque> opaque,
boolean isMultiRing,
boolean isUrgent boolean isUrgent
) { ) {
@ -723,17 +861,13 @@ public record MessageEnvelope(
.map(m -> m.stream().map(IceUpdate::from).toList()) .map(m -> m.stream().map(IceUpdate::from).toList())
.orElse(List.of()), .orElse(List.of()),
callMessage.getOpaqueMessage().map(Opaque::from), callMessage.getOpaqueMessage().map(Opaque::from),
callMessage.isMultiRing(),
callMessage.isUrgent()); callMessage.isUrgent());
} }
public record Offer(long id, String sdp, Type type, byte[] opaque) { public record Offer(long id, Type type, byte[] opaque) {
static Offer from(OfferMessage offerMessage) { static Offer from(OfferMessage offerMessage) {
return new Offer(offerMessage.getId(), return new Offer(offerMessage.getId(), Type.from(offerMessage.getType()), offerMessage.getOpaque());
offerMessage.getSdp(),
Type.from(offerMessage.getType()),
offerMessage.getOpaque());
} }
public enum Type { public enum Type {
@ -749,10 +883,10 @@ public record MessageEnvelope(
} }
} }
public record Answer(long id, String sdp, byte[] opaque) { public record Answer(long id, byte[] opaque) {
static Answer from(AnswerMessage answerMessage) { static Answer from(AnswerMessage answerMessage) {
return new Answer(answerMessage.getId(), answerMessage.getSdp(), answerMessage.getOpaque()); return new Answer(answerMessage.getId(), answerMessage.getOpaque());
} }
} }
@ -790,10 +924,10 @@ public record MessageEnvelope(
} }
} }
public record IceUpdate(long id, String sdp, byte[] opaque) { public record IceUpdate(long id, byte[] opaque) {
static IceUpdate from(IceUpdateMessage iceUpdateMessage) { static IceUpdate from(IceUpdateMessage iceUpdateMessage) {
return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getSdp(), iceUpdateMessage.getOpaque()); return new IceUpdate(iceUpdateMessage.getId(), iceUpdateMessage.getOpaque());
} }
} }
@ -824,9 +958,7 @@ public record MessageEnvelope(
Optional<TextAttachment> textAttachment Optional<TextAttachment> textAttachment
) { ) {
public static Story from( public static Story from(SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider) {
SignalServiceStoryMessage storyMessage, final AttachmentFileProvider fileProvider
) {
return new Story(storyMessage.getAllowsReplies().orElse(false), return new Story(storyMessage.getAllowsReplies().orElse(false),
storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())), storyMessage.getGroupContext().map(c -> GroupUtils.getGroupIdV2(c.getMasterKey())),
storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)), storyMessage.getFileAttachment().map(f -> Data.Attachment.from(f, fileProvider)),
@ -844,7 +976,8 @@ public record MessageEnvelope(
) { ) {
static TextAttachment from( static TextAttachment from(
SignalServiceTextAttachment textAttachment, final AttachmentFileProvider fileProvider SignalServiceTextAttachment textAttachment,
final AttachmentFileProvider fileProvider
) { ) {
return new TextAttachment(textAttachment.getText(), return new TextAttachment(textAttachment.getText(),
textAttachment.getStyle().map(Style::from), textAttachment.getStyle().map(Style::from),
@ -891,13 +1024,15 @@ public record MessageEnvelope(
public static MessageEnvelope from( public static MessageEnvelope from(
SignalServiceEnvelope envelope, SignalServiceEnvelope envelope,
SignalServiceContent content, SignalServiceContent content,
Map<String, String> longTexts,
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver, RecipientAddressResolver addressResolver,
final AttachmentFileProvider fileProvider, final AttachmentFileProvider fileProvider,
Exception exception Exception exception
) { ) {
final var source = !envelope.isUnidentifiedSender() && envelope.hasSourceServiceId() final var serviceId = envelope.getSourceServiceId();
? recipientResolver.resolveRecipient(envelope.getSourceAddress()) final var source = !envelope.isUnidentifiedSender() && serviceId != null
? recipientResolver.resolveRecipient(serviceId)
: envelope.isUnidentifiedSender() && content != null : envelope.isUnidentifiedSender() && content != null
? recipientResolver.resolveRecipient(content.getSender()) ? recipientResolver.resolveRecipient(content.getSender())
: exception instanceof ProtocolException e : exception instanceof ProtocolException e
@ -920,9 +1055,15 @@ public record MessageEnvelope(
receipt = content.getReceiptMessage().map(Receipt::from); receipt = content.getReceiptMessage().map(Receipt::from);
typing = content.getTypingMessage().map(Typing::from); typing = content.getTypingMessage().map(Typing::from);
data = content.getDataMessage() data = content.getDataMessage()
.map(dataMessage -> Data.from(dataMessage, recipientResolver, addressResolver, fileProvider)); .map(dataMessage -> Data.from(dataMessage,
edit = content.getEditMessage().map(s -> Edit.from(s, recipientResolver, addressResolver, fileProvider)); longTexts,
sync = content.getSyncMessage().map(s -> Sync.from(s, recipientResolver, addressResolver, fileProvider)); recipientResolver,
addressResolver,
fileProvider));
edit = content.getEditMessage()
.map(s -> Edit.from(s, longTexts, recipientResolver, addressResolver, fileProvider));
sync = content.getSyncMessage()
.map(s -> Sync.from(s, longTexts, recipientResolver, addressResolver, fileProvider));
call = content.getCallMessage().map(Call::from); call = content.getCallMessage().map(Call::from);
story = content.getStoryMessage().map(s -> Story.from(s, fileProvider)); story = content.getStoryMessage().map(s -> Story.from(s, fileProvider));
} else { } else {

View File

@ -3,5 +3,16 @@ package org.asamk.signal.manager.api;
public enum PhoneNumberSharingMode { public enum PhoneNumberSharingMode {
EVERYBODY, EVERYBODY,
CONTACTS, CONTACTS,
NOBODY, NOBODY;
public static PhoneNumberSharingMode valueOfOrNull(String value) {
if (value == null) {
return null;
}
try {
return valueOf(value);
} catch (IllegalArgumentException ignored) {
return null;
}
}
} }

View File

@ -0,0 +1,3 @@
package org.asamk.signal.manager.api;
public class PinLockMissingException extends Exception {}

View File

@ -26,6 +26,8 @@ public class Profile {
private final Set<Capability> capabilities; private final Set<Capability> capabilities;
private final PhoneNumberSharingMode phoneNumberSharingMode;
public Profile( public Profile(
final long lastUpdateTimestamp, final long lastUpdateTimestamp,
final String givenName, final String givenName,
@ -35,7 +37,8 @@ public class Profile {
final String avatarUrlPath, final String avatarUrlPath,
final byte[] mobileCoinAddress, final byte[] mobileCoinAddress,
final UnidentifiedAccessMode unidentifiedAccessMode, final UnidentifiedAccessMode unidentifiedAccessMode,
final Set<Capability> capabilities final Set<Capability> capabilities,
final PhoneNumberSharingMode phoneNumberSharingMode
) { ) {
this.lastUpdateTimestamp = lastUpdateTimestamp; this.lastUpdateTimestamp = lastUpdateTimestamp;
this.givenName = givenName; this.givenName = givenName;
@ -46,6 +49,7 @@ public class Profile {
this.mobileCoinAddress = mobileCoinAddress; this.mobileCoinAddress = mobileCoinAddress;
this.unidentifiedAccessMode = unidentifiedAccessMode; this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities; this.capabilities = capabilities;
this.phoneNumberSharingMode = phoneNumberSharingMode;
} }
private Profile(final Builder builder) { private Profile(final Builder builder) {
@ -58,6 +62,7 @@ public class Profile {
mobileCoinAddress = builder.mobileCoinAddress; mobileCoinAddress = builder.mobileCoinAddress;
unidentifiedAccessMode = builder.unidentifiedAccessMode; unidentifiedAccessMode = builder.unidentifiedAccessMode;
capabilities = builder.capabilities; capabilities = builder.capabilities;
phoneNumberSharingMode = builder.phoneNumberSharingMode;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -136,6 +141,10 @@ public class Profile {
return capabilities; return capabilities;
} }
public PhoneNumberSharingMode getPhoneNumberSharingMode() {
return phoneNumberSharingMode;
}
public enum UnidentifiedAccessMode { public enum UnidentifiedAccessMode {
UNKNOWN, UNKNOWN,
DISABLED, DISABLED,
@ -153,9 +162,7 @@ public class Profile {
public enum Capability { public enum Capability {
storage, storage,
gv1Migration, storageServiceEncryptionV2Capability;
senderKey,
announcementGroup;
public static Capability valueOfOrNull(String value) { public static Capability valueOfOrNull(String value) {
try { try {
@ -203,6 +210,7 @@ public class Profile {
private byte[] mobileCoinAddress; private byte[] mobileCoinAddress;
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
private Set<Capability> capabilities = Collections.emptySet(); private Set<Capability> capabilities = Collections.emptySet();
private PhoneNumberSharingMode phoneNumberSharingMode;
private long lastUpdateTimestamp = 0; private long lastUpdateTimestamp = 0;
private Builder() { private Builder() {
@ -243,6 +251,11 @@ public class Profile {
return this; return this;
} }
public Builder withPhoneNumberSharingMode(final PhoneNumberSharingMode val) {
phoneNumberSharingMode = val;
return this;
}
public Profile build() { public Profile build() {
return new Profile(this); return new Profile(this);
} }

View File

@ -10,12 +10,19 @@ public class ProofRequiredException extends Exception {
private final String token; private final String token;
private final Set<Option> options; private final Set<Option> options;
private final long retryAfterSeconds; private final long retryAfterMilliseconds;
public ProofRequiredException(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) { public ProofRequiredException(final String token, final Set<Option> options, final long retryAfterMilliseconds) {
this.token = e.getToken(); super("Rate limit");
this.options = e.getOptions().stream().map(Option::from).collect(Collectors.toSet()); this.token = token;
this.retryAfterSeconds = e.getRetryAfterSeconds(); this.options = options;
this.retryAfterMilliseconds = retryAfterMilliseconds;
}
public static ProofRequiredException from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException e) {
return new ProofRequiredException(e.getToken(),
e.getOptions().stream().map(Option::from).collect(Collectors.toSet()),
e.getRetryAfterSeconds() * 1000L);
} }
public String getToken() { public String getToken() {
@ -26,17 +33,17 @@ public class ProofRequiredException extends Exception {
return options; return options;
} }
public long getRetryAfterSeconds() { public long getRetryAfterMilliseconds() {
return retryAfterSeconds; return retryAfterMilliseconds;
} }
public enum Option { public enum Option {
RECAPTCHA, CAPTCHA,
PUSH_CHALLENGE; PUSH_CHALLENGE;
static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) { static Option from(org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException.Option option) {
return switch (option) { return switch (option) {
case RECAPTCHA -> RECAPTCHA; case CAPTCHA -> CAPTCHA;
case PUSH_CHALLENGE -> PUSH_CHALLENGE; case PUSH_CHALLENGE -> PUSH_CHALLENGE;
}; };
} }

View File

@ -2,14 +2,18 @@ package org.asamk.signal.manager.api;
public class RateLimitException extends Exception { public class RateLimitException extends Exception {
private final long nextAttemptTimestamp; private final Long retryAfterMilliseconds;
public RateLimitException(final long nextAttemptTimestamp) { public RateLimitException(final Long retryAfterMilliseconds) {
super("Rate limit"); super("Rate limit");
this.nextAttemptTimestamp = nextAttemptTimestamp; this.retryAfterMilliseconds = retryAfterMilliseconds;
} }
public long getNextAttemptTimestamp() { public static RateLimitException from(org.whispersystems.signalservice.api.push.exceptions.RateLimitException e) {
return nextAttemptTimestamp; return new RateLimitException(e.getRetryAfterMilliseconds().orElse(null));
}
public Long getRetryAfterMilliseconds() {
return retryAfterMilliseconds;
} }
} }

View File

@ -1,3 +1,9 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
public record ReceiveConfig(boolean ignoreAttachments, boolean ignoreStories, boolean sendReadReceipts) {} public record ReceiveConfig(
boolean ignoreAttachments,
boolean ignoreStories,
boolean ignoreAvatars,
boolean ignoreStickers,
boolean sendReadReceipts
) {}

View File

@ -20,13 +20,16 @@ public class Recipient {
private final Profile profile; private final Profile profile;
private final Boolean discoverable;
public Recipient( public Recipient(
final RecipientId recipientId, final RecipientId recipientId,
final RecipientAddress address, final RecipientAddress address,
final Contact contact, final Contact contact,
final ProfileKey profileKey, final ProfileKey profileKey,
final ExpiringProfileKeyCredential expiringProfileKeyCredential, final ExpiringProfileKeyCredential expiringProfileKeyCredential,
final Profile profile final Profile profile,
final Boolean discoverable
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.address = address; this.address = address;
@ -34,6 +37,7 @@ public class Recipient {
this.profileKey = profileKey; this.profileKey = profileKey;
this.expiringProfileKeyCredential = expiringProfileKeyCredential; this.expiringProfileKeyCredential = expiringProfileKeyCredential;
this.profile = profile; this.profile = profile;
this.discoverable = discoverable;
} }
private Recipient(final Builder builder) { private Recipient(final Builder builder) {
@ -41,8 +45,9 @@ public class Recipient {
address = builder.address; address = builder.address;
contact = builder.contact; contact = builder.contact;
profileKey = builder.profileKey; profileKey = builder.profileKey;
expiringProfileKeyCredential = builder.expiringProfileKeyCredential1; expiringProfileKeyCredential = builder.expiringProfileKeyCredential;
profile = builder.profile; profile = builder.profile;
discoverable = builder.discoverable;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -55,7 +60,7 @@ public class Recipient {
builder.address = copy.getAddress(); builder.address = copy.getAddress();
builder.contact = copy.getContact(); builder.contact = copy.getContact();
builder.profileKey = copy.getProfileKey(); builder.profileKey = copy.getProfileKey();
builder.expiringProfileKeyCredential1 = copy.getExpiringProfileKeyCredential(); builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
builder.profile = copy.getProfile(); builder.profile = copy.getProfile();
return builder; return builder;
} }
@ -84,6 +89,10 @@ public class Recipient {
return profile; return profile;
} }
public Boolean getDiscoverable() {
return discoverable;
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) return true; if (this == o) return true;
@ -108,8 +117,9 @@ public class Recipient {
private RecipientAddress address; private RecipientAddress address;
private Contact contact; private Contact contact;
private ProfileKey profileKey; private ProfileKey profileKey;
private ExpiringProfileKeyCredential expiringProfileKeyCredential1; private ExpiringProfileKeyCredential expiringProfileKeyCredential;
private Profile profile; private Profile profile;
private Boolean discoverable;
private Builder() { private Builder() {
} }
@ -135,7 +145,7 @@ public class Recipient {
} }
public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) { public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) {
expiringProfileKeyCredential1 = val; expiringProfileKeyCredential = val;
return this; return this;
} }
@ -144,6 +154,11 @@ public class Recipient {
return this; return this;
} }
public Builder withDiscoverable(final Boolean val) {
discoverable = val;
return this;
}
public Recipient build() { public Recipient build() {
return new Recipient(this); return new Recipient(this);
} }

View File

@ -1,49 +1,55 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Optional<String> username) { public record RecipientAddress(
Optional<String> aci, Optional<String> pni, Optional<String> number, Optional<String> username
) {
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID; public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
/** /**
* Construct a RecipientAddress. * Construct a RecipientAddress.
* *
* @param uuid The UUID of the user, if available. * @param aci The ACI of the user, if available.
* @param pni The PNI of the user, if available.
* @param number The phone number of the user, if available. * @param number The phone number of the user, if available.
*/ */
public RecipientAddress { public RecipientAddress {
uuid = uuid.isPresent() && uuid.get().equals(UNKNOWN_UUID) ? Optional.empty() : uuid; if (aci.isEmpty() && pni.isEmpty() && number.isEmpty() && username.isEmpty()) {
if (uuid.isEmpty() && number.isEmpty() && username.isEmpty()) { throw new AssertionError("Must have either a ACI, PNI, username or E164 number!");
throw new AssertionError("Must have either a UUID or E164 number!");
} }
} }
public RecipientAddress(UUID uuid, String e164) { public RecipientAddress(String e164) {
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.empty()); this(null, null, e164, null);
}
public RecipientAddress(UUID uuid, String e164, String username) {
this(Optional.ofNullable(uuid), Optional.ofNullable(e164), Optional.ofNullable(username));
}
public RecipientAddress(SignalServiceAddress address) {
this(Optional.of(address.getServiceId().getRawUuid()), address.getNumber(), Optional.empty());
} }
public RecipientAddress(UUID uuid) { public RecipientAddress(UUID uuid) {
this(Optional.of(uuid), Optional.empty(), Optional.empty()); this(uuid.toString(), null, null, null);
}
public RecipientAddress(String aci, String pni, String e164, String username) {
this(Optional.ofNullable(aci),
Optional.ofNullable(pni),
Optional.ofNullable(e164),
Optional.ofNullable(username));
}
public Optional<UUID> uuid() {
return aci.map(UUID::fromString);
} }
public String getIdentifier() { public String getIdentifier() {
if (uuid.isPresent()) { if (aci.isPresent()) {
return uuid.get().toString(); return aci.get();
} else if (number.isPresent()) { } else if (number.isPresent()) {
return number.get(); return number.get();
} else if (pni.isPresent()) {
return pni.get();
} else if (username.isPresent()) { } else if (username.isPresent()) {
return username.get(); return username.get();
} else { } else {
@ -54,17 +60,16 @@ public record RecipientAddress(Optional<UUID> uuid, Optional<String> number, Opt
public String getLegacyIdentifier() { public String getLegacyIdentifier() {
if (number.isPresent()) { if (number.isPresent()) {
return number.get(); return number.get();
} else if (uuid.isPresent()) {
return uuid.get().toString();
} else if (username.isPresent()) {
return username.get();
} else { } else {
throw new AssertionError("Given the checks in the constructor, this should not be possible."); return getIdentifier();
} }
} }
public boolean matches(RecipientAddress other) { public boolean matches(RecipientAddress other) {
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) return (aci.isPresent() && other.aci.isPresent() && aci.get().equals(other.aci.get()))
|| (
pni.isPresent() && other.pni.isPresent() && pni.get().equals(other.pni.get())
)
|| (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get())) || (number.isPresent() && other.number.isPresent() && number.get().equals(other.number.get()))
|| (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get())); || (username.isPresent() && other.username.isPresent() && username.get().equals(other.username.get()));
} }

View File

@ -1,9 +1,9 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.PhoneNumberFormatter;
import org.signal.core.util.UuidUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID; import java.util.UUID;
@ -24,11 +24,18 @@ public sealed interface RecipientIdentifier {
sealed interface Single extends RecipientIdentifier { sealed interface Single extends RecipientIdentifier {
static Single fromString(String identifier, String localNumber) throws InvalidNumberException { static Single fromString(String identifier, String localNumber) throws InvalidNumberException {
try { if (UuidUtil.INSTANCE.isUuid(identifier)) {
if (UuidUtil.isUuid(identifier)) {
return new Uuid(UUID.fromString(identifier)); return new Uuid(UUID.fromString(identifier));
} }
if (identifier.startsWith("PNI:")) {
final var pni = identifier.substring(4);
if (!UuidUtil.INSTANCE.isUuid(pni)) {
throw new InvalidNumberException("Invalid PNI");
}
return new Pni(UUID.fromString(pni));
}
if (identifier.startsWith("u:")) { if (identifier.startsWith("u:")) {
return new Username(identifier.substring(2)); return new Username(identifier.substring(2));
} }
@ -39,16 +46,15 @@ public sealed interface RecipientIdentifier {
logger.debug("Normalized number {} to {}.", identifier, normalizedNumber); logger.debug("Normalized number {} to {}.", identifier, normalizedNumber);
} }
return new Number(normalizedNumber); return new Number(normalizedNumber);
} catch (org.whispersystems.signalservice.api.util.InvalidNumberException e) {
throw new InvalidNumberException(e.getMessage(), e);
}
} }
static Single fromAddress(RecipientAddress address) { static Single fromAddress(RecipientAddress address) {
if (address.number().isPresent()) { if (address.number().isPresent()) {
return new Number(address.number().get()); return new Number(address.number().get());
} else if (address.uuid().isPresent()) { } else if (address.aci().isPresent()) {
return new Uuid(address.uuid().get()); return new Uuid(UUID.fromString(address.aci().get()));
} else if (address.pni().isPresent()) {
return new Pni(UUID.fromString(address.pni().get().substring(4)));
} else if (address.username().isPresent()) { } else if (address.username().isPresent()) {
return new Username(address.username().get()); return new Username(address.username().get());
} }
@ -71,6 +77,19 @@ public sealed interface RecipientIdentifier {
} }
} }
record Pni(UUID pni) implements Single {
@Override
public String getIdentifier() {
return "PNI:" + pni.toString();
}
@Override
public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, getIdentifier(), null, null);
}
}
record Number(String number) implements Single { record Number(String number) implements Single {
@Override @Override
@ -80,7 +99,7 @@ public sealed interface RecipientIdentifier {
@Override @Override
public RecipientAddress toPartialRecipientAddress() { public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, number); return new RecipientAddress(number);
} }
} }
@ -93,7 +112,7 @@ public sealed interface RecipientIdentifier {
@Override @Override
public RecipientAddress toPartialRecipientAddress() { public RecipientAddress toPartialRecipientAddress() {
return new RecipientAddress(null, null, username); return new RecipientAddress(null, null, null, username);
} }
} }

View File

@ -9,13 +9,13 @@ public record SendMessageResult(
boolean isNetworkFailure, boolean isNetworkFailure,
boolean isUnregisteredFailure, boolean isUnregisteredFailure,
boolean isIdentityFailure, boolean isIdentityFailure,
boolean isRateLimitFailure, RateLimitException rateLimitException,
ProofRequiredException proofRequiredFailure, ProofRequiredException proofRequiredFailure,
boolean isInvalidPreKeyFailure boolean isInvalidPreKeyFailure
) { ) {
public static SendMessageResult unregisteredFailure(RecipientAddress address) { public static SendMessageResult unregisteredFailure(RecipientAddress address) {
return new SendMessageResult(address, false, false, true, false, false, null, false); return new SendMessageResult(address, false, false, true, false, null, null, false);
} }
public static SendMessageResult from( public static SendMessageResult from(
@ -23,16 +23,30 @@ public record SendMessageResult(
RecipientResolver recipientResolver, RecipientResolver recipientResolver,
RecipientAddressResolver addressResolver RecipientAddressResolver addressResolver
) { ) {
final var rateLimitFailure = sendMessageResult.getRateLimitFailure();
final var proofRequiredFailure = sendMessageResult.getProofRequiredFailure();
return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient( return new SendMessageResult(addressResolver.resolveRecipientAddress(recipientResolver.resolveRecipient(
sendMessageResult.getAddress())).toApiRecipientAddress(), sendMessageResult.getAddress())).toApiRecipientAddress(),
sendMessageResult.isSuccess(), sendMessageResult.isSuccess(),
sendMessageResult.isNetworkFailure(), sendMessageResult.isNetworkFailure(),
sendMessageResult.isUnregisteredFailure(), sendMessageResult.isUnregisteredFailure(),
sendMessageResult.getIdentityFailure() != null, sendMessageResult.getIdentityFailure() != null,
sendMessageResult.getRateLimitFailure() != null || sendMessageResult.getProofRequiredFailure() != null, rateLimitFailure == null ? null : RateLimitException.from(rateLimitFailure),
sendMessageResult.getProofRequiredFailure() == null proofRequiredFailure == null ? null : ProofRequiredException.from(proofRequiredFailure),
? null
: new ProofRequiredException(sendMessageResult.getProofRequiredFailure()),
sendMessageResult.isInvalidPreKeyFailure()); sendMessageResult.isInvalidPreKeyFailure());
} }
public boolean isRateLimitFailure() {
return this.rateLimitException != null || this.proofRequiredFailure != null;
}
public Long rateLimitRetryAfterMilliseconds() {
if (proofRequiredFailure != null) {
return proofRequiredFailure.getRetryAfterMilliseconds();
} else if (rateLimitException != null) {
return rateLimitException.getRetryAfterMilliseconds();
} else {
return null;
}
}
} }

View File

@ -2,6 +2,7 @@ package org.asamk.signal.manager.api;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) { public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results) {
@ -19,4 +20,25 @@ public record SendMessageResults(long timestamp, Map<RecipientIdentifier, List<S
.allMatch(identityFailure -> identityFailure) .allMatch(identityFailure -> identityFailure)
&& results.values().stream().mapToInt(List::size).sum() > 0; && results.values().stream().mapToInt(List::size).sum() > 0;
} }
public boolean hasOnlyRateLimitFailure() {
return results.values()
.stream()
.flatMap(res -> res.stream().map(SendMessageResult::isRateLimitFailure))
.allMatch(r -> r) && results.values().stream().mapToInt(List::size).sum() > 0;
}
/**
* Longest rate-limit retry-after window across all rate-limited recipients, in milliseconds.
* Null when no recipient reported one (server omitted Retry-After, or no rate-limit failures).
*/
public Long maxRateLimitRetryAfterMilliseconds() {
return results.values()
.stream()
.flatMap(List::stream)
.map(SendMessageResult::rateLimitRetryAfterMilliseconds)
.filter(Objects::nonNull)
.max(Long::compareTo)
.orElse(null);
}
} }

View File

@ -1,5 +1,7 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.signal.core.util.Hex;
import java.util.Arrays; import java.util.Arrays;
public class StickerPackId { public class StickerPackId {
@ -32,4 +34,9 @@ public class StickerPackId {
public int hashCode() { public int hashCode() {
return Arrays.hashCode(id); return Arrays.hashCode(id);
} }
@Override
public String toString() {
return "StickerPackId{" + Hex.toStringCondensed(id) + '}';
}
} }

View File

@ -1,7 +1,7 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.whispersystems.signalservice.internal.util.Hex; import org.signal.core.util.Hex;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -11,10 +11,7 @@ import java.nio.charset.StandardCharsets;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty; import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public final class StickerPackUrl { public record StickerPackUrl(StickerPackId packId, byte[] packKey) {
private final StickerPackId packId;
private final byte[] packKey;
/** /**
* @throws InvalidStickerPackLinkException If url cannot be parsed. * @throws InvalidStickerPackLinkException If url cannot be parsed.
@ -48,11 +45,6 @@ public final class StickerPackUrl {
return new StickerPackUrl(packId, packKey); return new StickerPackUrl(packId, packKey);
} }
public StickerPackUrl(final StickerPackId packId, final byte[] packKey) {
this.packId = packId;
this.packKey = packKey;
}
public URI getUrl() { public URI getUrl() {
try { try {
return new URI("https", return new URI("https",
@ -67,15 +59,7 @@ public final class StickerPackUrl {
} }
} }
public StickerPackId getPackId() { public static final class InvalidStickerPackLinkException extends Exception {
return packId;
}
public byte[] getPackKey() {
return packKey;
}
public final static class InvalidStickerPackLinkException extends Exception {
public InvalidStickerPackLinkException(String message) { public InvalidStickerPackLinkException(String message) {
super(message); super(message);

View File

@ -1,7 +1,6 @@
package org.asamk.signal.manager.api; package org.asamk.signal.manager.api;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
public enum TrustLevel { public enum TrustLevel {
UNTRUSTED, UNTRUSTED,
@ -17,14 +16,6 @@ public enum TrustLevel {
return TrustLevel.cachedValues[i]; return TrustLevel.cachedValues[i];
} }
public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) {
return switch (identityState) {
case DEFAULT -> TRUSTED_UNVERIFIED;
case UNVERIFIED -> UNTRUSTED;
case VERIFIED -> TRUSTED_VERIFIED;
};
}
public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) { public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) {
return switch (verifiedState) { return switch (verifiedState) {
case DEFAULT -> TRUSTED_UNVERIFIED; case DEFAULT -> TRUSTED_UNVERIFIED;

View File

@ -0,0 +1,7 @@
package org.asamk.signal.manager.api;
import java.util.List;
public record TurnServer(
String username, String password, List<String> urls
) {}

View File

@ -19,6 +19,8 @@ public class UpdateGroup {
private final String avatarFile; private final String avatarFile;
private final Integer expirationTimer; private final Integer expirationTimer;
private final Boolean isAnnouncementGroup; private final Boolean isAnnouncementGroup;
private final String labelEmoji;
private final String labelString;
private UpdateGroup(final Builder builder) { private UpdateGroup(final Builder builder) {
name = builder.name; name = builder.name;
@ -36,6 +38,8 @@ public class UpdateGroup {
avatarFile = builder.avatarFile; avatarFile = builder.avatarFile;
expirationTimer = builder.expirationTimer; expirationTimer = builder.expirationTimer;
isAnnouncementGroup = builder.isAnnouncementGroup; isAnnouncementGroup = builder.isAnnouncementGroup;
labelEmoji = builder.labelEmoji;
labelString = builder.labelString;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -57,7 +61,9 @@ public class UpdateGroup {
copy.editDetailsPermission, copy.editDetailsPermission,
copy.avatarFile, copy.avatarFile,
copy.expirationTimer, copy.expirationTimer,
copy.isAnnouncementGroup); copy.isAnnouncementGroup,
copy.labelEmoji,
copy.labelString);
} }
public static Builder newBuilder( public static Builder newBuilder(
@ -75,7 +81,9 @@ public class UpdateGroup {
final GroupPermission editDetailsPermission, final GroupPermission editDetailsPermission,
final String avatarFile, final String avatarFile,
final Integer expirationTimer, final Integer expirationTimer,
final Boolean isAnnouncementGroup final Boolean isAnnouncementGroup,
final String labelEmoji,
final String labelString
) { ) {
return new Builder(name, return new Builder(name,
description, description,
@ -91,7 +99,9 @@ public class UpdateGroup {
editDetailsPermission, editDetailsPermission,
avatarFile, avatarFile,
expirationTimer, expirationTimer,
isAnnouncementGroup); isAnnouncementGroup,
labelEmoji,
labelString);
} }
public String getName() { public String getName() {
@ -154,6 +164,14 @@ public class UpdateGroup {
return isAnnouncementGroup; return isAnnouncementGroup;
} }
public String getLabelEmoji() {
return labelEmoji;
}
public String getLabelString() {
return labelString;
}
public static final class Builder { public static final class Builder {
private String name; private String name;
@ -171,6 +189,8 @@ public class UpdateGroup {
private String avatarFile; private String avatarFile;
private Integer expirationTimer; private Integer expirationTimer;
private Boolean isAnnouncementGroup; private Boolean isAnnouncementGroup;
private String labelEmoji;
private String labelString;
private Builder() { private Builder() {
} }
@ -190,7 +210,9 @@ public class UpdateGroup {
final GroupPermission editDetailsPermission, final GroupPermission editDetailsPermission,
final String avatarFile, final String avatarFile,
final Integer expirationTimer, final Integer expirationTimer,
final Boolean isAnnouncementGroup final Boolean isAnnouncementGroup,
final String labelEmoji,
final String labelString
) { ) {
this.name = name; this.name = name;
this.description = description; this.description = description;
@ -207,6 +229,8 @@ public class UpdateGroup {
this.avatarFile = avatarFile; this.avatarFile = avatarFile;
this.expirationTimer = expirationTimer; this.expirationTimer = expirationTimer;
this.isAnnouncementGroup = isAnnouncementGroup; this.isAnnouncementGroup = isAnnouncementGroup;
this.labelEmoji = labelEmoji;
this.labelString = labelString;
} }
public Builder withName(final String val) { public Builder withName(final String val) {
@ -284,6 +308,16 @@ public class UpdateGroup {
return this; return this;
} }
public Builder withLabelEmoji(final String val) {
labelEmoji = val;
return this;
}
public Builder withLabelString(final String val) {
labelString = val;
return this;
}
public UpdateGroup build() { public UpdateGroup build() {
return new UpdateGroup(this); return new UpdateGroup(this);
} }

View File

@ -0,0 +1,82 @@
package org.asamk.signal.manager.api;
import org.signal.core.util.Base64;
import org.signal.core.util.UuidUtil;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import java.io.IOException;
import java.util.Arrays;
import java.util.regex.Pattern;
public final class UsernameLinkUrl {
private static final Pattern URL_REGEX = Pattern.compile("(https://)?signal.me/?#eu/([a-zA-Z0-9+\\-_/]+)");
private static final String BASE_URL = "https://signal.me/#eu/";
private final String url;
private final UsernameLinkComponents usernameLinkComponents;
public static UsernameLinkUrl fromUri(String url) throws InvalidUsernameLinkException {
final var matcher = URL_REGEX.matcher(url);
if (!matcher.matches()) {
throw new InvalidUsernameLinkException("Invalid username link");
}
final var path = matcher.group(2);
final byte[] allBytes;
try {
allBytes = Base64.decode(path);
} catch (IOException e) {
throw new InvalidUsernameLinkException("Invalid base64 encoding");
}
if (allBytes.length != 48) {
throw new InvalidUsernameLinkException("Invalid username link");
}
final var entropy = Arrays.copyOfRange(allBytes, 0, 32);
final var serverId = Arrays.copyOfRange(allBytes, 32, allBytes.length);
final var serverIdUuid = UuidUtil.INSTANCE.parseOrNull(serverId);
if (serverIdUuid == null) {
throw new InvalidUsernameLinkException("Invalid serverId");
}
return new UsernameLinkUrl(new UsernameLinkComponents(entropy, serverIdUuid));
}
public UsernameLinkUrl(UsernameLinkComponents usernameLinkComponents) {
this.usernameLinkComponents = usernameLinkComponents;
this.url = createUrl(usernameLinkComponents);
}
private static String createUrl(UsernameLinkComponents usernameLinkComponents) {
final var entropy = usernameLinkComponents.getEntropy();
final var serverId = UuidUtil.toByteArray(usernameLinkComponents.getServerId());
final var combined = new byte[entropy.length + serverId.length];
System.arraycopy(entropy, 0, combined, 0, entropy.length);
System.arraycopy(serverId, 0, combined, entropy.length, serverId.length);
final var base64 = Base64.encodeUrlSafeWithoutPadding(combined);
return BASE_URL + base64;
}
public String getUrl() {
return url;
}
public UsernameLinkComponents getComponents() {
return usernameLinkComponents;
}
public static final class InvalidUsernameLinkException extends Exception {
public InvalidUsernameLinkException(String message) {
super(message);
}
public InvalidUsernameLinkException(Throwable cause) {
super(cause);
}
}
}

View File

@ -0,0 +1,5 @@
package org.asamk.signal.manager.api;
import java.util.UUID;
public record UsernameStatus(String username, UUID uuid, boolean unrestrictedUnidentifiedAccess) {}

View File

@ -0,0 +1,8 @@
package org.asamk.signal.manager.api;
public class VerificationMethodNotAvailableException extends Exception {
public VerificationMethodNotAvailableException() {
super("Invalid verification method");
}
}

View File

@ -1,18 +0,0 @@
package org.asamk.signal.manager.config;
import org.whispersystems.signalservice.api.push.TrustStore;
import java.io.InputStream;
class IasTrustStore implements TrustStore {
@Override
public InputStream getKeyStoreInputStream() {
return IasTrustStore.class.getResourceAsStream("ias.store");
}
@Override
public String getKeyStorePassword() {
return "whisper";
}
}

View File

@ -1,26 +1,3 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
public class KeyBackupConfig { public record KeyBackupConfig(String enclaveName, byte[] serviceId, String mrenclave) {}
private final String enclaveName;
private final byte[] serviceId;
private final String mrenclave;
public KeyBackupConfig(final String enclaveName, final byte[] serviceId, final String mrenclave) {
this.enclaveName = enclaveName;
this.serviceId = serviceId;
this.mrenclave = mrenclave;
}
public String getEnclaveName() {
return enclaveName;
}
public byte[] getServiceId() {
return serviceId;
}
public String getMrenclave() {
return mrenclave;
}
}

View File

@ -1,21 +1,19 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.bouncycastle.util.encoders.Hex; import org.signal.libsignal.net.Network.Environment;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore; import org.signal.network.config.HttpProxy;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.signal.network.config.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.signal.network.config.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.signal.network.config.SignalProxy;
import org.whispersystems.signalservice.internal.configuration.SignalProxy; import org.signal.network.config.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.signal.network.config.SignalServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.signal.network.config.SignalStorageUrl;
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; import org.signal.network.config.SignalSvr2Url;
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url; import org.signal.network.config.TrustStore;
import java.util.Base64; import java.util.Base64;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -23,39 +21,41 @@ import java.util.Optional;
import okhttp3.Dns; import okhttp3.Dns;
import okhttp3.Interceptor; import okhttp3.Interceptor;
import static org.asamk.signal.manager.api.ServiceEnvironment.LIVE;
class LiveConfig { class LiveConfig {
private final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"); .decode("BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF");
private final static String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
private final static String SVR2_MRENCLAVE = "6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094"; .decode("BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "ced8217b26228e4b210c985786999d095c4958a94faf37b14acaf25c4cbb02a4";
private static final String SVR2_MRENCLAVE_LEGACY = "1240acbd4aa26974184844c8a46b1022d3957ac8a76c1fd8f5b1a15141ee0708";
private final static String KEY_BACKUP_ENCLAVE_NAME = "e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5"; private static final String URL = "https://chat.signal.org";
private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode( private static final String CDN_URL = "https://cdn.signal.org";
"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89"); private static final String CDN2_URL = "https://cdn2.signal.org";
private final static String KEY_BACKUP_MRENCLAVE = "45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c"; private static final String CDN3_URL = "https://cdn3.signal.org";
private final static String FALLBACK_KEY_BACKUP_ENCLAVE_NAME = "0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f"; private static final String STORAGE_URL = "https://storage.signal.org";
private final static byte[] FALLBACK_KEY_BACKUP_SERVICE_ID = Hex.decode( private static final String SIGNAL_CDSI_URL = "https://cdsi.signal.org";
"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39"); private static final String SIGNAL_SVR2_URL = "https://svr2.signal.org";
private final static String FALLBACK_KEY_BACKUP_MRENCLAVE = "ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba"; private static final TrustStore TRUST_STORE = new WhisperTrustStore();
private final static String URL = "https://chat.signal.org"; private static final Optional<Dns> dns = Optional.empty();
private final static String CDN_URL = "https://cdn.signal.org"; private static final Optional<SignalProxy> proxy = Optional.empty();
private final static String CDN2_URL = "https://cdn2.signal.org"; private static final Optional<HttpProxy> systemProxy = Optional.empty();
private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org";
private final static String STORAGE_URL = "https://storage.signal.org";
private final static String SIGNAL_CDSI_URL = "https://cdsi.signal.org";
private final static String SIGNAL_SVR2_URL = "https://svr2.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private final static Optional<Dns> dns = Optional.empty(); private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
private final static Optional<SignalProxy> proxy = Optional.empty(); .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==");
private static final byte[] genericServerPublicParams = Base64.getDecoder()
private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P");
private final static byte[] genericServerPublicParams = Base64.getDecoder()
.decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN"); .decode("AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN");
private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O");
private static final Environment LIBSIGNAL_NET_ENV = Environment.PRODUCTION;
static SignalServiceConfiguration createDefaultServiceConfiguration( static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors final List<Interceptor> interceptors
) { ) {
@ -63,42 +63,38 @@ class LiveConfig {
Map.of(0, Map.of(0,
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
2, 2,
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)},
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, 3,
new SignalCdnUrl[]{new SignalCdnUrl(CDN3_URL, TRUST_STORE)}),
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)}, new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)}, new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
interceptors, interceptors,
dns, dns,
proxy, proxy,
systemProxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams); genericServerPublicParams,
backupServerPublicParams,
false);
} }
static ECPublicKey getUnidentifiedSenderTrustRoot() { static List<ECPublicKey> getUnidentifiedSenderTrustRoots() {
try { try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); return List.of(new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT),
new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT2));
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
static KeyBackupConfig createKeyBackupConfig() { static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) {
return new KeyBackupConfig(KEY_BACKUP_ENCLAVE_NAME, KEY_BACKUP_SERVICE_ID, KEY_BACKUP_MRENCLAVE); return new ServiceEnvironmentConfig(LIVE,
} LIBSIGNAL_NET_ENV,
createDefaultServiceConfiguration(interceptors),
static Collection<KeyBackupConfig> createFallbackKeyBackupConfigs() { getUnidentifiedSenderTrustRoots(),
return List.of(new KeyBackupConfig(FALLBACK_KEY_BACKUP_ENCLAVE_NAME, CDSI_MRENCLAVE,
FALLBACK_KEY_BACKUP_SERVICE_ID, List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
FALLBACK_KEY_BACKUP_MRENCLAVE));
}
static String getCdsiMrenclave() {
return CDSI_MRENCLAVE;
}
static String getSvr2Mrenclave() {
return SVR2_MRENCLAVE;
} }
private LiveConfig() { private LiveConfig() {

View File

@ -3,55 +3,47 @@ package org.asamk.signal.manager.config;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
import org.signal.libsignal.protocol.util.Medium; import org.signal.libsignal.protocol.util.Medium;
import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.push.TrustStore;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.Interceptor; import okhttp3.Interceptor;
public class ServiceConfig { public class ServiceConfig {
public final static int PREKEY_MINIMUM_COUNT = 10; public static final int PREKEY_MINIMUM_COUNT = 10;
public final static int PREKEY_BATCH_SIZE = 100; public static final int PREKEY_BATCH_SIZE = 100;
public final static int PREKEY_MAXIMUM_ID = Medium.MAX_VALUE; public static final int PREKEY_MAXIMUM_ID = Medium.MAX_VALUE;
public final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; public static final long PREKEY_ARCHIVE_AGE = TimeUnit.DAYS.toMillis(30);
public final static long MAX_ENVELOPE_SIZE = 0; public static final long PREKEY_STALE_AGE = TimeUnit.DAYS.toMillis(90);
public final static long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024; public static final long SIGNED_PREKEY_ROTATE_AGE = TimeUnit.DAYS.toMillis(2);
public final static boolean AUTOMATIC_NETWORK_RETRY = true;
public final static int GROUP_MAX_SIZE = 1001;
private final static KeyStore iasKeyStore; public static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
public static final long MAX_ENVELOPE_SIZE = 0;
static { public static final int MAX_INCREMENTAL_MACS_PER_ENVELOPE = 10;
try { public static final int MAX_MESSAGE_SIZE_BYTES = 2000;
TrustStore contactTrustStore = new IasTrustStore(); public static final long AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE = 10 * 1024 * 1024;
public static final boolean AUTOMATIC_NETWORK_RETRY = true;
var keyStore = KeyStore.getInstance("BKS"); public static final int GROUP_MAX_SIZE = 1001;
keyStore.load(contactTrustStore.getKeyStoreInputStream(), public static final int MAXIMUM_ONE_OFF_REQUEST_SIZE = 3;
contactTrustStore.getKeyStorePassword().toCharArray()); public static final long UNREGISTERED_LIFESPAN = TimeUnit.DAYS.toMillis(30);
iasKeyStore = keyStore;
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) { public static AccountAttributes.Capabilities getCapabilities(boolean isPrimaryDevice) {
final var giftBadges = !isPrimaryDevice; final var attachmentBackfill = !isPrimaryDevice;
return new AccountAttributes.Capabilities(false, true, true, true, true, giftBadges, false, false); final var spqr = true;
} final var usernameSyncChangeMessage = !isPrimaryDevice;
final var optionalPhoneNumber = !isPrimaryDevice;
public static KeyStore getIasKeyStore() { return new AccountAttributes.Capabilities(true,
return iasKeyStore; true,
attachmentBackfill,
spqr,
usernameSyncChangeMessage,
optionalPhoneNumber);
} }
public static ServiceEnvironmentConfig getServiceEnvironmentConfig( public static ServiceEnvironmentConfig getServiceEnvironmentConfig(
ServiceEnvironment serviceEnvironment, String userAgent ServiceEnvironment serviceEnvironment,
String userAgent
) { ) {
final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request() final Interceptor userAgentInterceptor = chain -> chain.proceed(chain.request()
.newBuilder() .newBuilder()
@ -61,20 +53,8 @@ public class ServiceConfig {
final var interceptors = List.of(userAgentInterceptor); final var interceptors = List.of(userAgentInterceptor);
return switch (serviceEnvironment) { return switch (serviceEnvironment) {
case LIVE -> new ServiceEnvironmentConfig(serviceEnvironment, case LIVE -> LiveConfig.getServiceEnvironmentConfig(interceptors);
LiveConfig.createDefaultServiceConfiguration(interceptors), case STAGING -> StagingConfig.getServiceEnvironmentConfig(interceptors);
LiveConfig.getUnidentifiedSenderTrustRoot(),
LiveConfig.createKeyBackupConfig(),
LiveConfig.createFallbackKeyBackupConfigs(),
LiveConfig.getCdsiMrenclave(),
LiveConfig.getSvr2Mrenclave());
case STAGING -> new ServiceEnvironmentConfig(serviceEnvironment,
StagingConfig.createDefaultServiceConfiguration(interceptors),
StagingConfig.getUnidentifiedSenderTrustRoot(),
StagingConfig.createKeyBackupConfig(),
StagingConfig.createFallbackKeyBackupConfigs(),
StagingConfig.getCdsiMrenclave(),
StagingConfig.getSvr2Mrenclave());
}; };
} }
} }

View File

@ -1,67 +1,17 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.asamk.signal.manager.api.ServiceEnvironment; import org.asamk.signal.manager.api.ServiceEnvironment;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.signal.network.config.SignalServiceConfiguration;
import java.util.Collection; import java.util.List;
public class ServiceEnvironmentConfig { public record ServiceEnvironmentConfig(
ServiceEnvironment type,
private final ServiceEnvironment type; Network.Environment netEnvironment,
private final SignalServiceConfiguration signalServiceConfiguration; SignalServiceConfiguration signalServiceConfiguration,
List<ECPublicKey> unidentifiedSenderTrustRoots,
private final ECPublicKey unidentifiedSenderTrustRoot; String cdsiMrenclave,
List<String> svr2Mrenclaves
private final KeyBackupConfig keyBackupConfig; ) {}
private final Collection<KeyBackupConfig> fallbackKeyBackupConfigs;
private final String cdsiMrenclave;
private final String svr2Mrenclave;
public ServiceEnvironmentConfig(
final ServiceEnvironment type,
final SignalServiceConfiguration signalServiceConfiguration,
final ECPublicKey unidentifiedSenderTrustRoot,
final KeyBackupConfig keyBackupConfig,
final Collection<KeyBackupConfig> fallbackKeyBackupConfigs,
final String cdsiMrenclave,
final String svr2Mrenclave
) {
this.type = type;
this.signalServiceConfiguration = signalServiceConfiguration;
this.unidentifiedSenderTrustRoot = unidentifiedSenderTrustRoot;
this.keyBackupConfig = keyBackupConfig;
this.fallbackKeyBackupConfigs = fallbackKeyBackupConfigs;
this.cdsiMrenclave = cdsiMrenclave;
this.svr2Mrenclave = svr2Mrenclave;
}
public ServiceEnvironment getType() {
return type;
}
public SignalServiceConfiguration getSignalServiceConfiguration() {
return signalServiceConfiguration;
}
public ECPublicKey getUnidentifiedSenderTrustRoot() {
return unidentifiedSenderTrustRoot;
}
public KeyBackupConfig getKeyBackupConfig() {
return keyBackupConfig;
}
public Collection<KeyBackupConfig> getFallbackKeyBackupConfigs() {
return fallbackKeyBackupConfigs;
}
public String getCdsiMrenclave() {
return cdsiMrenclave;
}
public String getSvr2Mrenclave() {
return svr2Mrenclave;
}
}

View File

@ -1,21 +1,19 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.bouncycastle.util.encoders.Hex; import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.push.TrustStore; import org.signal.network.config.HttpProxy;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.signal.network.config.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl; import org.signal.network.config.SignalCdsiUrl;
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.signal.network.config.SignalProxy;
import org.whispersystems.signalservice.internal.configuration.SignalProxy; import org.signal.network.config.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.signal.network.config.SignalServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.signal.network.config.SignalStorageUrl;
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; import org.signal.network.config.SignalSvr2Url;
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url; import org.signal.network.config.TrustStore;
import java.util.Base64; import java.util.Base64;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -23,39 +21,41 @@ import java.util.Optional;
import okhttp3.Dns; import okhttp3.Dns;
import okhttp3.Interceptor; import okhttp3.Interceptor;
import static org.asamk.signal.manager.api.ServiceEnvironment.STAGING;
class StagingConfig { class StagingConfig {
private final static byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder() private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT = Base64.getDecoder()
.decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx"); .decode("BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx");
private final static String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57"; private static final byte[] UNIDENTIFIED_SENDER_TRUST_ROOT2 = Base64.getDecoder()
private final static String SVR2_MRENCLAVE = "a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95"; .decode("BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm");
private static final String CDSI_MRENCLAVE = "0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57";
private static final String SVR2_MRENCLAVE = "3c699f4975aaa3d172c0aad042f94f031b2b03e10b9c19a45116a01693d83302";
private static final String SVR2_MRENCLAVE_LEGACY = "97f151f6ed078edbbfd72fa9cae694dcc08353f1f5e8d9ccd79a971b10ffc535";
private final static String KEY_BACKUP_ENCLAVE_NAME = "39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b"; private static final String URL = "https://chat.staging.signal.org";
private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode( private static final String CDN_URL = "https://cdn-staging.signal.org";
"ee1d0d972b7ea903615670de43ab1b6e7a825e811c70a29bb5fe0f819e0975fa"); private static final String CDN2_URL = "https://cdn2-staging.signal.org";
private final static String KEY_BACKUP_MRENCLAVE = "45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c"; private static final String CDN3_URL = "https://cdn3-staging.signal.org";
private final static String FALLBACK_KEY_BACKUP_ENCLAVE_NAME = "dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99"; private static final String STORAGE_URL = "https://storage-staging.signal.org";
private final static byte[] FALLBACK_KEY_BACKUP_SERVICE_ID = Hex.decode( private static final String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org";
"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a"); private static final String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org";
private final static String FALLBACK_KEY_BACKUP_MRENCLAVE = "ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba"; private static final TrustStore TRUST_STORE = new WhisperTrustStore();
private final static String URL = "https://chat.staging.signal.org"; private static final Optional<Dns> dns = Optional.empty();
private final static String CDN_URL = "https://cdn-staging.signal.org"; private static final Optional<SignalProxy> proxy = Optional.empty();
private final static String CDN2_URL = "https://cdn2-staging.signal.org"; private static final Optional<HttpProxy> systemProxy = Optional.empty();
private final static String SIGNAL_KEY_BACKUP_URL = "https://api-staging.backup.signal.org";
private final static String STORAGE_URL = "https://storage-staging.signal.org";
private final static String SIGNAL_CDSI_URL = "https://cdsi.staging.signal.org";
private final static String SIGNAL_SVR2_URL = "https://svr2.staging.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private final static Optional<Dns> dns = Optional.empty(); private static final byte[] zkGroupServerPublicParams = Base64.getDecoder()
private final static Optional<SignalProxy> proxy = Optional.empty(); .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==");
private static final byte[] genericServerPublicParams = Base64.getDecoder()
private final static byte[] zkGroupServerPublicParams = Base64.getDecoder()
.decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj");
private final static byte[] genericServerPublicParams = Base64.getDecoder()
.decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N"); .decode("AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N");
private static final byte[] backupServerPublicParams = Base64.getDecoder()
.decode("AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8");
private static final Network.Environment LIBSIGNAL_NET_ENV = Network.Environment.STAGING;
static SignalServiceConfiguration createDefaultServiceConfiguration( static SignalServiceConfiguration createDefaultServiceConfiguration(
final List<Interceptor> interceptors final List<Interceptor> interceptors
) { ) {
@ -63,42 +63,38 @@ class StagingConfig {
Map.of(0, Map.of(0,
new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
2, 2,
new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)}), new SignalCdnUrl[]{new SignalCdnUrl(CDN2_URL, TRUST_STORE)},
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, 3,
new SignalCdnUrl[]{new SignalCdnUrl(CDN3_URL, TRUST_STORE)}),
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)}, new SignalCdsiUrl[]{new SignalCdsiUrl(SIGNAL_CDSI_URL, TRUST_STORE)},
new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)}, new SignalSvr2Url[]{new SignalSvr2Url(SIGNAL_SVR2_URL, TRUST_STORE, null, null)},
interceptors, interceptors,
dns, dns,
proxy, proxy,
systemProxy,
zkGroupServerPublicParams, zkGroupServerPublicParams,
genericServerPublicParams); genericServerPublicParams,
backupServerPublicParams,
false);
} }
static ECPublicKey getUnidentifiedSenderTrustRoot() { static List<ECPublicKey> getUnidentifiedSenderTrustRoots() {
try { try {
return Curve.decodePoint(UNIDENTIFIED_SENDER_TRUST_ROOT, 0); return List.of(new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT),
new ECPublicKey(UNIDENTIFIED_SENDER_TRUST_ROOT2));
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
} }
static KeyBackupConfig createKeyBackupConfig() { static ServiceEnvironmentConfig getServiceEnvironmentConfig(List<Interceptor> interceptors) {
return new KeyBackupConfig(KEY_BACKUP_ENCLAVE_NAME, KEY_BACKUP_SERVICE_ID, KEY_BACKUP_MRENCLAVE); return new ServiceEnvironmentConfig(STAGING,
} LIBSIGNAL_NET_ENV,
createDefaultServiceConfiguration(interceptors),
static Collection<KeyBackupConfig> createFallbackKeyBackupConfigs() { getUnidentifiedSenderTrustRoots(),
return List.of(new KeyBackupConfig(FALLBACK_KEY_BACKUP_ENCLAVE_NAME, CDSI_MRENCLAVE,
FALLBACK_KEY_BACKUP_SERVICE_ID, List.of(SVR2_MRENCLAVE, SVR2_MRENCLAVE_LEGACY));
FALLBACK_KEY_BACKUP_MRENCLAVE));
}
static String getCdsiMrenclave() {
return CDSI_MRENCLAVE;
}
static String getSvr2Mrenclave() {
return SVR2_MRENCLAVE;
} }
private StagingConfig() { private StagingConfig() {

View File

@ -1,6 +1,6 @@
package org.asamk.signal.manager.config; package org.asamk.signal.manager.config;
import org.whispersystems.signalservice.api.push.TrustStore; import org.signal.network.config.TrustStore;
import java.io.InputStream; import java.io.InputStream;

View File

@ -18,7 +18,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
public class GroupUtils { public class GroupUtils {
public static void setGroupContext( public static void setGroupContext(
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo final SignalServiceDataMessage.Builder messageBuilder,
final GroupInfo groupInfo
) { ) {
if (groupInfo instanceof GroupInfoV1) { if (groupInfo instanceof GroupInfoV1) {
var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)

View File

@ -1,6 +1,6 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.signal.core.models.ServiceId.ACI;
public interface AccountFileUpdater { public interface AccountFileUpdater {

View File

@ -3,49 +3,70 @@ package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.DeviceLinkUrl; import org.asamk.signal.manager.api.DeviceLinkUrl;
import org.asamk.signal.manager.api.IncorrectPinException; 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.NonNormalizedPhoneNumberException;
import org.asamk.signal.manager.api.PinLockMissingException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.api.RateLimitException; import org.asamk.signal.manager.api.RateLimitException;
import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.jobs.SyncStorageJob;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.asamk.signal.manager.util.NumberVerificationUtils; import org.asamk.signal.manager.util.NumberVerificationUtils;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.core.models.ServiceId.ACI;
import org.signal.core.models.ServiceId.PNI;
import org.signal.core.util.Base64;
import org.signal.core.util.crypto.DeviceNameCipher;
import org.signal.libsignal.protocol.IdentityKeyPair; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.NoSessionException;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.state.KyberPreKeyRecord; import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord; import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.protocol.util.KeyHelper;
import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username; import org.signal.libsignal.usernames.Username;
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.push.ServiceIdType; 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.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException; import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity; import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.util.Base64UrlSafe; import org.whispersystems.signalservice.internal.push.SyncMessage;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.UUID;
import java.util.concurrent.TimeUnit; 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.asamk.signal.manager.util.Utils.handleResponseExceptionSuspend;
import static org.whispersystems.signalservice.internal.util.Util.isEmpty; import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
public class AccountHelper { public class AccountHelper {
private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class); private static final Logger logger = LoggerFactory.getLogger(AccountHelper.class);
private final Context context; private final Context context;
private final SignalAccount account; private final SignalAccount account;
@ -64,6 +85,42 @@ public class AccountHelper {
} }
public void checkAccountState() throws IOException { public void checkAccountState() throws IOException {
if (account.getAci() == null) {
account.setRegistered(false);
throw new IOException("Account without ACI");
}
try {
updateAccountAttributes();
if (account.getPreviousStorageVersion() < 9) {
context.getPreKeyHelper().forceRefreshPreKeys();
} else {
context.getPreKeyHelper().refreshPreKeysIfNecessary();
}
if (account.getPni() == null) {
checkWhoAmiI();
}
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
throw new IOException("Missing PNI identity key, relinking required");
}
if (account.getPreviousStorageVersion() < 10
&& account.isPrimaryDevice()
&& account.getRegistrationLockPin() != null) {
migrateRegistrationPin();
}
if (account.getUsername() != null && account.getUsernameLink() == null) {
try {
tryToSetUsernameLink(new Username(account.getUsername()));
} catch (BaseUsernameException e) {
logger.debug("Invalid local username");
}
}
} catch (DeprecatedVersionException e) {
logger.debug("Signal-Server returned deprecated version exception", e);
throw e;
} catch (AuthorizationFailedException e) {
account.setRegistered(false);
throw e;
}
if (account.getLastReceiveTimestamp() == 0) { if (account.getLastReceiveTimestamp() == 0) {
logger.info("The Signal protocol expects that incoming messages are regularly received."); logger.info("The Signal protocol expects that incoming messages are regularly received.");
} else { } else {
@ -75,27 +132,6 @@ public class AccountHelper {
days); days);
} }
} }
try {
updateAccountAttributes();
context.getPreKeyHelper().refreshPreKeysIfNecessary();
if (account.getAci() == null || account.getPni() == null) {
checkWhoAmiI();
}
if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
context.getSyncHelper().requestSyncPniIdentity();
}
if (account.getPreviousStorageVersion() < 4
&& account.isPrimaryDevice()
&& account.getRegistrationLockPin() != null) {
migrateRegistrationPin();
}
} catch (DeprecatedVersionException e) {
logger.debug("Signal-Server returned deprecated version exception", e);
throw e;
} catch (AuthorizationFailedException e) {
account.setRegistered(false);
throw e;
}
} }
public void checkWhoAmiI() throws IOException { public void checkWhoAmiI() throws IOException {
@ -118,11 +154,11 @@ public class AccountHelper {
account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair()); account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
} }
account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
// TODO check and update remote storage
context.getUnidentifiedAccessHelper().rotateSenderCertificates(); context.getUnidentifiedAccessHelper().rotateSenderCertificates();
dependencies.resetAfterAddressChange(); dependencies.resetAfterAddressChange();
context.getGroupV2Helper().clearAuthCredentialCache(); context.getGroupV2Helper().clearAuthCredentialCache();
context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci()); context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
context.getJobExecutor().enqueueJob(new SyncStorageJob());
} }
public void setPni( public void setPni(
@ -139,87 +175,290 @@ public class AccountHelper {
} }
public void startChangeNumber( public void startChangeNumber(
String newNumber, String captcha, boolean voiceVerification String newNumber,
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException { boolean voiceVerification,
String captcha
) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword()); final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager, final var registrationApi = accountManager.getRegistrationApi();
String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
account.getSessionId(newNumber), account.getSessionId(newNumber),
id -> account.setSessionId(newNumber, id), id -> account.setSessionId(newNumber, id),
voiceVerification, voiceVerification,
captcha); captcha);
NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification); NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
} }
public void finishChangeNumber( public void finishChangeNumber(
String newNumber, String verificationCode, String pin String newNumber,
) throws IncorrectPinException, PinLockedException, IOException { String verificationCode,
// TODO create new PNI identity key String pin
final List<OutgoingPushMessage> deviceMessages = null; ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null; for (var attempts = 0; attempts < 5; attempts++) {
final Map<String, KyberPreKeyEntity> devicePniLastResortKyberPrekeys = null; try {
final Map<String, Integer> pniRegistrationIds = null; finishChangeNumberInternal(newNumber, verificationCode, pin);
var sessionId = account.getSessionId(account.getNumber()); break;
} catch (MismatchedDevicesException e) {
logger.debug("Change number failed with mismatched devices, retrying.");
try {
dependencies.getMessageSender().handleChangeNumberMismatchDevices(e.getMismatchedDevices());
} catch (UntrustedIdentityException ex) {
throw new AssertionError(ex);
}
}
}
}
private void finishChangeNumberInternal(
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>();
final var devicePniLastResortKyberPreKeys = new HashMap<Integer, KyberPreKeyEntity>();
final var pniRegistrationIds = new HashMap<Integer, Integer>();
final var selfDeviceId = account.getDeviceId();
SyncMessage.PniChangeNumber selfChangeNumber = null;
final var deviceIds = new ArrayList<Integer>();
deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
final var aci = account.getAci();
final var accountDataStore = account.getSignalServiceDataStore().aci();
final var subDeviceSessions = accountDataStore.getSubDeviceSessions(aci.toString())
.stream()
.filter(deviceId -> accountDataStore.containsSession(new SignalProtocolAddress(aci.toString(),
deviceId)))
.toList();
deviceIds.addAll(subDeviceSessions);
final var messageSender = dependencies.getMessageSender();
for (final var deviceId : deviceIds) {
// Signed Prekey
final SignedPreKeyRecord signedPreKeyRecord;
try {
signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
pniIdentity.getPrivateKey());
final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
signedPreKeyRecord.getKeyPair().getPublicKey(),
signedPreKeyRecord.getSignature());
devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
} catch (InvalidKeyException e) {
throw new AssertionError("unexpected invalid key", e);
}
// Last-resort kyber prekey
final KyberPreKeyRecord lastResortKyberPreKeyRecord;
try {
lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
pniIdentity.getPrivateKey());
final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
lastResortKyberPreKeyRecord.getSignature());
devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
} catch (InvalidKeyException e) {
throw new AssertionError("unexpected invalid key", e);
}
// Registration Id
var pniRegistrationId = -1;
while (pniRegistrationId < 0 || pniRegistrationIds.containsValue(pniRegistrationId)) {
pniRegistrationId = KeyHelper.generateRegistrationId(false);
}
pniRegistrationIds.put(deviceId, pniRegistrationId);
// Device Message
final var pniChangeNumber = new SyncMessage.PniChangeNumber.Builder().identityKeyPair(ByteString.of(
pniIdentity.serialize()))
.signedPreKey(ByteString.of(signedPreKeyRecord.serialize()))
.lastResortKyberPreKey(ByteString.of(lastResortKyberPreKeyRecord.serialize()))
.registrationId(pniRegistrationId)
.newE164(newNumber)
.build();
if (deviceId == selfDeviceId) {
selfChangeNumber = pniChangeNumber;
} else {
try {
final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
pniChangeNumber);
encryptedDeviceMessages.add(message);
} catch (UntrustedIdentityException | IOException | InvalidKeyException | NoSessionException e) {
throw new RuntimeException(e);
}
}
}
final var sessionId = account.getSessionId(newNumber);
if (sessionId == null) {
throw new IOException("No change number verification session active");
}
final var result = NumberVerificationUtils.verifyNumber(sessionId, final var result = NumberVerificationUtils.verifyNumber(sessionId,
verificationCode, verificationCode,
pin, pin,
context.getPinHelper(), context.getPinHelper(),
(sessionId1, verificationCode1, registrationLock) -> { (sessionId1, verificationCode1, registrationLock) -> {
final var accountManager = dependencies.getAccountManager(); final var registrationApi = dependencies.getRegistrationApi();
final var accountApi = dependencies.getAccountApi();
try { try {
Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1)); handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
} catch (AlreadyVerifiedException e) { } catch (AlreadyVerifiedException e) {
// Already verified so can continue changing number // Already verified so can continue changing number
} }
return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest( return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
sessionId1,
null, null,
newNumber, newNumber,
registrationLock, registrationLock,
account.getPniIdentityKeyPair().getPublicKey(), pniIdentity.getPublicKey(),
deviceMessages, encryptedDeviceMessages,
devicePniSignedPreKeys, Utils.mapKeys(devicePniSignedPreKeys, Object::toString),
devicePniLastResortKyberPrekeys, Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString),
pniRegistrationIds))); Utils.mapKeys(pniRegistrationIds, Object::toString))));
}); });
// TODO handle response
updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni())); account.clearSessionId();
final var updatePni = PNI.parseOrThrow(result.first().getPni());
if (updatePni.equals(account.getPni())) {
logger.debug("PNI is unchanged after change number");
return;
}
handlePniChangeNumberMessage(selfChangeNumber, updatePni, false);
}
public boolean handlePniChangeNumberMessage(
final SyncMessage.PniChangeNumber pniChangeNumber,
final PNI updatedPni,
final boolean forcePniPreKeyRotation
) {
if (pniChangeNumber.identityKeyPair != null
&& pniChangeNumber.registrationId != null
&& pniChangeNumber.signedPreKey != null) {
logger.debug("New PNI: {}", updatedPni);
try {
setPni(updatedPni,
new IdentityKeyPair(pniChangeNumber.identityKeyPair.toByteArray()),
pniChangeNumber.newE164,
pniChangeNumber.registrationId,
new SignedPreKeyRecord(pniChangeNumber.signedPreKey.toByteArray()),
pniChangeNumber.lastResortKyberPreKey != null
? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
: null);
if (forcePniPreKeyRotation) {
try {
context.getPreKeyHelper().forceRefreshPreKeys(ServiceIdType.PNI);
} catch (IOException e) {
logger.warn("Failed to force refresh PNI pre keys after PNI change sync", e);
}
}
return true;
} catch (Exception e) {
logger.warn("Failed to handle change number message", e);
}
}
return false;
} }
public static final int USERNAME_MIN_LENGTH = 3; public static final int USERNAME_MIN_LENGTH = 3;
public static final int USERNAME_MAX_LENGTH = 32; public static final int USERNAME_MAX_LENGTH = 32;
public String reserveUsername(String nickname) throws IOException, BaseUsernameException { public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException {
final var currentUsername = account.getUsername(); final var currentUsername = account.getUsername();
if (currentUsername != null) { if (currentUsername != null) {
final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.')); final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
if (currentNickname.equals(nickname)) { if (currentNickname.equals(nickname)) {
try {
refreshCurrentUsername(); refreshCurrentUsername();
return currentUsername; return;
} catch (IOException | BaseUsernameException e) {
logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
}
} }
} }
final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH); final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
final var candidateHashes = new ArrayList<String>(); reserveUsername(candidates);
for (final var candidate : candidates) {
candidateHashes.add(Base64UrlSafe.encodeBytesWithoutPadding(candidate.getHash()));
} }
final var response = dependencies.getAccountManager().reserveUsername(candidateHashes); public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
final var hashIndex = candidateHashes.indexOf(response.getUsernameHash()); final var currentUsername = account.getUsername();
if (currentUsername != null) {
if (currentUsername.equals(username)) {
try {
refreshCurrentUsername();
return;
} catch (IOException | BaseUsernameException e) {
logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
}
}
}
final var candidates = List.of(new Username(username));
reserveUsername(candidates);
}
private void reserveUsername(final List<Username> candidates) throws IOException {
final var candidateHashes = new ArrayList<byte[]>();
for (final var candidate : candidates) {
candidateHashes.add(candidate.getHash());
}
final var usernameHash = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
final var hashIndex = candidateHashes.stream()
.filter(candidateHash -> Arrays.equals(candidateHash, usernameHash))
.findFirst()
.map(candidateHashes::indexOf)
.orElse(-1);
if (hashIndex == -1) { if (hashIndex == -1) {
logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes."); logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
throw new IOException("Unexpected username response"); throw new IOException("Unexpected username response");
} }
logger.debug("[reserveUsername] Successfully reserved username."); logger.debug("[reserveUsername] Successfully reserved username.");
final var username = candidates.get(hashIndex).getUsername(); final var username = candidates.get(hashIndex);
dependencies.getAccountManager().confirmUsername(username, response); final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsername(username); account.setUsername(username.getUsername());
account.setUsernameLink(linkComponents);
account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress()); account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
account.getRecipientStore().rotateSelfStorageId();
logger.debug("[confirmUsername] Successfully confirmed username."); logger.debug("[confirmUsername] Successfully confirmed username.");
}
return 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 { public void refreshCurrentUsername() throws IOException, BaseUsernameException {
@ -231,7 +470,8 @@ public class AccountHelper {
final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI(); final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
final var serverUsernameHash = whoAmIResponse.getUsernameHash(); final var serverUsernameHash = whoAmIResponse.getUsernameHash();
final var hasServerUsername = !isEmpty(serverUsernameHash); final var hasServerUsername = !isEmpty(serverUsernameHash);
final var localUsernameHash = Base64UrlSafe.encodeBytesWithoutPadding(new Username(localUsername).getHash()); final var username = new Username(localUsername);
final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash());
if (!hasServerUsername) { if (!hasServerUsername) {
logger.debug("No remote username is set."); logger.debug("No remote username is set.");
@ -243,55 +483,122 @@ public class AccountHelper {
if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) { if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
logger.debug("Attempting to resynchronize username."); logger.debug("Attempting to resynchronize username.");
tryReserveConfirmUsername(localUsername, localUsernameHash); try {
tryReserveConfirmUsername(username);
} catch (NonSuccessfulResponseCodeException e) {
logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
e.getMessage(),
e.getClass().getSimpleName());
account.setUsername(null);
account.setUsernameLink(null);
account.getRecipientStore().rotateSelfStorageId();
throw e;
}
} else { } else {
logger.debug("Username already set, not refreshing."); logger.debug("Username already set, not refreshing.");
} }
} }
private void tryReserveConfirmUsername(final String username, String localUsernameHash) throws IOException { private void tryReserveConfirmUsername(final Username username) throws IOException {
final var response = dependencies.getAccountManager().reserveUsername(List.of(localUsernameHash)); final var usernameLink = account.getUsernameLink();
if (usernameLink == null) {
handleResponseException(dependencies.getAccountApi().reserveUsername(List.of(username.getHash())));
logger.debug("[reserveUsername] Successfully reserved existing username."); logger.debug("[reserveUsername] Successfully reserved existing username.");
dependencies.getAccountManager().confirmUsername(username, response); final var linkComponents = confirmUsernameAndCreateNewLink(username);
account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully confirmed existing username."); logger.debug("[confirmUsername] Successfully confirmed existing username.");
} else {
final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
account.setUsernameLink(linkComponents);
logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
}
account.getRecipientStore().rotateSelfStorageId();
} }
private void tryToSetUsernameLink(Username username) {
for (var i = 1; i < 4; i++) {
try {
final var linkComponents = createUsernameLink(username);
account.setUsernameLink(linkComponents);
break;
} catch (IOException e) {
logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e);
}
}
}
@SuppressWarnings("unchecked")
public void deleteUsername() throws IOException { public void deleteUsername() throws IOException {
dependencies.getAccountManager().deleteUsername(); handleResponseException(dependencies.getAccountApi().deleteUsernameHash());
account.setUsernameLink(null);
account.setUsername(null); account.setUsername(null);
logger.debug("[deleteUsername] Successfully deleted the username."); logger.debug("[deleteUsername] Successfully deleted the username.");
} }
public void setDeviceName(String deviceName) { public void setDeviceName(String deviceName) {
final var privateKey = account.getAciIdentityKeyPair().getPrivateKey(); final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); account.setEncryptedDeviceName(Base64.encodeWithoutPadding(encryptedDeviceName));
account.setEncryptedDeviceName(encryptedDeviceName); }
public void setDeviceName(int deviceId, String deviceName) throws IOException {
final var encryptedDeviceName = getEncryptedDeviceName(deviceName);
handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
.setDeviceName(encryptedDeviceName, deviceId, cont));
context.getSyncHelper().sendDeviceNameChange(deviceId);
}
private byte[] getEncryptedDeviceName(final String deviceName) {
final var identityKey = account.getAciIdentityKeyPair();
return DeviceNameCipher.encryptDeviceName(deviceName.getBytes(StandardCharsets.UTF_8), identityKey);
}
public void refreshDeviceName() throws IOException {
final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
.getDevices(cont));
final var deviceId = account.getDeviceId();
final var device = devices.stream().filter(d -> d.id == deviceId).findFirst();
if (device.isPresent()) {
account.setEncryptedDeviceName(device.get().name);
}
} }
public void updateAccountAttributes() throws IOException { 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 { public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); final var linkDeviceApi = dependencies.getLinkDeviceApi();
final LinkedDeviceVerificationCodeResponse verificationCode;
try { try {
dependencies.getAccountManager() verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
.addDevice(deviceLinkInfo.deviceIdentifier(), } catch (DeviceLimitExceededException e) {
throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
}
handleResponseException(dependencies.getLinkDeviceApi()
.linkDevice(account.getNumber(),
account.getAci(),
account.getPni(),
deviceLinkInfo.deviceIdentifier(),
deviceLinkInfo.deviceKey(), deviceLinkInfo.deviceKey(),
account.getAciIdentityKeyPair(), account.getAciIdentityKeyPair(),
account.getPniIdentityKeyPair(), account.getPniIdentityKeyPair(),
account.getProfileKey(), account.getProfileKey(),
verificationCode); account.getOrCreateAccountEntropyPool(),
} catch (InvalidKeyException e) { account.getOrCreatePinMasterKey(),
throw new InvalidDeviceLinkException("Invalid device link", e); account.getOrCreateMediaRootBackupKey(),
} verificationCode.getVerificationCode(),
null,
account.getAuthCredentialSalt()));
account.setMultiDevice(true); account.setMultiDevice(true);
context.getJobExecutor().enqueueJob(new SyncStorageJob());
} }
public void removeLinkedDevices(int deviceId) throws IOException { public void removeLinkedDevices(int deviceId) throws IOException {
dependencies.getAccountManager().removeDevice(deviceId); handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi().removeDevice(deviceId, cont));
var devices = dependencies.getAccountManager().getDevices(); final List<DeviceInfo> devices = handleResponseExceptionSuspend(cont -> dependencies.getLinkDeviceApi()
.getDevices(cont));
account.setMultiDevice(devices.size() > 1); account.setMultiDevice(devices.size() > 1);
} }
@ -299,19 +606,23 @@ public class AccountHelper {
var masterKey = account.getOrCreatePinMasterKey(); var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey); context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
} }
public void setRegistrationPin(String pin) throws IOException { public void setRegistrationPin(String pin) throws IOException {
var masterKey = account.getOrCreatePinMasterKey(); var masterKey = account.getOrCreatePinMasterKey();
context.getPinHelper().setRegistrationLockPin(pin, masterKey); context.getPinHelper().setRegistrationLockPin(pin, masterKey);
handleResponseException(dependencies.getAccountApi().enableRegistrationLock(masterKey));
account.setRegistrationLockPin(pin); account.setRegistrationLockPin(pin);
updateAccountAttributes();
} }
public void removeRegistrationPin() throws IOException { public void removeRegistrationPin() throws IOException {
// Remove KBS Pin // Remove KBS Pin
context.getPinHelper().removeRegistrationLockPin(); context.getPinHelper().removeRegistrationLockPin();
handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
account.setRegistrationLockPin(null); account.setRegistrationLockPin(null);
} }
@ -320,7 +631,7 @@ public class AccountHelper {
// When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // 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 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. // 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()); handleResponseExceptionSuspend(cont -> dependencies.getAccountApi().clearFcmToken(cont));
account.setRegistered(false); account.setRegistered(false);
unregisteredListener.call(); unregisteredListener.call();
@ -334,7 +645,7 @@ public class AccountHelper {
} }
account.setRegistrationLockPin(null); account.setRegistrationLockPin(null);
dependencies.getAccountManager().deleteAccount(); handleResponseException(dependencies.getAccountApi().deleteAccount());
account.setRegistered(false); account.setRegistered(false);
unregisteredListener.call(); unregisteredListener.call();

View File

@ -6,14 +6,19 @@ import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.AttachmentStore; import org.asamk.signal.manager.storage.AttachmentStore;
import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.AttachmentUtils;
import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.InvalidMessageException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -25,12 +30,14 @@ import java.util.List;
public class AttachmentHelper { public class AttachmentHelper {
private final static Logger logger = LoggerFactory.getLogger(AttachmentHelper.class); private static final Logger logger = LoggerFactory.getLogger(AttachmentHelper.class);
private final SignalDependencies dependencies; private final SignalDependencies dependencies;
private final AttachmentStore attachmentStore; private final AttachmentStore attachmentStore;
private final Context context;
public AttachmentHelper(final Context context) { public AttachmentHelper(final Context context) {
this.context = context;
this.dependencies = context.getDependencies(); this.dependencies = context.getDependencies();
this.attachmentStore = context.getAttachmentStore(); this.attachmentStore = context.getAttachmentStore();
} }
@ -43,19 +50,87 @@ public class AttachmentHelper {
return attachmentStore.retrieveAttachment(id); return attachmentStore.retrieveAttachment(id);
} }
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException { public List<SignalServiceAttachment> uploadAttachments(
var attachmentStreams = AttachmentUtils.createAttachmentStreams(attachments); final List<String> attachments,
boolean voiceNote
) throws AttachmentInvalidException, IOException {
final var attachmentStreams = createAttachmentStreams(attachments, voiceNote);
try {
// Upload attachments here, so we only upload once even for multiple recipients // Upload attachments here, so we only upload once even for multiple recipients
var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size()); final var attachmentPointers = new ArrayList<SignalServiceAttachment>(attachmentStreams.size());
for (var attachmentStream : attachmentStreams) { for (final var attachmentStream : attachmentStreams) {
attachmentPointers.add(uploadAttachment(attachmentStream)); attachmentPointers.add(uploadAttachment(attachmentStream));
} }
return attachmentPointers; return attachmentPointers;
} finally {
for (final var attachmentStream : attachmentStreams) {
attachmentStream.close();
}
}
}
public List<SignalServiceAttachment> uploadAttachments(final List<String> attachments) throws AttachmentInvalidException, IOException {
return uploadAttachments(attachments, false);
}
private List<SignalServiceAttachmentStream> createAttachmentStreams(
List<String> attachments,
boolean voiceNote
) throws AttachmentInvalidException, IOException {
if (attachments == null) {
return null;
}
final var signalServiceAttachments = new ArrayList<SignalServiceAttachmentStream>(attachments.size());
for (var attachment : attachments) {
final var attachmentStream = getAttachmentStream(attachment, voiceNote);
signalServiceAttachments.add(attachmentStream);
}
return signalServiceAttachments;
}
private SignalServiceAttachmentStream getAttachmentStream(
final String attachment,
final boolean voiceNote
) throws AttachmentInvalidException {
try {
// Reject local files that point into the signal-cli data directory
if (attachment != null && !attachment.startsWith("data:")) {
try {
final var file = new File(attachment);
final var canonical = file.getCanonicalFile();
final var dataPath = context.getAccount().getDataPath().getCanonicalFile();
if (canonical.toPath().startsWith(dataPath.toPath())) {
throw new AttachmentInvalidException(attachment,
new IOException("Attaching files from the signal-cli data directory is not allowed"));
}
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
final var streamDetailsAndFileName = Utils.createStreamDetails(attachment);
final var streamDetails = streamDetailsAndFileName.first();
final var uploadSpec = getResumableUploadSpec(streamDetails);
return AttachmentUtils.createAttachmentStream(streamDetails,
streamDetailsAndFileName.second(),
voiceNote,
uploadSpec);
} catch (IOException e) {
throw new AttachmentInvalidException(attachment, e);
}
}
public ResumableUploadSpec getResumableUploadSpec(final StreamDetails streamDetails) throws IOException {
final var streamLength = streamDetails.getLength();
final var ciphertextLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(
streamLength));
return dependencies.getCdnService().getResumableUploadSpecBlocking(ciphertextLength);
} }
public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException { public SignalServiceAttachmentPointer uploadAttachment(String attachment) throws IOException, AttachmentInvalidException {
var attachmentStream = AttachmentUtils.createAttachmentStream(attachment); final var attachmentStream = getAttachmentStream(attachment, false);
return uploadAttachment(attachmentStream); return uploadAttachment(attachmentStream);
} }
@ -91,9 +166,7 @@ public class AttachmentHelper {
retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream)); retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream));
} }
public void retrieveAttachment( public void retrieveAttachment(SignalServiceAttachment attachment, AttachmentHandler consumer) throws IOException {
SignalServiceAttachment attachment, AttachmentHandler consumer
) throws IOException {
if (attachment.isStream()) { if (attachment.isStream()) {
var input = attachment.asStream().getInputStream(); var input = attachment.asStream().getInputStream();
// don't close input stream here, it might be reused later (e.g. with contact sync messages ...) // don't close input stream here, it might be reused later (e.g. with contact sync messages ...)
@ -118,11 +191,18 @@ public class AttachmentHelper {
} }
private InputStream retrieveAttachmentAsStream( private InputStream retrieveAttachmentAsStream(
SignalServiceAttachmentPointer pointer, File tmpFile SignalServiceAttachmentPointer pointer,
File tmpFile
) throws IOException { ) throws IOException {
if (pointer.getDigest().isEmpty()) {
throw new IOException("Attachment pointer has no digest.");
}
try { try {
return dependencies.getMessageReceiver() 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) { } catch (MissingConfigurationException | InvalidMessageException e) {
throw new IOException(e); throw new IOException(e);
} }

View File

@ -0,0 +1,809 @@
package org.asamk.signal.manager.helper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.CallInfo;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.TurnServer;
import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.IdentityKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
import org.whispersystems.signalservice.api.messages.calls.BusyMessage;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.asamk.signal.manager.util.Utils.callIdUnsigned;
import static org.asamk.signal.manager.util.Utils.handleResponseException;
/**
* Manages active voice calls: tracks state, spawns/monitors the signal-call-tunnel
* subprocess, routes incoming call messages, and handles timeouts.
*/
public class CallManager implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(CallManager.class);
private static final long RING_TIMEOUT_MS = 60_000;
private static final ObjectMapper mapper = new ObjectMapper();
private final Context context;
private final SignalAccount account;
private final SignalDependencies dependencies;
private final Map<Long, CallState> activeCalls = new ConcurrentHashMap<>();
private final List<Manager.CallEventListener> callEventListeners = new CopyOnWriteArrayList<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
var t = new Thread(r, "call-timeout-scheduler");
t.setDaemon(true);
return t;
});
public CallManager(final Context context) {
this.context = context;
this.account = context.getAccount();
this.dependencies = context.getDependencies();
}
public void addCallEventListener(Manager.CallEventListener listener) {
callEventListeners.add(listener);
}
public void removeCallEventListener(Manager.CallEventListener listener) {
callEventListeners.remove(listener);
}
private void fireCallEvent(CallState state, String reason) {
var callInfo = state.toCallInfo(account.getRecipientAddressResolver());
for (var listener : callEventListeners) {
try {
listener.handleCallEvent(callInfo, reason);
} catch (Throwable e) {
logger.warn("Call event listener failed, ignoring", e);
}
}
}
public CallInfo startOutgoingCall(
final RecipientId recipientId
) throws IOException {
var callId = generateCallId();
var recipientAddress = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
var state = new CallState(callId, CallInfo.State.RINGING_OUTGOING, recipientId, null, true);
logger.debug("Starting outgoing call {} to {} (recipientId: {})",
callIdUnsigned(callId),
recipientAddress,
recipientId);
activeCalls.put(callId, state);
dependencies.getAuthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
dependencies.getUnauthenticatedSignalWebSocket().registerKeepAliveToken("call" + callId);
fireCallEvent(state, null);
// Spawn call tunnel binary and connect control channel
spawnMediaTunnel(state);
// Fetch TURN servers
var turnServers = getTurnServers();
// Send createOutgoingCall + proceed via control channel
var createMsg = mapper.createObjectNode();
createMsg.put("type", "createOutgoingCall");
createMsg.put("callId", Utils.callIdUnsigned(callId));
createMsg.put("peerId", recipientAddress.toString());
sendControlMessage(state, writeJson(createMsg));
sendProceed(state, callId, turnServers);
// Schedule ring timeout
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
logger.debug("Started outgoing call {} to {}", callIdUnsigned(callId), recipientAddress);
return state.toCallInfo(account.getRecipientAddressResolver());
}
public CallInfo acceptIncomingCall(final long callId) throws IOException {
final var state = getActiveCall(callId);
if (state.state != CallInfo.State.RINGING_INCOMING) {
throw new IOException("Call "
+ callId
+ " is not in RINGING_INCOMING state (current: "
+ state.state
+ ")");
}
// Defer the accept until the tunnel reports Ringing state.
// Sending accept too early (while RingRTC is in ConnectingBeforeAccepted)
// causes it to be silently dropped.
state.acceptPending = true;
// If the tunnel is already in Ringing state, send immediately
sendAcceptIfReady(state);
state.state = CallInfo.State.CONNECTING;
fireCallEvent(state, null);
logger.debug("Accepted incoming call {}", callIdUnsigned(callId));
return state.toCallInfo(account.getRecipientAddressResolver());
}
public void hangupCall(final long callId) throws IOException {
getActiveCall(callId);
endCall(callId, "local_hangup");
}
public SendMessageResult rejectCall(final long callId) throws IOException {
final var callState = getActiveCall(callId);
final var result = sendBusyMessage(callState.callId, callState.recipientId, callState.deviceId);
endCall(callId, "rejected");
return result;
}
public List<CallInfo> listActiveCalls() {
return activeCalls.values()
.stream()
.map((CallState callState) -> callState.toCallInfo(account.getRecipientAddressResolver()))
.toList();
}
public List<TurnServer> getTurnServers() throws IOException {
try {
var turnServerList = handleResponseException(dependencies.getCallingApi().getTurnServerInfo());
return turnServerList.stream()
.map(info -> new TurnServer(info.getUsername(), info.getPassword(), info.getUrls()))
.toList();
} catch (Throwable e) {
logger.warn("Failed to get TURN server info, returning empty list", e);
return List.of();
}
}
// --- Incoming call message handling ---
public void handleIncomingOffer(
final RecipientId recipientId,
final int deviceId,
final long callId,
final MessageEnvelope.Call.Offer.Type type,
final byte[] opaque
) {
if (callEventListeners.isEmpty()) {
logger.debug("Ignoring incoming offer for call {}: no call event listeners registered",
callIdUnsigned(callId));
return;
}
var senderAddress = account.getRecipientAddressResolver()
.resolveRecipientAddress(recipientId)
.toApiRecipientAddress();
logger.debug("Incoming offer opaque ({} bytes)", opaque == null ? 0 : opaque.length);
var state = new CallState(callId, CallInfo.State.RINGING_INCOMING, recipientId, deviceId, false);
logger.debug("Starting incoming call {} from {} (recipientId: {})",
callIdUnsigned(callId),
senderAddress,
recipientId);
activeCalls.put(callId, state);
// Spawn call tunnel binary immediately
spawnMediaTunnel(state);
// Get identity keys for the receivedOffer message
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
// Fetch TURN servers
List<TurnServer> turnServers;
try {
turnServers = getTurnServers();
} catch (IOException e) {
logger.warn("Failed to get TURN servers for incoming call {}", callIdUnsigned(callId), e);
turnServers = List.of();
}
// Send receivedOffer to subprocess
var offerMsg = mapper.createObjectNode();
offerMsg.put("type", "receivedOffer");
offerMsg.put("callId", Utils.callIdUnsigned(callId));
offerMsg.put("peerId", senderAddress.toString());
offerMsg.put("senderDeviceId", deviceId);
offerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
offerMsg.put("age", 0);
offerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
offerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
sendControlMessage(state, writeJson(offerMsg));
// Send proceed with TURN servers
sendProceed(state, callId, turnServers);
fireCallEvent(state, null);
// Schedule ring timeout
scheduler.schedule(() -> handleRingTimeout(callId), RING_TIMEOUT_MS, TimeUnit.MILLISECONDS);
logger.debug("Incoming call {} from {}", callIdUnsigned(callId), senderAddress);
}
public void handleIncomingAnswer(final long callId, final int deviceId, final byte[] opaque) {
var state = activeCalls.get(callId);
if (state == null) {
logger.warn("Received answer for unknown call {}", callIdUnsigned(callId));
return;
}
// Get identity keys
// Use raw 32-byte Curve25519 public key (without 0x05 DJB prefix) to match Signal Android
byte[] localIdentityKey = getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
byte[] remoteIdentityKey = getRemoteIdentityKey(state);
// Forward raw opaque to subprocess
var answerMsg = mapper.createObjectNode();
answerMsg.put("type", "receivedAnswer");
answerMsg.put("opaque", java.util.Base64.getEncoder().encodeToString(opaque));
answerMsg.put("senderDeviceId", deviceId);
answerMsg.put("senderIdentityKey", java.util.Base64.getEncoder().encodeToString(remoteIdentityKey));
answerMsg.put("receiverIdentityKey", java.util.Base64.getEncoder().encodeToString(localIdentityKey));
sendControlMessage(state, writeJson(answerMsg));
state.deviceId = deviceId;
state.state = CallInfo.State.CONNECTING;
fireCallEvent(state, null);
logger.debug("Received answer for call {}", callIdUnsigned(callId));
}
public void handleIncomingIceCandidate(final long callId, final byte[] opaque, final int deviceId) {
var state = activeCalls.get(callId);
if (state == null) {
logger.debug("Received ICE candidate for unknown call {}", callIdUnsigned(callId));
return;
}
// Forward to subprocess as receivedIce
var iceMsg = mapper.createObjectNode();
iceMsg.put("type", "receivedIce");
iceMsg.put("senderDeviceId", deviceId);
var candidates = iceMsg.putArray("candidates");
candidates.add(java.util.Base64.getEncoder().encodeToString(opaque));
sendControlMessage(state, writeJson(iceMsg));
logger.debug("Forwarded ICE candidate to tunnel for call {}", callIdUnsigned(callId));
}
public void handleIncomingHangup(final long callId) {
if (callEventListeners.isEmpty() && !activeCalls.containsKey(callId)) {
return;
}
endCall(callId, "remote_hangup");
}
public void handleIncomingBusy(final long callId) {
if (callEventListeners.isEmpty() && !activeCalls.containsKey(callId)) {
return;
}
endCall(callId, "remote_busy");
}
// --- Internal helpers ---
private CallState getActiveCall(final long callId) throws IOException {
var state = activeCalls.get(callId);
if (state == null) {
throw new IOException("No active call with id " + callIdUnsigned(callId));
}
return state;
}
private SendMessageResult sendBusyMessage(final long callId, final RecipientId recipientId, final int deviceId) {
var busyMessage = new BusyMessage(callId);
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, deviceId);
return context.getSendHelper().sendCallMessage(callMessage, recipientId);
}
private void sendControlMessage(CallState state, String json) {
if (state.controlWriter == null) {
logger.debug("Queueing control message for call {} (not yet connected): {}",
callIdUnsigned(state.callId),
json);
state.pendingControlMessages.add(json);
return;
}
state.controlWriter.println(json);
}
private void sendProceed(CallState state, long callId, List<TurnServer> turnServers) {
var proceedMsg = mapper.createObjectNode();
proceedMsg.put("type", "proceed");
proceedMsg.put("callId", Utils.callIdUnsigned(callId));
proceedMsg.put("hideIp", false);
var iceServers = proceedMsg.putArray("iceServers");
for (var ts : turnServers) {
var server = iceServers.addObject();
server.put("username", ts.username());
server.put("password", ts.password());
var urls = server.putArray("urls");
for (var url : ts.urls()) {
urls.add(url);
}
}
sendControlMessage(state, writeJson(proceedMsg));
}
private void spawnMediaTunnel(CallState state) {
try {
var command = new ArrayList<>(List.of(findTunnelBinary()));
var processBuilder = new ProcessBuilder(command);
// Keep stdout and stderr separate: stdout = control protocol, stderr = logging
processBuilder.redirectErrorStream(false);
var process = processBuilder.start();
state.tunnelProcess = process;
// Write config JSON to stdin, then keep stdin open for control messages
var config = buildConfig(state);
var stdinStream = process.getOutputStream();
stdinStream.write(config.getBytes(StandardCharsets.UTF_8));
stdinStream.write('\n');
stdinStream.flush();
// stdin is the control write channel
state.controlWriter = new PrintWriter(new OutputStreamWriter(stdinStream, StandardCharsets.UTF_8), true);
// Flush any pending control messages
for (var msg : state.pendingControlMessages) {
state.controlWriter.println(msg);
}
state.pendingControlMessages.clear();
// If accept was deferred, send it now
sendAcceptIfReady(state);
// Read control events from subprocess stdout
Thread.ofVirtual()
.name("control-read-" + callIdUnsigned(state.callId))
.start(() -> readControlEvents(state, process.getInputStream()));
// Drain subprocess stderr to prevent pipe buffer deadlock
Thread.ofVirtual().name("tunnel-stderr-" + callIdUnsigned(state.callId)).start(() -> {
try (var reader = new BufferedReader(new InputStreamReader(process.getErrorStream(),
StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
logger.debug("[tunnel-{}] {}", callIdUnsigned(state.callId), line);
}
} catch (IOException ignored) {
}
});
// Monitor process exit
process.onExit().thenAcceptAsync(p -> {
logger.debug("Tunnel for call {} exited with code {}", callIdUnsigned(state.callId), p.exitValue());
if (activeCalls.containsKey(state.callId)) {
endCall(state.callId, "tunnel_exit");
}
});
logger.debug("Spawned signal-call-tunnel for call {}", callIdUnsigned(state.callId));
} catch (Exception e) {
logger.error("Failed to spawn tunnel for call {}", callIdUnsigned(state.callId), e);
endCall(state.callId, "tunnel_spawn_error");
}
}
private String findTunnelBinary() {
// Check environment variable first
var envPath = System.getenv("SIGNAL_CALL_TUNNEL_BIN");
if (envPath != null && !envPath.isEmpty()) {
return envPath;
}
// Check relative to the signal-cli installation directory
try {
var codeSource = CallManager.class.getProtectionDomain().getCodeSource();
if (codeSource != null) {
var jarPath = Path.of(codeSource.getLocation().toURI());
var binPath = tunnelBinaryFromCodeSourcePath(jarPath);
if (Files.isExecutable(binPath)) {
return binPath.toString();
}
}
} catch (Exception e) {
logger.debug("Failed to determine install dir from code source", e);
}
// Fall back to PATH
return "signal-call-tunnel";
}
/**
* Resolves the expected tunnel binary path from a code source path.
* The code source (jar or class dir) is expected to be in {@code <install>/lib/},
* so we go up two levels to reach the install root, then look for
* {@code bin/signal-call-tunnel}.
*/
static Path tunnelBinaryFromCodeSourcePath(Path codeSourcePath) {
var installDir = codeSourcePath.getParent().getParent();
return installDir.resolve("bin").resolve("signal-call-tunnel");
}
private String buildConfig(CallState state) {
var config = mapper.createObjectNode();
config.put("call_id", Utils.callIdUnsigned(state.callId));
config.put("is_outgoing", state.isOutgoing);
config.put("local_device_id", 1);
return writeJson(config);
}
private void readControlEvents(CallState state, java.io.InputStream inputStream) {
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
logger.debug("Control event for call {}: {}", callIdUnsigned(state.callId), line);
try {
var json = mapper.readTree(line);
var type = json.has("type") ? json.get("type").asText() : "";
switch (type) {
case "ready" -> {
if (json.has("inputDeviceName")) {
state.inputDeviceName = json.get("inputDeviceName").asText();
}
if (json.has("outputDeviceName")) {
state.outputDeviceName = json.get("outputDeviceName").asText();
}
logger.debug("Tunnel ready for call {}: input={}, output={}",
callIdUnsigned(state.callId),
state.inputDeviceName,
state.outputDeviceName);
}
case "sendOffer" -> {
var opaqueB64 = json.get("opaque").asText();
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
logSendMessageResult(sendOfferViaSignal(state, opaque));
}
case "sendAnswer" -> {
var opaqueB64 = json.get("opaque").asText();
var opaque = java.util.Base64.getDecoder().decode(opaqueB64);
logSendMessageResult(sendAnswerViaSignal(state, opaque));
}
case "sendIce" -> {
var candidatesArr = json.get("candidates");
var opaqueList = new ArrayList<byte[]>();
for (var c : candidatesArr) {
opaqueList.add(java.util.Base64.getDecoder().decode(c.get("opaque").asText()));
}
logSendMessageResult(sendIceViaSignal(state, opaqueList));
}
case "sendHangup" -> {
// RingRTC wants us to send a hangup message via Signal protocol.
// This is NOT a local state change local state is handled by stateChange events.
var hangupType = json.has("hangupType")
? json.get("hangupType").asText("normal")
: "normal";
// Skip multi-device hangup types signal-cli is single-device,
// and sending these to the remote peer causes it to terminate the call.
if (hangupType.contains("onanotherdevice")) {
logger.debug("Ignoring multi-device hangup type: {}", hangupType);
} else {
logSendMessageResult(sendHangupViaSignal(state, hangupType));
}
}
case "sendBusy" -> {
logSendMessageResult(sendBusyViaSignal(state));
}
case "stateChange" -> {
var ringrtcState = json.get("state").asText();
var reason = json.has("reason") ? json.get("reason").asText(null) : null;
handleStateChange(state, ringrtcState, reason);
}
case "error" -> {
var message = json.has("message") ? json.get("message").asText("unknown") : "unknown";
logger.error("Tunnel error for call {}: {}", callIdUnsigned(state.callId), message);
endCall(state.callId, "tunnel_error");
}
default -> {
logger.debug("Unknown control event type '{}' for call {}",
type,
callIdUnsigned(state.callId));
}
}
} catch (Exception e) {
logger.warn("Failed to parse control event JSON for call {}: {}",
callIdUnsigned(state.callId),
e.getMessage());
}
}
} catch (IOException e) {
logger.debug("Control read ended for call {}: {}", callIdUnsigned(state.callId), e.getMessage());
}
}
private void handleStateChange(CallState state, String ringrtcState, String reason) {
if (ringrtcState.startsWith("Incoming")) {
// Don't downgrade if we've already accepted
if (state.state == CallInfo.State.CONNECTING) return;
state.state = CallInfo.State.RINGING_INCOMING;
} else if (ringrtcState.startsWith("Outgoing")) {
state.state = CallInfo.State.RINGING_OUTGOING;
} else if ("Ringing".equals(ringrtcState)) {
// Tunnel is now ready to accept flush deferred accept if pending
state.tunnelRinging = true;
sendAcceptIfReady(state);
return;
} else if ("Connected".equals(ringrtcState)) {
state.state = CallInfo.State.CONNECTED;
} else if ("Connecting".equals(ringrtcState)) {
state.state = CallInfo.State.RECONNECTING;
} else if ("Ended".equals(ringrtcState) || "Rejected".equals(ringrtcState)) {
endCall(state.callId, reason != null ? reason : ringrtcState.toLowerCase());
return;
} else if ("Concluded".equals(ringrtcState)) {
// Cleanup, no-op
return;
}
fireCallEvent(state, reason);
}
public static void logSendMessageResult(SendMessageResult result) {
var identifier = result.getAddress().getIdentifier();
if (result.getProofRequiredFailure() != null) {
final var failure = result.getProofRequiredFailure();
logger.warn(
"CAPTCHA proof required for sending to \"{}\", available options \"{}\" with challenge token \"{}\", or wait \"{}\" seconds.\n",
identifier,
failure.getOptions()
.stream()
.map(ProofRequiredException.Option::toString)
.collect(Collectors.joining(", ")),
failure.getToken(),
failure.getRetryAfterSeconds());
} else if (result.isNetworkFailure()) {
logger.warn("Network failure for \"{}\"", identifier);
} else if (result.getRateLimitFailure() != null) {
logger.warn("Rate limit failure for \"{}\"", identifier);
} else if (result.isUnregisteredFailure()) {
logger.warn("Unregistered user \"{}\"", identifier);
} else if (result.getIdentityFailure() != null) {
logger.warn("Untrusted Identity for \"{}\"", identifier);
}
}
private void sendAcceptIfReady(CallState state) {
if (state.acceptPending && state.tunnelRinging && state.controlWriter != null) {
state.acceptPending = false;
logger.debug("Sending deferred accept for call {}", callIdUnsigned(state.callId));
var acceptMsg = mapper.createObjectNode();
acceptMsg.put("type", "accept");
state.controlWriter.println(writeJson(acceptMsg));
}
}
private SendMessageResult sendOfferViaSignal(CallState state, byte[] opaque) {
var offerMessage = new OfferMessage(state.callId, OfferMessage.Type.AUDIO_CALL, opaque);
var callMessage = SignalServiceCallMessage.forOffer(offerMessage, state.deviceId);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
logger.debug("Sent offer via Signal for call {}", callIdUnsigned(state.callId));
return result;
}
private SendMessageResult sendAnswerViaSignal(CallState state, byte[] opaque) {
var answerMessage = new AnswerMessage(state.callId, opaque);
var callMessage = SignalServiceCallMessage.forAnswer(answerMessage, state.deviceId);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
logger.debug("Sent answer via Signal for call {}", callIdUnsigned(state.callId));
return result;
}
private SendMessageResult sendIceViaSignal(CallState state, List<byte[]> opaqueList) {
var iceUpdates = opaqueList.stream().map(opaque -> new IceUpdateMessage(state.callId, opaque)).toList();
var callMessage = SignalServiceCallMessage.forIceUpdates(iceUpdates, state.deviceId);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
logger.debug("Sent {} ICE candidates via Signal for call {}", opaqueList.size(), callIdUnsigned(state.callId));
return result;
}
private SendMessageResult sendBusyViaSignal(CallState state) {
var busyMessage = new BusyMessage(state.callId);
var callMessage = SignalServiceCallMessage.forBusy(busyMessage, state.deviceId);
return context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
}
private SendMessageResult sendHangupViaSignal(CallState state, String hangupType) {
var type = switch (hangupType) {
case "accepted", "acceptedonanotherdevice" -> HangupMessage.Type.ACCEPTED;
case "declined", "declinedonanotherdevice" -> HangupMessage.Type.DECLINED;
case "busy", "busyonanotherdevice" -> HangupMessage.Type.BUSY;
default -> HangupMessage.Type.NORMAL;
};
var hangupMessage = new HangupMessage(state.callId, type, 0);
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, state.deviceId);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
logger.debug("Sent hangup ({}) via Signal for call {}", hangupType, callIdUnsigned(state.callId));
return result;
}
private byte[] getRemoteIdentityKey(CallState state) {
try {
var address = context.getRecipientHelper().resolveSignalServiceAddress(state.recipientId);
var serviceId = address.getServiceId();
var identityInfo = account.getIdentityKeyStore().getIdentityInfo(serviceId);
if (identityInfo != null) {
return getRawIdentityKeyBytes(identityInfo.getIdentityKey());
}
} catch (Exception e) {
logger.warn("Failed to get remote identity key for call {}", callIdUnsigned(state.callId), e);
}
logger.warn("Using local identity key as fallback for remote identity key");
return getRawIdentityKeyBytes(account.getAciIdentityKeyPair().getPublicKey());
}
/**
* Strip the 0x05 DJB type prefix from a serialized identity key to get the
* raw 32-byte Curve25519 public key. Signal Android does this via
* WebRtcUtil.getPublicKeyBytes() before passing keys to RingRTC.
*/
private static byte[] getRawIdentityKeyBytes(IdentityKey identityKey) {
var serializedKey = identityKey.serialize();
return getRawIdentityKeyBytes(serializedKey);
}
private static byte[] getRawIdentityKeyBytes(final byte[] serializedKey) {
if (serializedKey.length == 33 && serializedKey[0] == 0x05) {
return java.util.Arrays.copyOfRange(serializedKey, 1, serializedKey.length);
}
return serializedKey;
}
private static String writeJson(ObjectNode node) {
try {
return mapper.writeValueAsString(node);
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
throw new RuntimeException("Failed to serialize JSON", e);
}
}
private void endCall(final long callId, final String reason) {
var state = activeCalls.remove(callId);
dependencies.getAuthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
dependencies.getUnauthenticatedSignalWebSocket().removeKeepAliveToken("call" + callId);
if (state == null) return;
state.state = CallInfo.State.ENDED;
fireCallEvent(state, reason);
logger.debug("Call {} ended: {}", callIdUnsigned(callId), reason);
// Send Signal protocol hangup to remote peer (unless they initiated the end)
if (!"remote_hangup".equals(reason)
&& !"rejected".equals(reason)
&& !"remote_busy".equals(reason)
&& !"ringrtc_hangup".equals(reason)) {
var hangupMessage = new HangupMessage(callId, HangupMessage.Type.NORMAL, 0);
var callMessage = SignalServiceCallMessage.forHangup(hangupMessage, null);
final var result = context.getSendHelper().sendCallMessage(callMessage, state.recipientId);
if (!result.isSuccess()) {
logger.warn("Failed to send hangup to remote for call {}", callIdUnsigned(callId));
logSendMessageResult(result);
}
}
// Send hangup via control channel (stdin) before killing process
if (state.controlWriter != null) {
try {
var hangupMsg = mapper.createObjectNode();
hangupMsg.put("type", "hangup");
state.controlWriter.println(writeJson(hangupMsg));
state.controlWriter.close();
} catch (Exception e) {
logger.debug("Failed to send hangup via control channel", e);
}
}
// Kill tunnel process
if (state.tunnelProcess != null && state.tunnelProcess.isAlive()) {
state.tunnelProcess.destroy();
}
}
private void handleRingTimeout(final long callId) {
var state = activeCalls.get(callId);
if (state == null) return;
if (state.state == CallInfo.State.RINGING_INCOMING || state.state == CallInfo.State.RINGING_OUTGOING) {
logger.debug("Call {} ring timeout", callIdUnsigned(callId));
endCall(callId, "ring_timeout");
}
}
private static long generateCallId() {
return new BigInteger(64, new SecureRandom()).longValue();
}
@Override
public void close() {
scheduler.shutdownNow();
for (var callId : new ArrayList<>(activeCalls.keySet())) {
endCall(callId, "shutdown");
}
synchronized (callEventListeners) {
callEventListeners.clear();
}
}
// --- Internal call state tracking ---
static class CallState {
final long callId;
volatile CallInfo.State state;
final RecipientId recipientId;
volatile Integer deviceId;
final boolean isOutgoing;
volatile String inputDeviceName;
volatile String outputDeviceName;
volatile Process tunnelProcess;
volatile PrintWriter controlWriter;
// Control messages queued before the tunnel process starts
final List<String> pendingControlMessages = Collections.synchronizedList(new ArrayList<>());
// Accept deferred until tunnel reports Ringing state
volatile boolean acceptPending = false;
// True once the tunnel has reported "Ringing" (ready to accept)
volatile boolean tunnelRinging = false;
CallState(
long callId,
CallInfo.State state,
RecipientId recipientId,
final Integer deviceId,
boolean isOutgoing
) {
this.callId = callId;
this.state = state;
this.recipientId = recipientId;
this.deviceId = deviceId;
this.isOutgoing = isOutgoing;
}
CallInfo toCallInfo(RecipientAddressResolver addressResolver) {
return new CallInfo(callId,
state,
addressResolver.resolveRecipientAddress(recipientId).toApiRecipientAddress(),
inputDeviceName,
outputDeviceName,
isOutgoing);
}
}
}

Some files were not shown because too many files have changed in this diff Show More