mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-28 20:49:50 +00:00
core, directory: allow voice messages during member approval phase to allow audio captchas in groups that prohibit voice messages (#6624)
* rfcs: add member-support-voice rfc * update based on the feedback * implement RFC * add new tests * fix protocol tests and update plans * restrict voice captcha exemption to host approval phase * update agent_query_plans.txt
This commit is contained in:
@@ -54,7 +54,7 @@ import Simplex.Chat.Core
|
||||
import Simplex.Chat.Markdown (Format (..), FormattedText (..), parseMaybeMarkdownList, viewName)
|
||||
import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Protocol (MsgContent (..))
|
||||
import Simplex.Chat.Protocol (MsgContent (..), memberSupportVoiceVersion)
|
||||
import Simplex.Chat.Store.Direct (getContact)
|
||||
import Simplex.Chat.Store.Groups (getGroupLink, getGroupMember, setGroupCustomData) -- TODO remove setGroupCustomData
|
||||
import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo)
|
||||
@@ -569,7 +569,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
a = groupMemberAcceptance g
|
||||
captchaNotice =
|
||||
"Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "."
|
||||
<> if isJust (voiceCaptchaGenerator opts) then "\nSend /audio to receive a voice captcha." else ""
|
||||
<> if canSendVoiceCaptcha g m then "\nSend /audio to receive a voice captcha." else ""
|
||||
|
||||
sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> CaptchaMode -> IO ()
|
||||
sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts mode = do
|
||||
@@ -614,6 +614,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
img : _ -> MCImage "" $ ImageData img
|
||||
textMsg = MCText $ T.pack s
|
||||
|
||||
canSendVoiceCaptcha :: GroupInfo -> GroupMember -> Bool
|
||||
canSendVoiceCaptcha gInfo m =
|
||||
isJust (voiceCaptchaGenerator opts)
|
||||
&& (groupFeatureUserAllowed SGFVoice gInfo || supportsVersion m memberSupportVoiceVersion)
|
||||
|
||||
approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO ()
|
||||
approvePendingMember a g@GroupInfo {groupId} m@GroupMember {memberProfile = LocalProfile {displayName, image}} = do
|
||||
gli_ <- join . eitherToMaybe <$> withDB' "getGroupLinkInfo" cc (\db -> getGroupLinkInfo db userId groupId)
|
||||
@@ -637,16 +642,20 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
isAudioCmd = T.strip msgText == "/audio"
|
||||
cmd = fromRight (ADC SDRUser DCUnknownCommand) $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.strip msgText
|
||||
atomically (TM.lookup gmId $ pendingCaptchas env) >>= \case
|
||||
Nothing ->
|
||||
let mode = if isAudioCmd then CMAudio else CMText
|
||||
in sendMemberCaptcha g m (Just ciId) noCaptcha 0 mode
|
||||
Nothing
|
||||
| isAudioCmd && canSendVoiceCaptcha g m -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 CMAudio
|
||||
| isAudioCmd -> sendComposedMessages_ cc sendRef [(Just ciId, MCText voiceCaptchaUnavailable)]
|
||||
| otherwise -> sendMemberCaptcha g m (Just ciId) noCaptcha 0 CMText
|
||||
Just pc@PendingCaptcha {captchaText, sentAt, attempts, captchaMode}
|
||||
| isAudioCmd -> case captchaMode of
|
||||
CMText -> do
|
||||
atomically $ TM.insert gmId pc {captchaMode = CMAudio} $ pendingCaptchas env
|
||||
sendVoiceCaptcha sendRef (T.unpack captchaText)
|
||||
CMAudio ->
|
||||
sendComposedMessages_ cc sendRef [(Just ciId, MCText audioAlreadyEnabled)]
|
||||
| isAudioCmd ->
|
||||
if canSendVoiceCaptcha g m
|
||||
then case captchaMode of
|
||||
CMText -> do
|
||||
atomically $ TM.insert gmId pc {captchaMode = CMAudio} $ pendingCaptchas env
|
||||
sendVoiceCaptcha sendRef (T.unpack captchaText)
|
||||
CMAudio ->
|
||||
sendComposedMessages_ cc sendRef [(Just ciId, MCText audioAlreadyEnabled)]
|
||||
else sendComposedMessages_ cc sendRef [(Just ciId, MCText voiceCaptchaUnavailable)]
|
||||
| otherwise -> case cmd of
|
||||
ADC SDRUser (DCSearchGroup _) -> do
|
||||
ts <- getCurrentTime
|
||||
@@ -679,6 +688,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
||||
noCaptcha = "Unexpected message, please try again."
|
||||
audioAlreadyEnabled :: Text
|
||||
audioAlreadyEnabled = "Audio captcha is already enabled."
|
||||
voiceCaptchaUnavailable :: Text
|
||||
voiceCaptchaUnavailable = "Voice captcha is not available - please update SimpleX Chat to v6.5+ or use text captcha."
|
||||
unknownCommand :: Text
|
||||
unknownCommand = "Unknown command, please enter captcha text."
|
||||
tooManyAttempts :: Text
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# Voice messages in member support scope
|
||||
|
||||
## Table of contents
|
||||
|
||||
1. Executive summary
|
||||
2. Problem
|
||||
3. High-level design
|
||||
4. Detailed implementation plan
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
Allow voice messages from host/admin during the approval phase (member pending) regardless of group voice settings, gated behind chat protocol version 17. This enables the directory bot to send voice captchas in groups that prohibit voice messages. Old clients that don't support this exemption will receive text/image captchas instead.
|
||||
|
||||
## 2. Problem
|
||||
|
||||
The directory bot sends voice captchas to joining members via the member support scope (`GCSMemberSupport`). However, `prohibitedGroupContent` (Internal.hs:338) blocks voice messages when the group disables voice — with no scope exemption:
|
||||
|
||||
```haskell
|
||||
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice
|
||||
```
|
||||
|
||||
Other content types (files, reports, simplex links) already have `isNothing scopeInfo` guards that exempt them in member support scope. Voice does not.
|
||||
|
||||
This means voice captchas fail in the majority of real groups that prohibit voice messages. The check runs on both sender side (Commands.hs:3856) and recipient side (Subscriber.hs:1738), so both the bot and the joining member reject voice in these groups.
|
||||
|
||||
## 3. High-level design
|
||||
|
||||
1. **Protocol version 17** (`memberSupportVoiceVersion`): gates the `prohibitedGroupContent` exemption for host voice during the approval phase.
|
||||
|
||||
2. **Core library change** (Internal.hs): exempt voice in `prohibitedGroupContent` when sender is admin+ (host) AND the member is in the approval phase (pending status). Voice is NOT generally allowed in member support scope — only during approval, only from host.
|
||||
|
||||
3. **Directory bot change** (Service.hs): check member's protocol version and group voice settings before offering or sending voice captcha. Fall back to text/image captcha for old clients in voice-disabled groups.
|
||||
|
||||
## 4. Detailed implementation plan
|
||||
|
||||
### 4.1. Protocol.hs — add version 17
|
||||
|
||||
**File:** `src/Simplex/Chat/Protocol.hs`
|
||||
|
||||
Add to version history comment (after line 79):
|
||||
|
||||
```
|
||||
-- 17 - allow host voice messages during member approval regardless of group voice setting (2026-02-10)
|
||||
```
|
||||
|
||||
Update `currentChatVersion` (line 85):
|
||||
|
||||
```haskell
|
||||
currentChatVersion = VersionChat 17
|
||||
```
|
||||
|
||||
Add version constant (after `shortLinkDataVersion`, line 146):
|
||||
|
||||
```haskell
|
||||
-- support host voice messages during member approval regardless of group voice setting
|
||||
memberSupportVoiceVersion :: VersionChat
|
||||
memberSupportVoiceVersion = VersionChat 17
|
||||
```
|
||||
|
||||
### 4.2. Internal.hs — exempt host voice during approval phase
|
||||
|
||||
**File:** `src/Simplex/Chat/Library/Internal.hs`
|
||||
|
||||
Change function header (line 337) to bind sender's role and full membership:
|
||||
|
||||
```haskell
|
||||
prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole = userRole}} m@GroupMember {memberRole = senderRole} scopeInfo mc ft file_ sent
|
||||
```
|
||||
|
||||
Change line 338 from:
|
||||
|
||||
```haskell
|
||||
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```haskell
|
||||
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) && not hostApprovalVoice = Just GFVoice
|
||||
```
|
||||
|
||||
Add to the `where` clause:
|
||||
|
||||
```haskell
|
||||
hostApprovalVoice = senderRole >= GRAdmin && inApprovalPhase
|
||||
inApprovalPhase = case scopeInfo of
|
||||
Just (GCSIMemberSupport (Just scopeMem)) -> memberPending scopeMem
|
||||
Just (GCSIMemberSupport Nothing) -> memberPending mem
|
||||
Nothing -> False
|
||||
```
|
||||
|
||||
Note: `memberPending` returns True for both `GSMemPendingApproval` and `GSMemPendingReview`. The exemption applies to both phases — the member hasn't been fully admitted in either state.
|
||||
|
||||
**Why two cases for `inApprovalPhase`:**
|
||||
|
||||
- **Sender side** (bot sending via Commands.hs:3856): `scopeInfo = GCSIMemberSupport (Just pendingMember)` — the scope contains the pending member being supported. `memberPending pendingMember` checks their status.
|
||||
- **Receiver side** (member receiving via Subscriber.hs:1738): `scopeInfo = GCSIMemberSupport Nothing` — `Nothing` means the member's own support conversation (constructed by `mkGroupSupportChatInfo` in Internal.hs:1535). `memberPending mem` checks the local user's (receiving member's) status.
|
||||
|
||||
**Behavior matrix:**
|
||||
|
||||
| Scenario | `hostApprovalVoice` | Voice allowed? |
|
||||
|----------|---------------------|----------------|
|
||||
| Host → pending member, voice disabled | True | Yes (new) |
|
||||
| Host → approved member in support, voice disabled | False (`memberPending` = False) | No |
|
||||
| Pending member → host, voice disabled | False (`senderRole` < GRAdmin) | No |
|
||||
| Anyone outside support scope, voice disabled | False (`inApprovalPhase` = False) | No |
|
||||
| Any sender, voice enabled | N/A (`groupFeatureMemberAllowed` = True) | Yes (existing) |
|
||||
|
||||
**Version gating:** Old clients (< v17) don't have this exemption. On the sender side this is handled by the bot (4.3). On the recipient side:
|
||||
|
||||
- Old recipient + voice-disabled group: recipient rejects the voice message (shows "Voice messages: received, prohibited")
|
||||
- This is why the bot must check the member's version before sending voice
|
||||
|
||||
### 4.3. Service.hs — version-aware voice captcha logic
|
||||
|
||||
**File:** `apps/simplex-directory-service/src/Directory/Service.hs`
|
||||
|
||||
#### 4.3.1. Add import
|
||||
|
||||
Add `memberSupportVoiceVersion` to the `Protocol` import:
|
||||
|
||||
```haskell
|
||||
import Simplex.Chat.Protocol (MsgContent (..), memberSupportVoiceVersion)
|
||||
```
|
||||
|
||||
#### 4.3.2. Add helper predicate
|
||||
|
||||
Add a helper in the `directoryService` `where` block (same scope as `sendMemberCaptcha`, `sendVoiceCaptcha`, etc., where `opts` is in scope):
|
||||
|
||||
```haskell
|
||||
canSendVoiceCaptcha :: GroupInfo -> GroupMember -> Bool
|
||||
canSendVoiceCaptcha gInfo m =
|
||||
isJust (voiceCaptchaGenerator opts)
|
||||
&& (groupFeatureUserAllowed SGFVoice gInfo || supportsVersion m memberSupportVoiceVersion)
|
||||
```
|
||||
|
||||
Logic:
|
||||
- Voice captcha generator must be configured
|
||||
- AND either the group allows voice for the bot/host (any client version works — old clients accept voice from permitted senders) OR the member's client supports v17 (exemption applies on receive side)
|
||||
|
||||
Note: `groupFeatureUserAllowed` checks if the bot (group owner) is permitted to send voice. This is what the recipient's `prohibitedGroupContent` checks — it validates the *sender's* permission (`m` parameter = sender's GroupMember), not the recipient's. Using `groupFeatureMemberAllowed SGFVoice m gInfo` (joining member) would be wrong: it would incorrectly block voice captcha in groups with role-based voice settings (e.g., "admins only").
|
||||
|
||||
#### 4.3.3. Update `dePendingMember` hint text (line 572)
|
||||
|
||||
Change from:
|
||||
|
||||
```haskell
|
||||
<> if isJust (voiceCaptchaGenerator opts) then "\nSend /audio to receive a voice captcha." else ""
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```haskell
|
||||
<> if canSendVoiceCaptcha g m then "\nSend /audio to receive a voice captcha." else ""
|
||||
```
|
||||
|
||||
This hides the `/audio` hint when voice captcha cannot be delivered.
|
||||
|
||||
#### 4.3.4. Update `dePendingMemberMsg` `/audio` handling (lines 644-649)
|
||||
|
||||
When a member sends `/audio`, check `canSendVoiceCaptcha` before switching mode. If voice captcha is not possible, reply with an upgrade message:
|
||||
|
||||
```haskell
|
||||
| isAudioCmd ->
|
||||
if canSendVoiceCaptcha g m
|
||||
then case captchaMode of
|
||||
CMText -> do
|
||||
atomically $ TM.insert gmId pc {captchaMode = CMAudio} $ pendingCaptchas env
|
||||
sendVoiceCaptcha sendRef (T.unpack captchaText)
|
||||
CMAudio ->
|
||||
sendComposedMessages_ cc sendRef [(Just ciId, MCText audioAlreadyEnabled)]
|
||||
else sendComposedMessages_ cc sendRef [(Just ciId, MCText voiceCaptchaUnavailable)]
|
||||
```
|
||||
|
||||
#### 4.3.5. Add message constant
|
||||
|
||||
```haskell
|
||||
voiceCaptchaUnavailable :: Text
|
||||
voiceCaptchaUnavailable = "Voice captcha is not available - please update SimpleX Chat to v6.5+ or use text captcha."
|
||||
```
|
||||
|
||||
#### 4.3.6. Update `dePendingMemberMsg` no-captcha `/audio` path (lines 640-642)
|
||||
|
||||
Same check for the case when no pending captcha exists yet:
|
||||
|
||||
```haskell
|
||||
Nothing ->
|
||||
if isAudioCmd && canSendVoiceCaptcha g m
|
||||
then sendMemberCaptcha g m (Just ciId) noCaptcha 0 CMAudio
|
||||
else if isAudioCmd
|
||||
then sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [(Just ciId, MCText voiceCaptchaUnavailable)]
|
||||
else let mode = CMText
|
||||
in sendMemberCaptcha g m (Just ciId) noCaptcha 0 mode
|
||||
```
|
||||
|
||||
### 4.4. Tests
|
||||
|
||||
**File:** `tests/Bots/DirectoryTests.hs`
|
||||
|
||||
Update existing audio captcha tests to cover:
|
||||
1. Group with voice enabled + any client version: `/audio` works (existing behavior)
|
||||
2. Group with voice disabled + member version >= 17: `/audio` works
|
||||
3. Group with voice disabled + member version < 17: `/audio` shows unavailable message, hint is hidden
|
||||
|
||||
### 4.5. Changes summary
|
||||
|
||||
| File | Change | Lines affected |
|
||||
|------|--------|----------------|
|
||||
| `Protocol.hs` | Add v17 constant, bump `currentChatVersion` | ~4 lines added |
|
||||
| `Internal.hs` | Exempt host voice during approval phase | ~6 lines modified/added |
|
||||
| `Service.hs` | Version-aware voice captcha logic | ~15 lines modified/added |
|
||||
| `DirectoryTests.hs` | Test coverage for version gating | TBD |
|
||||
@@ -334,13 +334,22 @@ quoteContent mc qmc ciFile_
|
||||
qTextOrFile = if T.null qText then qFileName else qText
|
||||
|
||||
prohibitedGroupContent :: GroupInfo -> GroupMember -> Maybe GroupChatScopeInfo -> MsgContent -> Maybe MarkdownList -> Maybe f -> Bool -> Maybe GroupFeature
|
||||
prohibitedGroupContent gInfo@GroupInfo {membership = GroupMember {memberRole = userRole}} m scopeInfo mc ft file_ sent
|
||||
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice
|
||||
prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole = userRole}} m scopeInfo mc ft file_ sent
|
||||
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) && not hostApprovalVoice = Just GFVoice
|
||||
| isNothing scopeInfo && not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles
|
||||
| isNothing scopeInfo && isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports
|
||||
| isNothing scopeInfo && prohibitedSimplexLinks gInfo m ft = Just GFSimplexLinks
|
||||
| otherwise = Nothing
|
||||
where
|
||||
hostApprovalVoice
|
||||
| sent = userRole >= GRAdmin && sendApprovalPhase
|
||||
| otherwise = memberCategory m == GCHostMember && hostApprovalPhase
|
||||
hostApprovalPhase = case scopeInfo of
|
||||
Just (GCSIMemberSupport Nothing) -> memberStatus mem == GSMemPendingApproval
|
||||
_ -> False
|
||||
sendApprovalPhase = case scopeInfo of
|
||||
Just (GCSIMemberSupport (Just scopeMem)) -> memberStatus scopeMem == GSMemPendingApproval
|
||||
_ -> False
|
||||
-- admins cannot send reports, non-admins cannot receive reports
|
||||
badReportUser
|
||||
| sent = userRole >= GRModerator
|
||||
|
||||
@@ -77,12 +77,13 @@ import Simplex.Messaging.Version hiding (version)
|
||||
-- 14 - support sending and receiving group join rejection (2025-02-24)
|
||||
-- 15 - support specifying message scopes for group messages (2025-03-12)
|
||||
-- 16 - support short link data (2025-06-10)
|
||||
-- 17 - allow host voice messages during member approval regardless of group voice setting (2026-02-10)
|
||||
|
||||
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
|
||||
-- This indirection is needed for backward/forward compatibility testing.
|
||||
-- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code.
|
||||
currentChatVersion :: VersionChat
|
||||
currentChatVersion = VersionChat 16
|
||||
currentChatVersion = VersionChat 17
|
||||
|
||||
-- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above)
|
||||
supportedChatVRange :: VersionRangeChat
|
||||
@@ -145,6 +146,10 @@ groupKnockingVersion = VersionChat 15
|
||||
shortLinkDataVersion :: VersionChat
|
||||
shortLinkDataVersion = VersionChat 16
|
||||
|
||||
-- support host voice messages during member approval regardless of group voice setting
|
||||
memberSupportVoiceVersion :: VersionChat
|
||||
memberSupportVoiceVersion = VersionChat 17
|
||||
|
||||
agentToChatVersion :: VersionSMPA -> VersionChat
|
||||
agentToChatVersion v
|
||||
| v < pqdrSMPAgentVersion = initialChatVersion
|
||||
|
||||
@@ -1205,10 +1205,6 @@ Query: UPDATE ratchets SET ratchet_state = ? WHERE conn_id = ?
|
||||
Plan:
|
||||
SEARCH ratchets USING PRIMARY KEY (conn_id=?)
|
||||
|
||||
Query: UPDATE rcv_file_chunk_replicas SET delay = ?, retries = retries + 1, updated_at = ? WHERE rcv_file_chunk_replica_id = ?
|
||||
Plan:
|
||||
SEARCH rcv_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query: UPDATE rcv_file_chunk_replicas SET received = 1, updated_at = ? WHERE rcv_file_chunk_replica_id = ?
|
||||
Plan:
|
||||
SEARCH rcv_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
@@ -4741,14 +4741,6 @@ Query:
|
||||
Plan:
|
||||
SEARCH protocol_servers USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE rcv_files
|
||||
SET to_receive = 1, user_approved_relays = ?, updated_at = ?
|
||||
WHERE file_id = ?
|
||||
|
||||
Plan:
|
||||
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
|
||||
|
||||
Query:
|
||||
UPDATE remote_controllers
|
||||
SET ctrl_device_name = ?, dh_priv_key = ?, prev_dh_priv_key = dh_priv_key
|
||||
|
||||
@@ -73,6 +73,8 @@ directoryServiceTests = do
|
||||
it "should ask member to pass captcha screen" testCapthaScreening
|
||||
it "should send voice captcha on /audio command" testVoiceCaptchaScreening
|
||||
it "should retry with voice captcha after switching to audio mode" testVoiceCaptchaRetry
|
||||
it "should send voice captcha when voice disabled but client supports v17" testVoiceCaptchaVoiceDisabled
|
||||
it "should show unavailable message for old client in voice-disabled group" testVoiceCaptchaOldClient
|
||||
it "should reject member after too many captcha attempts" testCaptchaTooManyAttempts
|
||||
it "should respond to unknown command during captcha" testCaptchaUnknownCommand
|
||||
describe "store log" $ do
|
||||
@@ -1369,6 +1371,139 @@ testVoiceCaptchaRetry ps@TestParams {tmpPath} = do
|
||||
cath <#. "#privacy (support) 'SimpleX Directory'> sends file "
|
||||
cath <##. "use /fr 2"
|
||||
|
||||
testVoiceCaptchaVoiceDisabled :: HasCallStack => TestParams -> IO ()
|
||||
testVoiceCaptchaVoiceDisabled ps@TestParams {tmpPath} = do
|
||||
let mockScript = tmpPath </> "mock_voice_gen_vdisabled.py"
|
||||
writeFile mockScript $ unlines
|
||||
[ "#!/usr/bin/env python3",
|
||||
"import os, tempfile",
|
||||
"out = os.environ.get('VOICE_CAPTCHA_OUT')",
|
||||
"if not out:",
|
||||
" fd, out = tempfile.mkstemp(suffix='.m4a')",
|
||||
" os.close(fd)",
|
||||
"open(out, 'wb').write(b'\\x00' * 100)",
|
||||
"print(out)",
|
||||
"print(5)"
|
||||
]
|
||||
setPermissions mockScript $ setOwnerExecutable True $ setOwnerReadable True $ setOwnerWritable True emptyPermissions
|
||||
withDirectoryServiceVoiceCaptcha ps mockScript $ \superUser dsLink ->
|
||||
withNewTestChat ps "bob" bobProfile $ \bob ->
|
||||
withNewTestChat ps "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
bob #> "@'SimpleX Directory' /role 1"
|
||||
bob <# "'SimpleX Directory'> > /role 1"
|
||||
bob <## " The initial member role for the group privacy is set to member"
|
||||
bob <## "Send /'role 1 observer' to change it."
|
||||
bob <## ""
|
||||
note <- getTermLine bob
|
||||
let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note
|
||||
bob #> "@'SimpleX Directory' /filter 1 captcha"
|
||||
bob <# "'SimpleX Directory'> > /filter 1 captcha"
|
||||
bob <## " Spam filter settings for group privacy set to:"
|
||||
bob <## "- reject long/inappropriate names: disabled"
|
||||
bob <## "- pass captcha to join: enabled"
|
||||
bob <## ""
|
||||
bob <## "/'filter 1 name' - enable name filter"
|
||||
bob <## "/'filter 1 name captcha' - enable both"
|
||||
bob <## "/'filter 1 off' - disable filter"
|
||||
-- disable voice messages in the group
|
||||
bob ##> "/set voice #privacy off"
|
||||
bob <## "updated group preferences:"
|
||||
bob <## "Voice messages: off"
|
||||
-- cath (new client, supports v17 exemption) joins, /audio hint shown
|
||||
cath ##> ("/c " <> groupLink)
|
||||
cath <## "connection request sent!"
|
||||
cath <## "#privacy: joining the group..."
|
||||
cath <## "#privacy: you joined the group, pending approval"
|
||||
cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service."
|
||||
cath <## ""
|
||||
cath <## "Send captcha text to join the group privacy."
|
||||
cath <## "Send /audio to receive a voice captcha."
|
||||
captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
|
||||
-- voice captcha works despite voice being disabled (v17 host approval exemption)
|
||||
cath #> "#privacy (support) /audio"
|
||||
cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)"
|
||||
cath <#. "#privacy (support) 'SimpleX Directory'> sends file "
|
||||
cath <##. "use /fr 1"
|
||||
sendCaptcha cath captcha
|
||||
cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://"
|
||||
cath <## "#privacy: member bob (Bob) is connected"
|
||||
bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)"
|
||||
bob <## "#privacy: new member cath is connected"
|
||||
where
|
||||
sendCaptcha cath captcha = do
|
||||
cath #> ("#privacy (support) " <> captcha)
|
||||
cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
|
||||
cath <## " Correct, you joined the group privacy"
|
||||
cath <## "#privacy: you joined the group"
|
||||
|
||||
testVoiceCaptchaOldClient :: HasCallStack => TestParams -> IO ()
|
||||
testVoiceCaptchaOldClient ps@TestParams {tmpPath} = do
|
||||
let mockScript = tmpPath </> "mock_voice_gen_oldclient.py"
|
||||
writeFile mockScript $ unlines
|
||||
[ "#!/usr/bin/env python3",
|
||||
"import os, tempfile",
|
||||
"out = os.environ.get('VOICE_CAPTCHA_OUT')",
|
||||
"if not out:",
|
||||
" fd, out = tempfile.mkstemp(suffix='.m4a')",
|
||||
" os.close(fd)",
|
||||
"open(out, 'wb').write(b'\\x00' * 100)",
|
||||
"print(out)",
|
||||
"print(5)"
|
||||
]
|
||||
setPermissions mockScript $ setOwnerExecutable True $ setOwnerReadable True $ setOwnerWritable True emptyPermissions
|
||||
withDirectoryServiceVoiceCaptcha ps mockScript $ \superUser dsLink ->
|
||||
withNewTestChat ps "bob" bobProfile $ \bob ->
|
||||
withNewTestChatCfg ps testCfgVPrev "cath" cathProfile $ \cath -> do
|
||||
bob `connectVia` dsLink
|
||||
registerGroup superUser bob "privacy" "Privacy"
|
||||
bob #> "@'SimpleX Directory' /role 1"
|
||||
bob <# "'SimpleX Directory'> > /role 1"
|
||||
bob <## " The initial member role for the group privacy is set to member"
|
||||
bob <## "Send /'role 1 observer' to change it."
|
||||
bob <## ""
|
||||
note <- getTermLine bob
|
||||
let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note
|
||||
bob #> "@'SimpleX Directory' /filter 1 captcha"
|
||||
bob <# "'SimpleX Directory'> > /filter 1 captcha"
|
||||
bob <## " Spam filter settings for group privacy set to:"
|
||||
bob <## "- reject long/inappropriate names: disabled"
|
||||
bob <## "- pass captcha to join: enabled"
|
||||
bob <## ""
|
||||
bob <## "/'filter 1 name' - enable name filter"
|
||||
bob <## "/'filter 1 name captcha' - enable both"
|
||||
bob <## "/'filter 1 off' - disable filter"
|
||||
-- disable voice messages in the group
|
||||
bob ##> "/set voice #privacy off"
|
||||
bob <## "updated group preferences:"
|
||||
bob <## "Voice messages: off"
|
||||
-- cath (old client, max version < v17) joins, /audio hint NOT shown
|
||||
cath ##> ("/c " <> groupLink)
|
||||
cath <## "connection request sent!"
|
||||
cath <## "#privacy: joining the group..."
|
||||
cath <## "#privacy: you joined the group, pending approval"
|
||||
cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service."
|
||||
cath <## ""
|
||||
cath <## "Send captcha text to join the group privacy."
|
||||
captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
|
||||
-- /audio unavailable: old client can't receive voice in voice-disabled group
|
||||
cath #> "#privacy (support) /audio"
|
||||
cath <# "#privacy (support) 'SimpleX Directory'!> > cath /audio"
|
||||
cath <## " Voice captcha is not available - please update SimpleX Chat to v6.5+ or use text captcha."
|
||||
-- text captcha still works
|
||||
sendCaptcha cath captcha
|
||||
cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://"
|
||||
cath <## "#privacy: member bob (Bob) is connected"
|
||||
bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)"
|
||||
bob <## "#privacy: new member cath is connected"
|
||||
where
|
||||
sendCaptcha cath captcha = do
|
||||
cath #> ("#privacy (support) " <> captcha)
|
||||
cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
|
||||
cath <## " Correct, you joined the group privacy"
|
||||
cath <## "#privacy: you joined the group"
|
||||
|
||||
withDirectoryServiceVoiceCaptcha :: HasCallStack => TestParams -> FilePath -> (TestCC -> String -> IO ()) -> IO ()
|
||||
withDirectoryServiceVoiceCaptcha ps voiceScript test = do
|
||||
dsLink <-
|
||||
|
||||
@@ -133,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
|
||||
it "x.msg.new chat message with chat version range" $
|
||||
"{\"v\":\"1-16\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
"{\"v\":\"1-17\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
|
||||
##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
|
||||
it "x.msg.new quote" $
|
||||
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
|
||||
@@ -249,13 +249,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing
|
||||
it "x.grp.mem.new with member chat version range" $
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-16\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing
|
||||
it "x.grp.mem.intro" $
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing
|
||||
it "x.grp.mem.intro with member chat version range" $
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-16\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing
|
||||
it "x.grp.mem.intro with member restrictions" $
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
@@ -270,7 +270,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
|
||||
it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-16\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
|
||||
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
|
||||
it "x.grp.mem.info" $
|
||||
"{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
|
||||
Reference in New Issue
Block a user