* 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>
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.
* 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>
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>
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>
* 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>