* WIP: Haskell impl of resizeImageToStrSize
* WIP: add FFI export wrapper
* WIP: switch ios resizeImageToStrSizeSync to backend
* WIP: fix resizeImageToStrSizeSync
* WIP: add JNI wrappers
* Add non-base64 resize and cWriteImage
* WIP: add chat_write_image
* fix resizeToStr
* fixes
* fix syntax, raise jpeg quality back
* add chatWriteImage
* Fix cfArgs in tests
* Use jpeg-turbo encoder
* add nix pins
* bump jpeg-turbo
* add CI deps
* add encoding funs to android lib
* bump jpeg-turbo for 20.04 fix
* jpeg-turbo fix for 8.10
* android fixes
* Bump jpeg-turbo
* set static flag for jpeg-turbo
* Fix nix for aarch64/android
* Fix android C api
* Fix resize in android
* Bump hs-libjpeg-turbo
* target correct override
* override android lib from android pkgs
* add macos build tweaks
* bump hs-jpeg-turbo for 8.10 fixes
* more build fixes
* make ios compile
* Always use pkgconfig for libjpeg-turbo
* WIP: debug libjpeg in ubuntu pkg-config CI
* Add pkg-config to dockerfiles
* Add libturbojpeg to dockerfiles
* Use -dev version of libturbojpeg in dockerfiles
* Install pkg-config in windows CI
* Disable flag for custom libturbojpeg linking options
Workarounds relocation error on x86_64-linux with gcc:
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/libturbojpeg.a(turbojpeg.c.o): relocation R_X86_64_TPOFF32 against `errStr' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: failed to set dynamic section sizes: bad value
* WIP: debug libjpeg in windows pkg-config CI
* WIP: debug libjpeg package in windows CI
* core: image resize module and buffer FFI
* ci: link libjpeg-turbo statically on all platforms
* ui: resize images in the core
* core: rename image resize functions to chat_resize_image and chat_resize_image_uri
* ci: restore whitespace in build.yml
* ci: build libjpeg-turbo 3.1.4.1 from a submodule
* core: hs-jpeg-turbo from github.com/simplex-chat
* nodejs: resize images
* python: resize images
* core: hs-jpeg-turbo from gitlab.com, the GitHub import lacks LFS objects
* core: generate /img previews
* ci: link libjpeg-turbo on Windows without pkg-config
* ci: build libjpeg-turbo on Windows with the GHC toolchain
* docs: plan for image resizing in the core
* docs: update image resizing plan
* ios: prevent crash when wallpaper image cannot be resized
* android, desktop: raise an error when an image cannot be resized
* docs: update spec references for image resizing
* docker: build the CLI image from a clone without submodules
* docs: apply review fixes to image resizing plan
* docker: build the CLI from a git worktree with the submodule present
* docs: apply second review to image resizing plan
* docs: point image resizing plan at the rewritten commit
* core: choose image format by transparency in chat_resize_image
* nodejs: resize images to the app sizes
* python: resize images to the app sizes
* nodejs, python: describe the core modules as used for image resizing
* docs: add license notices of the image libraries
* ci: describe the avatar method without attributing it to the apps
* ci, ios, nix: remove the libjpeg-turbo build outside cabal
* python: name resize_image in the core module text
* nodejs, python: describe resizing to the app image sizes
* core: refuse images larger than the supported size
* core: resize to JPEG images with a side above 65500 pixels
* docs: record image resizing decisions for the bundled libjpeg-turbo
* core, docs: build libjpeg-turbo and stb from the fork submodules
* docs: record the fork pins in the image resizing plan
* core: resize images to at most 4320 pixels per side, refuse above 16384
* docs: record the image side limits in the image resizing plan
* core: pin the rebased hs-jpeg-turbo fork
* core: pin the hs-jpeg-turbo fork without package.yaml
* docs: plan scaled JPEG decoding and EXIF orientation of images
* docs: copy the JuicyPixels-stbir licence of the pinned fork
* core: pin the hs-jpeg-turbo fork with scaled JPEG decoding
* core: decode JPEG at the display size, up to 65500 pixels per side
* core: rotate JPEG images by their EXIF orientation
* docs: refer the image plan to the JPEG decoding code
* docs: record the approved image processing decisions
* core: limit JPEG decoding memory to 1024 MB outside iOS and Android
* docs: name JPEG decoding in the libjpeg-turbo notice
* docs: plan image resizing from files and the decisions of 26 Oct
* core: scale images in one Catmull-Rom pass with sharpening
* docs: plan image sizes at a fixed quality
* core: resize images at a fixed quality to fill the size limit
* core: resize images from a file path or a file descriptor
* ios, android, desktop: resize picked images from their files
* core: encode resized images with mozjpeg
* tests: check descriptor closing by the file it refers to
* core: raise JPEG quality only for a full-size image below 97% of the limit
* ci: name the mozjpeg submodule in the Windows cleanup comments
* docs: refer the image plan to the current code
* docs: refer the image plan to the code merged with master
* core: export every image resizing function from each library
* core: close the file descriptor when its handle cannot be created
* docs: count the C sources of mozjpeg with the arithmetic decoder
* core: use the reviewed hs-jpeg-turbo and JuicyPixels-stbir forks
* ci: build the armv7a Android core library on request
* core: round image heights half up in the size search
* ios: describe the file image resizing functions in SimpleX.h
* ci: pick, preview and send images in the Claude app tests
* core: decode PNG images with libspng and refuse other formats
* core: encode PNG with libspng, keeping the smaller filter choice
* ci: pick, preview and send images in the scripted app smoke tests
* flake: build the PNG decoder package in nix builds
* ios: remove the copy of a picked image after sending it
* core: refuse image files larger than the decode memory limit
* core: scale a JPEG image to its largest size once per resize
* core: bisect JPEG quality without a step function
* core: reduce the memory used to sharpen and to decode PNG images
* android, desktop: pass the image format to the core as an integer
* android, desktop: fuse the preview conditions of picked media
* android, desktop, ci: check that the core resized the smoke test images
* python: remove the docstrings of the explicit image resizing functions
* docs: add the second review fixes to the image plan
* ci: tap the iOS attachment button by its accessibility label
* core: keep the colour profiles of resized images
* docs: add the colour profiles to the image plan
* ci: wait up to 30 s for the new group screens in the iOS smoke test
* ci: pick the Android smoke test images from their own photo folders
* ci: tap the iOS photo picker by position, as its tree is not shown
* ci: pick the test card and the logo by their iOS picker positions
* core: update JuicyPixels-stbir with the stb_image_resize licence terms
* core: write a fixed Display P3 profile for Display P3 images only
* core: do not keep the input image in memory for its colour profile
* core: read the JPEG segments once for the orientation and the profile
* core: read an image file into one buffer of its size
* docs: add the third review fixes to the image plan
* core: resize images with Display P3 or no profile, not profile bytes
* ci: check the pixel sizes of the images sent in the iOS smoke test
* core: update hs-jpeg-turbo with PNG chunk limits and encoder fixes
* docs: list Compact ICC Profiles, libspng and miniz in the dependencies
* android, desktop: reword the picked media preview comment
* docs: refer the image plan to the code merged with master
* ci: render the iOS snapshots before the images case
* ci: dismiss the snapshot tests' camera alert before the iOS images case
* core: update hs-jpeg-turbo with tests and the JuicyPixels-spng synopsis
* core: pin hs-jpeg-turbo and JuicyPixels-stbir in simplex-chat repositories
* update ios core library
* core, ui: accept only JPEG and PNG image data
* core: update JuicyPixels-stbir
* docs: plan for the iOS copy of picked images
---------
Co-authored-by: IC Rainbow <aenor.realm@gmail.com>
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Alexandre Esteves <alexfmpe@proton.me>
* core: test that history posted before channel signing reaches later joiners as signature missing, and channel moderator delete for all
Confirms the case the owner's new alert describes: a subscriber who joins after the owner turned on "Sign messages" sees the earlier posts as "signature missing" and later ones as signed, while earlier subscribers keep them unmarked. Also covers a non-owner channel moderator deleting a contributor's signed post for everyone.
Run: 001-channel-signing-moderators, slice 1
* ios: keep "Chat with admins" off when saving group preferences
toGroupPreferences left out the support preference, so the saved profile had none and core filled in its default, on: every channel preference save from iOS silently turned on chats with admins, which have no end-to-end encryption in channels. Android and desktop already pass it.
Run: 001-channel-signing-moderators, slice 2
* ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing
The channel preference was hidden while signing was recipient-only. Owners now see the switch; turning it on asks for confirmation, because subscribers who join later receive the earlier posts unsigned through history and see them as "signature missing". The "Signature missing" explanation now says the message may predate signing or have been forwarded by a relay without its signature, instead of only implying tampering.
Run: 001-channel-signing-moderators, slice 3
* ui: channel owners can make subscribers moderators, and moderators see the subscriber list
Core already lets a channel owner make a subscriber a moderator; the role picker offered only subscriber and contributor. It now offers moderator too (not admin, by decision). Moderators see the full member list the owner sees, so they can reach the subscribers they may block (subscribers can't post, so there was no other way to reach them), and member info is headed Moderator or Contributor instead of Subscriber for those roles.
Run: 001-channel-signing-moderators, slice 4
* ci: long-press and right-click for app tests, safer ui arguments, desktop Kotlin 4 GB, desktop session limits from claude-scope
Message menus open only on long-press (iOS) or right-click (desktop), so Claude sessions couldn't reach Moderate, Info or Delete. Ported from f/directory-search unchanged, with its hardening: the guards refuse unquoted glob, brace and tilde expansion, and desktop ui checks numbers and sends one key per call so a key argument can't run an xdotool command. The desktop app job's Kotlin compiler gets 4 GB, as f/directory-search needed after merging master. Desktop sessions take max-turns and max-budget-usd from claude-scope like iOS (60 and 3 when unset, as before), and the Claude job may run 120 minutes.
Run: 001-channel-signing-moderators, slice 5
* ci: Claude test plans for channel message signing and channel moderators
This branch's iOS device test plan and desktop prompt replace master's smoke tests: the owner creates a channel on the preset relays, turns on Sign messages through its alert, and two more profiles in the same app join by the copied link; the owner makes one a moderator, who then sees the subscriber list, moderates a contributor's message and blocks the contributor. Signatures are checked as text in message Info. One layout, no test backend.
Run: 001-channel-signing-moderators, slice 6
* fixup! ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing
* update plans
* fixup! ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing
* fixup! ci: Claude test plans for channel message signing and channel moderators
* fixup! ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing
* fixup! ci: Claude test plans for channel message signing and channel moderators
* fixup! ui: let channel owners turn on "Sign messages", warning that earlier messages will show signature missing
* fixup! ci: Claude test plans for channel message signing and channel moderators
---------
Co-authored-by: spacedandy-agents-simplex <342056592+spacedandy-agents-simplex@users.noreply.github.com>
* plan: UI improvements to improve LLM usage via SimpleX
* core, ui: types for chat layout
* core, ui: settings for alternative terminal-like chat layout for managing LLMs or other service bots
* plan: message priority
* fixes
* query plan
* update plans
* remove silent messages plan
* rename migrations
* implement layout
* amend
* fix
* fix
* fix
* improve
* time on the right, sender name in blue
* no avatar in direct chats
* better previews
* update, fixes
* update
* time gap
* changes
* ui: terminal layout line heights, gaps and time placement
* ui: simplify terminal meta layout, correct its docs and tests
* ui: right-to-left emoji edge in terminal rows, time placement tests
* android, desktop: raise Kotlin compiler memory to 4 GB
* ui: terminal captions as follow-up text, classic event separators
* ui: terminal media edge and overlay time, plan decisions
* ui: simplify terminal row separation, correct doc line references
* ui: keep the media time inside terminal rows, restore caption conditions
* ui: terminal heading capitals at large font scales, idle live message time
* ui: rename the classic voice-with-text flag, correct the meta copy docs
* ui: smaller terminal layout diff, sender gap and meta names
* ui: terminal row as tall as the media time
* docs: remove the chat layout section from the bots readme
* docs: shorten the terminal layout section of the chat view
* core: address settings API as on master, /auto_accept reads the stored layout
* ui: smaller terminal headings
* ui: classic sender rows and avatar rule as on master
* ui: classic reply quote text colour as on master
* ios: keep the chat layout setting with other UI settings
* ui: go-to button at the end of terminal rows, no gutter in direct chats
* ui: forwarded arrow on the header line of terminal rows, above the time
* ui: forwarded arrow centred above the time line of terminal rows
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
* tests: fix group tests
* fix test
* fix
* fix delivery worker fix
* watch ci
* debug
* fix delivery worker not forwarding to moderators during host acceptance
* add script, update simplexmq
* debug lines
* more debug
* rerun
* debug
* fix, debug
* simplexmq
* simplexmq
* fix
* debug: log member-not-found call sites, restore hang diagnostics, use simplexmq fork
* tests: do not prompt for migration confirmation in strict tables test
* ci: run tests with one capability; simplexmq: end inactive sessions without closing under readers
* ci: run tests on all capabilities
* remote: close the HTTP2 client before its TLS connection
* cabal: use warp that kills timed out connection threads without closing their sockets
* remove diagnostics: member-not-found log, thread dump, CI watchdog; update simplexmq
* badge-service: prevent interleaving of concurrent log lines
* tests: wait for badge group broadcasts before closing members
* tests: log contact connected events received twice
* tests: log member connected events received twice
* core: use simplexmq that logs connections emitting CON twice
* core: keep the team contact above notes in a new user's chat list
* ci: skip macOS and Windows builds while hunting a Linux-only flake
* core: use simplexmq that logs the history of connections emitting CON twice
* tests: remove the logging of connected events received twice
* ci: run macOS and Windows builds again
* core: use simplexmq that runs one thread per agent worker
* core: use the merged simplex-chat/wai warp and the updated simplexmq
* core: use the merged simplexmq
* core: update simplexmq
* core, badge service: comment single writes of terminal segments and log lines
* rfc: invite member of one group to another group
* update
* update
* implement
* version
* update
* do not show error if contact was removed when joining group
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* core: group fixes/improvements
* Large messages dropped by relays and hosts (the same 15602 limit was applied, without accounting for forwarding header)
* Members below v18 did not receive forwarded messages or history in p2p groups.
* Join confirmations near the size limit could fail.
* SMP servers are limited to 2 host names each.
* simplify
* remove legacy forwarding and history support
* only restrict p2p groups
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* core, ui: remove database backups after key changes
* ui: delete database backups on chat start without delaying encryption marker reset
Keeping encryptionStartedAt set until chat start made DatabaseErrorView show
"The attempt to change database passphrase was not completed" with a Restore
button after every successful passphrase change when the passphrase is not
stored. Restore the marker handling from master and delete backups
unconditionally after chat starts.
* another flag
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com>
* core: cancel the chat callback on interrupt
* badges: add multi-use codes
* badges: add the managed group
* badges: document multi-use codes and the group
* core: sign test badges with whole-second expiry
* badges: rework tracker and reply texts
* core: support message signing in p2p groups
* improve
* add member key
* distribute keys and sign
* refactor
* better query
* map
* sign in relay groups too
* clean up
* list
* fix test
* update bot types
* some refactor
* remove unnecessary condition
* simplify
* refactor
* simplify
* move
* clean up
* diff
* diff
* limit attempts for key sending
* optimize
* fix test
* split
* fuse
* null
* only mark as "key sent" when forwarder supports binary encoding
* fix bot apis
* fix some tests
* add key distribution steps, and fix some tests
* fix test
* increase timeout
* fix tests
* fix more tests
* simplify
* disable test output
* mark keys sent with invitations
* fix test
* fix test, query plans
* unify signing of connection info packets
* revert change to createNewGroup
* create key at group/member creation
* rename, remove liftIO
* clean up
* fix type
* remove ad hoc key sending
* update bot api
* diff
* reduce diff
* failing test
* fix sending messages in groups with members before version 18
* remove test delays
* update query plans
* update test
* add tests
* fix tests
---------
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
* core, ui: auto-accept group invitations per user profile
Add a per-profile toggle for auto-accepting group invitations, and regroup it
with the existing contact-requests setting under a single Auto-accept section
in Privacy & Security, relabelled "Contact requests in groups".
The join is fully async. processGroupInvitation already had an async accept
path, used when the invitation matches a group link the user opened:
prepareAgentJoin + createMemberConnectionAsync + joinAgentConnectionAsync,
with the outcome reported later against the CFJoinConn command id. Auto-accept
takes that same path instead of going through APIJoinGroup, so it works while
the app is closed and never blocks message processing.
An auto-accepted invitation still records a CIRcvGroupInvitation item in the
chat with the inviting contact, so there is a record of who added the user to
which group.
Two details worth noting for review:
hostContact is reported to clients only for group links. Clients respond to it
by replacing the transient host connection view with the group and removing
that chat - correct for a group link, where the contact is a placeholder, but
wrong for a plain invitation, where it is a real contact.
A resent invitation returns the existing group, because createGroupInvitation
is idempotent on inv_queue_info. The join therefore only runs while the
membership is still GSMemInvited, so a resend cannot open a second connection.
* booldef
* order
* refactor
* update translation key
* query plans
* ios: export translations
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Messages of blocked members are hidden, but they still marked chat item
as user mention, showing mention badge and counting group as unread in
"mentions only" notification mode:
- createNonLive passed mentions unfiltered, allowing members blocked by
admin to mention user - it is the default path, as it is only used
when full delete is not allowed;
- userReply was set for replies to user messages from blocked members.
* core: remove SimpleX Status preset contact
Preset contact cards are only created at user record creation
(createPresetContactCards), so this affects new profiles only;
existing profiles keep their stored SimpleX Status contact.
Removing the card shifts contact ids allocated after /create user
down by one, hence the test id updates.
* plans: justify SimpleX Status preset contact removal
* core: don't mark member support chat items read when reading group without scope
Reading a group without a scope marked support-scope items read without
decrementing the per-member support_chat_items_* counters, so members stayed
unread in the support list even after their chat was fully read. Restrict the
no-scope group read and its timed-items query to main-scope items.
* plans: support chat unread on no-scope group read
* core: update query plans for group scope read
The main-scope read and timed-items queries now filter on group_scope_tag
and group_scope_group_member_id, so they seek via idx_chat_items_group_scope_stats_all
(5-column) instead of idx_chat_items_groups_user_mention (3-column).
* plans: document query-plan and benchmark performance results
* tests: fix unreliable support item id capture in no-scope group read test
lastItemId returns the latest item by item_ts, which right after createGroup2
can be the group "connected" event rather than the just-sent support message.
The per-item read then targeted the wrong (already-read, main-scope) item and
never decremented the support counters, so the test failed regardless of the
fix (consistently in CI, flakily locally depending on item ordering).
Capture the support item id directly from the member-support scope instead,
keeping the change contained to this test. Verified: the test passes with the
fix and fails when the fix is reverted.
* tests: fix name-shadowing build error in no-scope group read test
The local pattern binding `itemId` shadowed the `itemId` helper imported
from ChatTests.Utils, which -Wname-shadowing (Werror) rejects. Rename the
local binding to `iId`.
* core, ui: plan per-server roles for self-hosted servers
* core: add per-server roles field to UserServer
* core: add nullable role columns to protocol_servers
* core: persist per-server roles
* core: validate server coverage using per-server roles
* test: cover per-server roles resolution and coverage
* multiplatform: per-server role toggles for self-hosted servers
* ios: per-server role toggles for self-hosted servers
* core: per-server role overrides with per-role defaults
* test: cover three-state per-server role resolution
* fix: derive Eq for ServerRolesOverride
* multiplatform: three-state role dropdowns on saved servers
* ios: three-state role pickers on saved servers
* test: per-server roles independent across two servers
* test: enable names role in name-resolution tests
* style: trim comments in per-server roles code
* chore: rename server_roles migration to 20260716 (last)
* core: per-server roles override operator roles, inherit when unset
* multiplatform: per-server role default inherits from operator
* ios: per-server role default inherits from operator
* refactor(servers): tidy per-server roles per review
- dedup no-operator default into ServerRoles.noOperatorDefault (Kotlin/Swift)
- iOS: move roles-section control flow to the call site via a named gate,
and parse the server address once instead of up to three times
- Kotlin: collapse redundant derivedState; revert defaultOn->default rename
for cross-platform parity
- drop unused Hashable conformance on Swift ServerRoles
- add agentServerCfgs test for names inheritance from an operator
- remove no-op enableNamesRole calls from dormant DirectoryTests
- fix ChatClient import ordering
* chore(migration): date server_roles migration 20260720
* only show roles when server is enabled, move section above QR code
---------
Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
* cli: add --relay-address-server option for chat relay
New CLI flag --relay-address-server SERVER selects the SMP server used
for the chat relay address link created at startup. Only valid together
with --relay; errors out otherwise.
Threads Maybe SMPServerWithAuth through APICreateMyAddress to the new
agent createConnection parameter.
* cli: add --user-display-name option
Selects or creates the active user non-interactively:
- no active user: create one with the given display name
- active user with matching localDisplayName: continue
- active user with different name: exit with error
Mutually exclusive with --create-bot-display-name.
* cli: add --user-image-file option
Sets the active user's profile image from a .png/.jpg/.jpeg file at
startup. Reads file, base64-encodes as data URL, and updates the user
profile directly in the DB - no notification is sent to existing
contacts. Skips the update if the stored image already matches.
Requires --user-display-name.
* cli: address PR review comments
- rename APICreateMyAddress field srv_ to server_
- extract repeated `loop` and putStrLn from createActiveUser via
prompt where-clause
- fuse u_ inspection: validate active user display name in the same
case that creates the user when missing
* cli: enforce profile image size limit in --user-image-file
Reject the file if the encoded data URL exceeds 12500 bytes - matches
the cap mobile and desktop UIs pass to resizeImageToStrSize for profile
images. Without this, oversized images would be silently set on the
user profile.
* core: validate profile image size in chat commands
Enforced in CreateActiveUser, updateProfile_, newGroup, runUpdateGroupProfile
via checkProfileImageSize; max 12500 bytes (matches mobile UIs).
* fix: thread new ChatOpts/CoreChatOpts fields through bot/test constructors
* bots/docs: filter hidden params before type introspection
Hide APICreateMyAddress server_ field so the bot doc generator does not
try to introspect SMPServerWithAuth (an unregistered type).
* core: validate full encoded profile size
Add checkProfileSize / checkGroupProfileSize that encode the full
ChatMessage and check against maxEncodedInfoLength, so a long
displayName/bio combined with a near-max image is also caught at
command time instead of failing later at send time with CEException.
Run alongside the existing checkProfileImageSize (image-only cap of
12500 bytes, matching mobile UIs) in CreateActiveUser, updateProfile_,
newGroup, runUpdateGroupProfile. Update genProfileImg to fit the cap.
* cli: add --headless option for chat relay
Skips interactive prompts (relay address creation, display name) so the
chat relay can run non-interactively as a service. Requires --relay;
creating a new profile also requires --user-display-name.
* test: cover profile image size limit and address server
Adds tests for two new capabilities:
- profile image size validation rejects oversized images
- /_address with a server pins the address to the requested SMP server
* core: update simplexmq (pass optional SMP server to prepareConnectionLink)
* cli: add /set profile image file, fix image flag
Add "/set profile image file <path>" command to set the profile image
from a .png/.jpg/.jpeg file in a running session.
Make --user-image-file create-only: for an existing user it now no-ops
with a note instead of failing with "chat not started" (the update ran
before the chat controller was started).
* core: unify image loading and profile size checks
- loadImageFile (CLI) and readProfileImageFile (command) now share one
loadImageData; removes the duplicated mime/base64 encoding and its
decodeUtf8/safeDecodeUtf8 divergence. A missing --user-image-file no
longer crashes with an uncaught IOException, and empty files are rejected.
- checkProfileSize/checkGroupProfileSize share checkInfoSize.
- --relay-address-server/--headless requires-relay checks use
errorWithoutStackTrace, matching the adjacent validation (no callstack dump).
* bots/docs: document UpdateProfileImageFromFile as a CLI command