cli: add name, image and relay server address flags (#6944)

* 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
This commit is contained in:
sh authored and GitHub committed 2026-07-15 09:31:19 +01:00
1 parent 937c79bc3c
commit 6e2995cab6
14 files changed
+265 -49

No files matched your search

+75
View File
@@ -47,6 +47,8 @@ chatProfileTests = do
describe "user profiles" $ do
it "update user profile and notify contacts" testUpdateProfile
it "update user profile with image" testUpdateProfileImage
it "reject profile image that is too large" testSetProfileImageTooLarge
it "set profile image from file" testSetProfileImageFromFile
it "use multiword profile names" testMultiWordProfileNames
it "present supporter badge to contacts" testUserBadgeBroadcast
it "supporter badge sent to contact connecting after attach" testUserBadgeOnConnect
@@ -57,6 +59,7 @@ chatProfileTests = do
it "supporter badge sent to contact connecting via address" testUserBadgeContactAddress
describe "user contact link" $ do
it "create and connect via contact link" testUserContactLink
it "create address on specified server" testCreateAddressOnServer
it "retry connecting via contact link" testRetryConnectingViaContactLink
it "add contact link to profile" testProfileLink
it "auto accept contact requests" testUserContactLinkAutoAccept
@@ -423,6 +426,52 @@ testUpdateProfileImage =
bob <## "use @alice2 <message> to send messages"
(bob </)
testSetProfileImageTooLarge :: HasCallStack => TestParams -> IO ()
testSetProfileImageTooLarge =
testChat2 aliceProfile bobProfile $
\alice bob -> do
connectUsers alice bob
-- image within the size limit is accepted
alice ##> "/set profile image data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="
alice <## "profile image updated"
-- image over the size limit is rejected;
-- the long command wraps in the virtual terminal, so drain echo lines until the error
alice `send` ("/set profile image data:image/png;base64," <> replicate 12500 'A')
let errPrefix = "bad chat command: Profile image is too large"
expectError = do
l <- getTermLine alice
unless (take (length errPrefix) l == errPrefix) expectError
expectError
(bob </)
testSetProfileImageFromFile :: HasCallStack => TestParams -> IO ()
testSetProfileImageFromFile ps = testChat aliceProfile test ps
where
tmp = tmpPath ps
pngPath = tmp <> "/avatar.png"
gifPath = tmp <> "/avatar.gif"
missingPath = tmp <> "/missing.png"
emptyPath = tmp <> "/empty.png"
test alice = do
B.writeFile pngPath "fake png bytes" -- content is not validated, only the extension
-- set profile image from a .png file
alice ##> ("/set profile image file " <> pngPath)
alice <## "profile image updated"
alice ##> "/show profile image"
alice <## "Profile image:"
alice <##. "data:image/png;base64,"
-- unsupported extension is rejected
B.writeFile gifPath "GIF89a"
alice ##> ("/set profile image file " <> gifPath)
alice <##. "bad chat command: unsupported image extension"
-- missing file is rejected
alice ##> ("/set profile image file " <> missingPath)
alice <##. "bad chat command: image file not found"
-- empty file is rejected
B.writeFile emptyPath ""
alice ##> ("/set profile image file " <> emptyPath)
alice <##. "bad chat command: image file is empty"
testMultiWordProfileNames :: HasCallStack => TestParams -> IO ()
testMultiWordProfileNames =
testChat3 aliceProfile' bobProfile' cathProfile' $
@@ -529,6 +578,32 @@ testUserContactLink =
alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")]
alice <##> cath
testCreateAddressOnServer :: HasCallStack => TestParams -> IO ()
testCreateAddressOnServer ps = testChat aliceProfile test ps
where
tmp = tmpPath ps
-- second SMP server, distinct from alice's configured server (localhost:7001)
altServer = "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"
altServerCfg =
smpServerCfg
{ transports = [("7003", transport @TLS, False)],
serverStoreCfg = persistentServerStoreCfg tmp
}
test alice = do
withSmpServer' altServerCfg $ do
-- without a server the address is created on the configured server (7001)
alice ##> "/_address 1"
(_, defaultLink) <- getContactLinks alice True
defaultLink `shouldContain` "localhost%3A7001" -- server is URL-encoded in the link
alice ##> "/_delete_address 1"
alice <## "Your chat address is deleted - accepted contacts will remain connected."
alice <## "To create a new chat address use /ad"
-- with a server the address is pinned to the requested server (7003)
alice ##> ("/_address 1 " <> altServer)
(_, pinnedLink) <- getContactLinks alice True
pinnedLink `shouldContain` "localhost%3A7003"
alice <## "disconnected 1 connections on server localhost"
testRetryConnectingViaContactLink :: HasCallStack => TestParams -> IO ()
testRetryConnectingViaContactLink ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile test ps
where
+4 -1
View File
@@ -24,6 +24,7 @@ import Data.Maybe (fromMaybe)
import Data.String
import qualified Data.Text as T
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), mkStoreCxt)
import Simplex.Chat.Library.Commands (maxProfileImageSize)
import Simplex.Chat.Markdown (viewName)
import Simplex.Chat.Messages.CIContent (e2eInfoNoPQText, e2eInfoPQText)
import Simplex.Chat.Protocol
@@ -241,7 +242,9 @@ genProfileImg = do
g <- C.newRandom
atomically $ B64.encode <$> C.randomBytes lrgLen g
where
lrgLen = maxEncodedInfoLength * 3 `div` 4 - 420
-- raw bytes that base64-encode to fit maxProfileImageSize when prefixed with "data:image/png;base64,"
lrgLen = (maxProfileImageSize - imagePrefixLen) * 3 `div` 4 - 1
imagePrefixLen = 22
-- PQ combinators /