Files
simplex-chat/tests/Bots/DirectoryTests.hs
T
sh 10a814694c core, ui: support SimpleX names (#7045)
* deps: bump simplexmq for ConnectTarget

* chat: migration adds simplex_name to contacts, groups, connections

Nullable TEXT column on all three tables, with partial indexes on
contacts(user_id, simplex_name) and groups(user_id, simplex_name)
for the upcoming connectPlanName lookup. connections.simplex_name
is the transient carrier from APIConnect -> XInfo handler, where
the value is copied to contacts.simplex_name at delayed create.
No reads or writes yet - column threading lands in subsequent commits.

* tests: provide namesConfig = Nothing in smpServerCfg

Follow-up to the simplexmq pin bump (ee0a45e9). The new
namesConfig :: Maybe NamesConfig field on ServerConfig (introduced
in simplexmq's namespace branch) needs to appear in the test
fixture's record literal, otherwise the test suite fails to compile
under -Werror. Disabled by default (Nothing).

* chat: thread simplexName through Contact/GroupInfo/Connection records

Adds `simplexName :: Maybe SimplexNameInfo` to the three records and
extends every SELECT path that reconstructs them to read the new
column. Decoded via eitherToMaybe . strDecode . encodeUtf8 (the
codebase's established pattern for Maybe Text -> typed-decode fields),
extracted as decodeSimplexName helper since the chain appears in
toContact / toContact' / toGroupInfo / toConnection. INSERT paths
still write Nothing - the write-side wiring lands in the next commit
(Task 7).

* deps: bump simplexmq for SimplexNameInfo FromField/ToField

* chat: cross-reference groupInfoQueryFields and getGroupAndMember_

Two helpers redundantly maintain the same g.* column list. A future
g.* addition must be applied to both sites; the cross-reference
comments flag this for maintainers. A proper refactor (reusing
groupInfoQueryFields from Connections.hs's inline SELECT) is out of
scope for this branch.

* chat: persist simplexName on prepare and connect-via-plan write paths

createPreparedContact/createPreparedGroup gain a Maybe SimplexNameInfo
parameter that they write to contacts.simplex_name / groups.simplex_name
directly. createConnection_ writes to connections.simplex_name as a
transient carrier for the connect-via-plan path. The XInfo handler
in Library/Subscriber.hs reads the connection's simplexName and passes
it to createDirectContact so the final contact row captures the name.

All current callers pass Nothing; the actual flow lights up when
APIConnectPlan accepts ConnectTarget and connectPlanName threads the
name through (later commits in this branch).

Uses the upstream ToField SimplexNameInfo (simplexmq 0b334b66) for
writes; reads continue to go via the soft-degradation helper.

* chat: APIConnectPlan accepts ConnectTarget; connectPlanName looks up by name

APIConnectPlan/Connect flip from Maybe AConnectionLink to Maybe ConnectTarget.
connectPlan dispatches CTLink -> connectPlanLink (the prior body, renamed)
and CTName -> connectPlanName (new) which looks the name up against
contacts.simplex_name and groups.simplex_name via the new
getContactBySimplexName / getGroupInfoBySimplexName store helpers.

The hit path returns the contact's / group's stored conn link from
preparedContact / preparedGroup; missing prepared state or unknown
names return CEInvalidConnReq. RSLV on-chain resolution is out of
scope for this branch -- known-name lookup is enough for conversation
display, search, and external-link share.

connLinkP_ parser is unchanged: APIConnect's preparedLink_ stays
ACreatedConnLink-shaped, and the Connect / APIConnectPlan parsers
already use inline strP for ConnectTarget without going through the
helper.

Directory.Service call sites updated to wrap their AConnectionLink in
CTLink when invoking APIConnectPlan.

* chat: surface simplexName in conversation view + JSON output

viewConnectionPlan now shows the simplex name beneath known
contacts/groups (ILPKnown, CAPKnown, CAPContactViaAddress, GLPKnown
active/prepared/deleted). The TH-derived Contact / GroupInfo JSON
instances automatically expose `simplexName` (omitted when Nothing
per defaultJSON's omitNothingFields), which unblocks client-side
display and search.

No JSON test added: there is no Contact-level JSON test module in
this codebase; coverage is already provided by defaultJSON's
omitNothingFields = True behaviour.

Server-side substring search has no existing pattern in this
codebase; client renderers index simplexName themselves once it
appears in the JSON shape.

External-link share (preferring simplex:/name… form over the raw
link when the contact has a simplexName) lands in the next commit.

* chat: share/copy output prefers simplexName when present

When a contact or group has a simplex_name stored, the share-link
render path emits the canonical simplex:/name... URI (via strEncode)
instead of the underlying connection link. Falls back to the existing
link rendering when simplexName is Nothing.

Final commit of the ConnectTarget plumbing chain: end-to-end users
can now (a) connect via @alice.simplex / #group.simplex with the
agent layer carrying the name, (b) see the simplex name on the
contact/group records and in viewConnectionPlan, (c) share the
contact using the namespace-canonical form rather than the raw URI.

* deps: bump simplexmq for boundedNonSpace + drop unused FromField

* chat: simplex_name partial indexes are UNIQUE

A simplex name is a stable, per-user identity (one name → one contact
or group). Without a unique constraint, a later writer that populates
the column twice for the same name would silently produce two matching
rows, and getContactBySimplexName/getGroupInfoBySimplexName would
return whichever the planner picks first.

Promote the partial indexes added in M20260603 to UNIQUE before any
caller wires the writes. Predicate (WHERE simplex_name IS NOT NULL)
already scopes the constraint to rows that opted in.

* chat: regen Postgres schema dump for UNIQUE simplex_name indexes

Follow-up to f71c579c. The SchemaDump test runs against SQLite only;
the parallel PostgresSchemaDump suite gates on -fclient_postgres and
a running localhost PG instance, which this environment doesn't have.
Updated the Postgres schema dump by hand to mirror the migration
change (two lines: CREATE INDEX → CREATE UNIQUE INDEX).

* chat: CESimplexNameNotFound for name lookup misses

connectPlanName now distinguishes "name not found" from "connection
link is invalid". CEInvalidConnReq's message ("Connection link is
invalid, possibly it was created in a previous version") was
misleading when a user typed @alice.simplex against a database that
simply has no contact by that name.

The two "missing prepared link" cases stay on CEInvalidConnReq —
the lookup found a row but the stored link is unusable, which is
closer to the existing semantics. The two truly-missing cases
(no contact found / no group found) move to CESimplexNameNotFound,
which also surfaces the name back to the client for a precise UX.

* chat: exclude soft-deleted contacts from idx_contacts_simplex_name

The lookup `getContactBySimplexName` (Store/Direct.hs:781) filters
`AND deleted = 0`, but the index predicate `WHERE simplex_name IS NOT
NULL` covered tombstoned rows too. Forward-compat trap: once writers
land a non-Nothing simplex_name, soft-deleting a contact would block
re-claiming its name (UNIQUE conflict) even though the lookup reports
the slot as free.

Tighten the partial-index predicate to also require deleted = 0 so the
constraint scope matches the live-lookup scope. Groups have no soft-
delete column, so their index stays as-is.

* chat: connectPlanName dispatches on nameType first

Previously the function always probed contacts.simplex_name first and
fell through to groups for NTPublicGroup misses. But the discriminator
(`@`/`#`) is embedded in the stored bytes via strEncode, so an
`#group.simplex` lookup can never match a contact row. Reorder to
case on nameType up front, saving one DB query and one withFastStore
transaction acquire on the group path.

* chat: CESimplexNameUnprepared for found-but-no-link cases + member-removed filter

connectPlanName previously threw CEInvalidConnReq when a name lookup
hit a contact / group row whose preparedContact / preparedGroup was
NULL. The error message ("Connection link is invalid, possibly it was
created in a previous version") was wrong: the name resolved fine,
the device just has no link material to reconnect via (typical for a
contact created via the XInfo handler rather than the prepare path).
Introduce CESimplexNameUnprepared SimplexNameInfo for this case.

Also mirror the link-based path's gPlan (Commands.hs:4133) for groups
whose membership state is GSMemRemoved — return CESimplexNameNotFound
rather than GLPKnown for a removed-member group, since GLPKnown for
removed members would be inconsistent with how /_connect plan over a
short link handles the same situation.

* chat: fix stale CRITICAL comment in saveConnInfo

Comment claimed SEDBException is re-thrown as CRITICAL but only
SEDBBusyError is (via the `critical` helper at Subscriber.hs:136
and the showCritical branch at :1695). Updated to describe the actual
behaviour.

* chat: fix misleading decodeSimplexName docstring

The comment described "@alice.simplex" as the column's surface form,
but ToField SimplexNameInfo writes the canonical strEncode output
("simplex:/name@alice.simplex"). Aligns the docstring with what the
column actually holds.

* chat: drop redundant T.unpack in CESimplexName* error rendering

plain has a Text instance (verified by sibling simplexNameLine at
View.hs:2176 which uses it directly). The T.unpack in the new error
renderings was inconsistent with the same-feature helper. Cosmetic
cleanup.

* deps: bump simplexmq for resolveSimplexName

* chat: simplexName field on Profile, GroupProfile, LocalProfile

Adds a Maybe SimplexNameInfo field to the wire-level Profile and
GroupProfile (and their DB sibling LocalProfile). JSON instances are
TH-derived with omitNothingFields = True, so the new optional field is
auto-handled and old peers / old JSON without the key decode as Nothing.

Existing record-construction sites are set to simplexName = Nothing as
a placeholder. Outgoing dissemination (userProfileDirect /
userProfileInGroup) and incoming persistence wire-up land in follow-up
commits. redactedMemberProfile passes the field through, matching how
peerType is preserved.

* chat: load LocalProfile.simplexName from simplex_name column

Populate the embedded LocalProfile.simplexName field for the user's own
profile and for peer Contact / GroupInfo from the existing simplex_name
columns on contacts and groups. Previously every DB read set this field
to Nothing (Task 1 placeholder), so downstream consumers that work off
LocalProfile / GroupProfile (e.g., userProfileDirect / userProfileInGroup
that build outgoing XInfo / XGrpInfo via fromLocalProfile) saw Nothing
unconditionally.

Scope is limited to the rows where the simplex_name column actually
exists: contacts (per-user) and groups (per-user). Sites that only read
contact_profiles / group_profiles (toContactRequest, toContactProfile,
toGroupProfile, rowToLocalProfile) remain Nothing; Task 3 adds the
profile-table columns and wires them up.

* chat: test outgoing Profile carries simplexName from User profile

userProfileDirect, userProfileInGroup' and redactedMemberProfile already
pass simplexName through via fromLocalProfile (Task 1) once the embedded
LocalProfile field is populated (previous commit). Lock that behavior in
with focused unit tests:

- userProfileDirect with Just simplexName -> wire Profile.simplexName Just
- userProfileDirect with Nothing -> wire Nothing
- userProfileDirect with an incognito Profile overlay -> wire Nothing
  (incognito identity must not leak the user's registered name)
- userProfileInGroup' pass-through
- redactedMemberProfile pass-through (forwarded member profiles)

* chat: clarify groups.simplex_name stopgap comment

Drop the asymmetric toContact comment (the mirror is now obvious post-Task-1)
and rewrite the toGroupInfo stopgap comment to reflect the actual semantics:
groups.simplex_name is per-user locally-known, mirrored into groupProfile as a
stopgap until group_profiles.simplex_name lands.

* chat: add simplex_name to contact_profiles and group_profiles

Adds nullable simplex_name TEXT column and partial UNIQUE
(user_id, simplex_name) index to both contact_profiles and group_profiles
tables. Distinct from contacts.simplex_name / groups.simplex_name
(M20260603), which carry the user's locally-known label set by the
prepare-via-name path; the new columns will carry the peer's broadcast
claim received via XInfo / XGrpInfo (wired up in following commits).

* chat: persist peer-claimed simplexName from incoming profiles

Write paths:
- updateContactProfile_' / updateGroupProfile_ now set the new
  contact_profiles.simplex_name / group_profiles.simplex_name columns
  from Profile.simplexName / GroupProfile.simplexName respectively.
- createContact_ INSERT writes Profile.simplexName to the new
  contact_profiles column (separate from the existing simplexName arg,
  which still writes contacts.simplex_name — the user's locally-known
  label).

Read paths (closing Task 2's deferred sites):
- toContact splits simplex_name reads: Contact.simplexName from
  contacts.simplex_name (existing); LocalProfile.simplexName from
  contact_profiles.simplex_name (new column).
- toGroupInfo similarly splits: GroupInfo.simplexName from
  groups.simplex_name; groupProfile.simplexName from
  group_profiles.simplex_name.
- ProfileRow / rowToLocalProfile, toContactRequest, getUserContactProfiles,
  toGroupProfile, getProfileById, groupMemberQuery, getGroupAndMember_,
  saveRcvChatItem-related quotes — all extended to read p.simplex_name
  and decode it into LocalProfile.simplexName / GroupProfile.simplexName.

Conflict handling (Decision B):
- clearConflictingContactProfileSimplexName_ / *Group* helpers do an
  atomic UPDATE-with-RETURNING that NULLs simplex_name on any other
  row in the same user that would collide on the partial UNIQUE index,
  returning the displaced row's display_name.
- updateContactProfileWithConflict / updateGroupProfileWithConflict
  bundle clear+update in one transaction.
- processContactProfileUpdate / xGrpInfo invoke the *WithConflict
  variants and emit CEvtSimplexNameConflict when a displacement
  happened (with the claiming and displaced display names).

Adds ChatEvent CEvtSimplexNameConflict and SimplexNameConflictEntity
(SNCEContact / SNCEGroup) with JSON instances and View.hs rendering.

* chat: fix review findings on simplex_name persistence

- updateUserProfile no longer writes contact_profiles.simplex_name on
  the user's own row (the column is reserved for peer claims; the user's
  broadcastable name lives on contacts.simplex_name via uct.simplex_name).
- updateMemberContactProfile_'/Reset_' now write simplex_name; new
  updateMemberProfileWithConflict / updateContactMemberProfileWithConflict
  variants run conflict-clear and return the displaced name, with
  processMemberProfileUpdate emitting CEvtSimplexNameConflict.
- createContact_ runs conflict-clear before INSERT to avoid UNIQUE
  constraint violations on first-write peer collisions, returning the
  displaced name; createPreparedContact / createDirectContact thread it
  through to APIPrepareContact and saveConnInfo XInfo for event emission.
- groups conflict-clear takes ProfileId directly (avoids the NOT IN (NULL)
  silent-noop edge case when groups.group_profile_id is ON DELETE SET NULL).
- Moves clearConflictingContactProfileSimplexName_ to Shared.hs so
  createContact_ can call it without inducing a circular import.

* chat: resolveOnUserServers iterates user SMP servers for RSLV

* chat: connectPlanName falls back to RSLV when local lookup misses

* chat: RSLV-resolved NameRecord dispatched through prepared row

dispatchResolvedRecord now picks the first nrContactLinks (NTContact) or
nrChannelLinks (NTPublicGroup) entry from the resolved record, decodes it
as AConnShortLink, fetches the short-link data, and eagerly calls
createPreparedContact / createPreparedGroup with the simplex_name set.

Returning CPContactAddress (CAPKnown ct) / CPGroupLink (GLPKnown g ...)
mirrors the local-store-hit branch of connectPlanName: hit and miss
converge on the same plan shape, so the connectWithPlan caller cannot
distinguish where the prepared row came from. Threading uses the
existing Maybe SimplexNameInfo parameter added in c6f26150 for the
local-prepare path -- no new write path or transient carrier.

Pure helper firstNameLink is extracted and exported so the link-picker
contract is testable without a DB / agent. ResolveNameTests gains five
cases covering the per-type selection, the first-link policy, and the
empty-list to CESimplexNameNotFound collapse.

* chat: regen query plans after simplex_name plumbing

* chat: register ConnectTarget + CEvtSimplexNameConflict in bot docs

Bot API docs generator failed with "Undefined type: ConnectTarget"
since f2394d121 (prior plan) flipped APIConnectPlan/Connect from
Maybe AConnectionLink to Maybe ConnectTarget without updating
bots/src/API/Docs/*. Also adds SimplexNameConflictEntity (new in
cd0de9659) and documents the CEvtSimplexNameConflict event for
peer-name displacement notifications. Regenerates the affected
markdown / TypeScript / Python artefacts.

* deps: bump simplexmq for NameRecord reshape; update consumers

simplexmq 5ee014dd reshaped NameRecord to align with the Python resolver
JSON: nrChannelLinks/nrContactLinks (lists of NameLink) became
nrSimplexChannel/nrSimplexContact (Maybe Text); nrDisplayName became
nrName; nrResolver was added; the NameLink wrapper type and nrIsTest/
nrExpiry/nrAdminAddress/nrAdminEmail fields were dropped.

Update dispatchResolvedRecord destructure and firstNameLink signature
to the new Maybe Text shape, and refresh the ResolveNameTests fixtures
and assertions accordingly.

* chat: resolveOnUserServers iterates only on transport errors

Privacy: every miss previously broadcast the candidate name to every
enabled SMP server. Now only NETWORK / TIMEOUT failures fall through
to the next server; definite resolver answers (NAME / AUTH / CMD
PROHIBITED / other ERR) stop iteration.

* chat: document why groups simplex_name index has no soft-delete filter

The contacts simplex_name index filters on (deleted = 0); the groups
index has no analogous filter because the groups table has no `deleted`
column. Groups are hard-deleted by deleteGroup, so the asymmetry is
intentional. The remaining "removed member, row retained" edge case is
flagged in the lookup comment for follow-up.

* chat: document connections.simplex_name as transient carrier

Audit flagged the column as "INSERTed but never UPDATEd". This is by
design per the prior plan's connect-via-plan flow: the column is a
transient carrier between connection-creation and contact-creation.
After the Contact row is created via XInfo handling, contacts.simplex_name
is the source of truth and the connections value is a historical snapshot.
Documents the intent so future readers don't reflag it.

* chat: extract surfaceSimplexNameConflict helper

Six call sites duplicated the same forM_ ((,) <$> claim <*> displaced)
shape emitting CEvtSimplexNameConflict. Extract to a single helper so
future call sites don't drift on whether to emit, and so the conflict
event shape change (post-Task-3 SimplexNameConflictEntity split into
SNCEContact / SNCEGroup) propagates through one site.

* chat: APIVerifySimplexName command + CEvtSimplexNameUnverified warning

Addresses the TOFU vulnerability where peer-claimed simplex_name was
accepted unverified. Adds:

- contacts.simplex_name_verified_at + groups.simplex_name_verified_at
  (M20260606_simplex_name_verified)
- APIVerifySimplexName ChatRef command: RSLV-resolves the claimed name
  and compares the resolved link to the peer's stored connection link;
  on match writes verified_at and emits CEvtSimplexNameVerified;
  on mismatch emits CEvtSimplexNameVerifyFailed
- CEvtSimplexNameUnverified passive warning emitted on incoming XInfo /
  XGrpInfo when a name claim arrives without a current verification
- updateContactProfileWithConflict / updateGroupProfileWithConflict
  clear simplex_name_verified_at whenever the peer's claim transitions
  (any value change including Nothing<->Just): the prior verification
  was bound to the prior claim.

UI can surface the unverified indicator next to a contact / group's
name, and prompt the user to invoke the verify command. This shifts
the security model from "TOFU + last-writer-wins" to "TOFU + on-demand
RSLV verification".

* chat: register APIVerifySimplexName + verify events in bot docs

ebe90f716 added the verify command + events + SimplexNameVerifyFailReason
type without touching bots/src/API/Docs/. Mirrors commit 0d7ea8061 which
addressed the same gap for ConnectTarget. Regenerates the affected
markdown / TypeScript / Python artefacts.

* chat: bump simplexmq pin + document cross-table simplex_name discriminator

Pin bump 5ee014dd -> c9c2d19 picks up the 8 simplexmq commits since the
last bump (parseBare lowercase fix, forwarded-param cleanup, ServerTests
+ agent end-to-end tests, TldRegistries removal, SNRC ABI decoder,
NameRecord/NameOwner module extraction).

Adds a brief comment on clearConflictingContactProfileSimplexName_
explaining why the audit's flagged cross-table collision (between
contact_profiles.simplex_name and group_profiles.simplex_name) is
structurally impossible: SimplexNameInfo's strEncode prefixes contact
names with '@' and group names with '#', so the stored bytes never
overlap between the two tables.

Query-plan regen deferred (the test is non-deterministic in CI / dev
sandbox — see prior 6c990696c).

* deps: bump simplexmq for HTTP resolver; adapt NameRecord consumers

simplexmq 92b3d049 reshaped NameRecord text fields from Maybe Text to
Text (empty string sentinel). Adapt firstNameLink to take Text directly
and treat T.null as "absent". dispatchResolvedRecord destructure
unchanged; passes the text values straight through. apiVerifySimplexName
switches from Just/Nothing pattern to a T.null guard with the same UX.
Test fixtures updated.

* deps: bump simplexmq for multi-link NameRecord; adapt consumers

* core: treat RSLV CMD UNKNOWN as no name-resolution support

* core: fix contact-by-connection query missing simplex_name_verified_at

* deps: update sha256map for simplexmq f555e9af pin

* core: iterate past RSLV-unsupported name servers

* core: filter RSLV servers by operator enablement

* core: align resolver docs/tests with RSLV errors

* deps: bump simplexmq to df1aa24c

* refactor(names): agent resolution + one error type

Adopt the simplexmq names rework (PR #7045): name resolution is now
owned by the agent (resolveSimplexName picks a names-role server), so
the chat-side iteration is removed - delete ResolveError,
iterateResolvers, resolveOnUserServers, enabledSMPServersForUser and
resolveErrorToChatError.

One error type: resolver/agent failures flow through ChatErrorAgent;
remove the CEvtSimplexName* events, SimplexNameVerifyFailReason,
SimplexNameConflictEntity and CESimplexNameResolverUnavailable.
APIVerifySimplexName returns CRSimplexNameVerified (verified::Bool),
mirroring CRConnectionVerified. connectPlan handles the name target
directly; updateProfile WithConflict aliases collapsed into the plain
functions.

Add the per-operator "names" SMP server role (migration
20260612_smp_role_names, official operator on by default) feeding
ServerRoles.names -> UserServers.nameSrvs.

Bump simplexmq pin to ce69adfd and regenerate sha256map.nix.

* fix(store): match chat_schema.sql to sqlite 3.46+ indent

The schema-dump test renders the partial-index WHERE via the sqlite3
CLI; sqlite >=3.46 wraps a multi-condition WHERE onto two lines
("IS NOT NULL" + indented "AND ...") where 3.45 kept it on one. The
committed schema was generated with 3.45, so CI (newer sqlite) failed
the comparison on idx_contacts_simplex_name. Regenerated with the
newer formatter; only that one WHERE clause changes.

* feat(operators): warn when no server resolves names

Mirror USWNoChatRelays: validateUserServers emits USWNoNamesServers
when no enabled server of an enabled operator carries the SMP names
role. noNamesServersWarns is self-contained with local predicates,
matching the sibling noChatRelaysWarns; noServersErrs is untouched.

* test(operators): expect USWNoNamesServers warning in no-servers cases

* fix(store): single-line simplex_name WHERE to match CI sqlite (<=3.45)

* chore: bump simplexmq pin to 6843b14c

* refactor(store): consolidate names migrations into one

Unshipped feature - merge the four incremental simplex_name migrations
(0603/0604/0606/0612) into a single M20260603_simplex_name. The combined
UP applies the ALTERs/indexes in the same order, so the resulting schema
is byte-identical (verified by SchemaDump on SQLite and pg_dump on Postgres).

* update simplexmq

* plan for name resolution

* update types and schema

* simpler resolution, name proofs

* simplexmq

* generate bot types, schema, unStrJSON, fix tests

* ad hoc link comparison, create short link

* update simplexmq

* remove same link, use simplexmq instead

* split verify API for contacts and public groups

* remove comment

* simplify warnings

* test: remove unused

* remove cute language

* type name

* remove spurious comments

* refactor setting user name

* refactor setting user name

* remove trivial tests

* refactor

* remove tests using pre-short-link addresses

* rename

* move names

* bots api

* refactor more

* refactor

* refactor to another type

* load own names

* read short links when looking up by name

* renames

* refactor verification

* mapM_

* update api types

* change field for name

* rename columns

* api types, schema

* renames

* fix links

* simplify

* remove comments

* rename fields

* simplify

* remove proof from channel addres

* refactor

* name resolution test

* change tests

* fix tests

* fix tests

* fix plan for names

* test

* test verification status

* bot api

* fix tests

* update bot api types

* query plan

* add api for setting public group access

* android, desktop, ios: connect via SimpleX name (#7068)

* android, desktop, ios: connect via SimpleX name

* android, desktop, ios: open known contact on name lookup; surface prepared contact

Name search opens the contact (not list-filter); resolved/prepared contacts and groups are added to the chat list so they're visible and openable. Kotlin compile-verified; iOS edits pattern-matched, pending Xcode build.

* feat(names): UI names role + agent NAME error

Parity with the core names rework (#7045):

- Add `names` to ServerRoles (Android + iOS) and a per-operator
  "To resolve names" toggle under the SMP section (xftp has no names
  role; the shared ServerRoles field stays false there).
- Mirror the new agent error: NameErrorType + a NAME case on both
  AgentErrorType and ProtocolErrorType (the SMP ErrorType mirror), so
  the new SMP/agent NAME errors decode instead of crashing the decoder.
- Remove ChatErrorType.SimplexNameResolverUnavailable (deleted in core)
  and repoint its "name resolution unavailable" alert to the agent
  NAME NO_SERVERS error, reusing the existing strings.

Android (multiplatform) compiles clean; iOS mirrors the same changes
(builds in Xcode).

* feat(names): UI warning when no server resolves names

Mirror core USWNoNamesServers: add the NoNamesServers variant to
UserServersWarning (Kotlin sealed class + Swift enum) and its
globalWarning / globalServersWarning branch, rendered by the existing
ServersWarningFooter / ServersWarningView. Matches the noChatRelays
warning exactly.

* fix(servers): show all validation errors and warnings, not just the first

globalServersError/Warning returned only the first entry, so a second
warning (e.g. no names servers behind no chat relays) or a second error
(e.g. no XFTP servers behind no SMP servers) was never displayed. Make
them return all entries (globalServersErrors/Warnings) and render one
footer row each, across the three combined-footer views. Per-protocol
SMP/XFTP footers are unchanged.

* docs(names): add SimpleX name UI plan

* feat(names): add name model fields + SimplexName helpers

* feat(names): verify + set-name API & responses

* docs(names): bump core sync to 5008b4e62

* feat(names): show name + verification on chat info

* feat(names): add Verify SimpleX names privacy toggle

* feat(names): add set-name screens (user + channel)

* update ui

* fix kotlin

* fix codable

* fix ios

* fix errors

* api in UI

* send name as string in protocol

* update simplexmq, capitalize

* verify that name is in profile for own and known contacts and channels as condition of name resolution

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>

* add log

* bot types

* finalize renames, ui alerts

* more renames

* kotlin alerts

* show set name

* alerts

* texts, icons, footers

* move JSON parsing to a persistent thread with large stack

* simplexmq

* use uikit alerts

* remove comment

* move name verification to more privacy

* change icon on verify names setting

* verify name proof

* nix shas

* show verified names

* better alert when saving names

* revert breaking field name change

* simplexmq

* clean up

* remove unused

* remove empty line

* fix error

* simplify

* use domain without prefix in profiles and in database, rename fields and types

* rename types

* fix JSON name

* pass verified domain to prepare API

* fix prepare api

* fix ios encoding

* enable name resolution via Flux servers

* fix test

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-07-03 12:51:52 +01:00

2315 lines
120 KiB
Haskell

{-# LANGUAGE CPP #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}
module Bots.DirectoryTests where
import ChatClient
import ChatTests.DBUtils
import ChatTests.Groups (memberJoinChannel, prepareChannel1Relay)
import ChatTests.Utils
import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Exception (finally)
import Control.Monad (forM_, when)
import qualified Data.Aeson as J
import qualified Data.Text as T
import Directory.Captcha
import Directory.Listing
import Directory.Options
import Directory.Service
import Directory.Store
import System.Directory (emptyPermissions, setOwnerExecutable, setOwnerReadable, setOwnerWritable, setPermissions)
import System.IO (hClose)
import Simplex.Chat.Bot.KnownContacts
import Simplex.Chat.Controller (ChatConfig (..))
import qualified Simplex.Chat.Markdown as MD
import Simplex.Chat.Options (CoreChatOpts (..))
import Simplex.Chat.Options.DB
import Simplex.Chat.Protocol (memberSupportVoiceVersion)
import Simplex.Chat.Types (ChatPeerType (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Messaging.Version
import System.FilePath ((</>))
import Test.Hspec hiding (it)
directoryServiceTests :: SpecWith TestParams
directoryServiceTests = do
it "should register group" testDirectoryService
it "should suspend and resume group, send message to owner" testSuspendResume
it "should delete group registration" testDeleteGroup
it "admin should delete group registration" testDeleteGroupAdmin
it "should change initial member role" testSetRole
it "should join found group via link" testJoinGroup
it "should support group names with spaces" testGroupNameWithSpaces
it "should return more groups in search, all and recent groups" testSearchGroups
it "should invite to owners' group if specified" testInviteToOwnersGroup
it "should re-invite owner who left owners' group" testInviteOwnerAfterLeavingOwnersGroup
describe "de-listing the group" $ do
it "should de-list if owner leaves the group" testDelistedOwnerLeaves
it "should de-list if owner is removed from the group" testDelistedOwnerRemoved
it "should NOT de-list if another member leaves the group" testNotDelistedMemberLeaves
it "should NOT de-list if another member is removed from the group" testNotDelistedMemberRemoved
it "should de-list if service is removed from the group" testDelistedServiceRemoved
it "should de-list if group is deleted" testDelistedGroupDeleted
it "should de-list/re-list when service/owner roles change" testDelistedRoleChanges
it "should NOT de-list if another member role changes" testNotDelistedMemberRoleChanged
it "should NOT send to approval if roles are incorrect" testNotSentApprovalBadRoles
it "should NOT allow approving if roles are incorrect" testNotApprovedBadRoles
describe "should require re-approval if profile is changed by" $ do
it "the registration owner" testRegOwnerChangedProfile
it "another owner" testAnotherOwnerChangedProfile
it "another owner not connected to directory" testNotConnectedOwnerChangedProfile
describe "should require profile update if group link is removed by " $ do
it "the registration owner" testRegOwnerRemovedLink
it "another owner" testAnotherOwnerRemovedLink
it "another owner not connected to directory" testNotConnectedOwnerRemovedLink
describe "duplicate groups (same display name and full name)" $ do
it "should ask for confirmation if a duplicate group is submitted" testDuplicateAskConfirmation
it "should prohibit registration if a duplicate group is listed" testDuplicateProhibitRegistration
it "should prohibit confirmation if a duplicate group is listed" testDuplicateProhibitConfirmation
it "should prohibit when profile is updated and not send for approval" testDuplicateProhibitWhenUpdated
it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval
describe "list and promote groups" $ do
it "should list and promote user's groups" $ testListUserGroups True
describe "member admission" $ do
it "should require captcha by default for new groups" testCaptchaByDefault
it "should require captcha in all groups with --always-captcha" testAlwaysCaptcha
it "should require admin review in all groups with --knocking" testKnocking
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
it "should restore directory service state" testRestoreDirectory
describe "captcha" $ do
it "should accept some incorrect spellings" testCaptcha
it "should generate captcha of correct length" testGetCaptchaStr
describe "help commands" $ do
it "should not list audio command" testHelpNoAudio
it "should reject audio command in DM" testAudioCommandInDM
describe "public group registration" $ do
it "should register channel via shared link card" testRegisterChannelViaCard
it "should suggest share via chat when link sent as text" testLinkAsTextSearch
it "should reject card shared by non-owner" testNonOwnerSharesCard
it "should delete channel registration and leave" testDeleteChannelRegistration
it "should handle re-registration when already listed" testReregistrationAlreadyListed
it "should update subscriber count periodically" testLinkCheckUpdatesCount
directoryProfile :: Profile
directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing}
mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> Maybe FilePath -> DirectoryOpts
mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder =
DirectoryOpts
{ coreOptions =
testCoreOpts
{ dbOptions =
(dbOptions testCoreOpts)
#if defined(dbPostgres)
{dbSchemaPrefix = "client_" <> serviceDbPrefix}
#else
{dbFilePrefix = ps </> serviceDbPrefix}
#endif
},
adminUsers = [],
superUsers,
ownersGroup,
noAddress = False,
blockedFragmentsFile = Nothing,
blockedWordsFile = Nothing,
blockedExtensionRules = Nothing,
nameSpellingFile = Nothing,
profileNameLimit = maxBound,
captchaGenerator = Nothing,
voiceCaptchaGenerator = Nothing,
directoryLog = Just $ ps </> "directory_service.log",
migrateDirectoryLog = Nothing,
serviceName = "SimpleX Directory",
clientService = True,
runCLI = False,
searchResults = 3,
webFolder,
linkCheckInterval = 0,
prohibitedToObserver = False,
alwaysCaptcha = False,
knocking = False,
testing = True
}
serviceDbPrefix :: FilePath
serviceDbPrefix = "directory_service"
viewName :: String -> String
viewName = T.unpack . MD.viewName . T.pack
testDirectoryService :: HasCallStack => TestParams -> IO ()
testDirectoryService ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
bob #> "@'SimpleX Directory' privacy"
bob <# "'SimpleX Directory'> > privacy"
bob <## " No groups found"
-- putStrLn "*** create a group"
bob ##> "/g PSA Privacy, Security & Anonymity"
bob <## "group #PSA (Privacy, Security & Anonymity) is created"
bob <## "to add members use /a PSA <name> or /create link #PSA"
bob ##> "/a PSA 'SimpleX Directory' member"
bob <## "invitation to join the group #PSA sent to 'SimpleX Directory'"
bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group"
bob ##> "/mr PSA 'SimpleX Directory' admin"
-- putStrLn "*** discover service joins group and creates the link for profile"
bob <## "#PSA: you changed the role of 'SimpleX Directory' to admin"
bob <# "'SimpleX Directory'> Joining the group PSA…"
bob <## "#PSA: 'SimpleX Directory' joined the group"
bob <# "'SimpleX Directory'> Joined the group PSA, creating the link…"
bob <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online."
bob <## ""
bob <## "Please add it to the group welcome message."
bob <## "For example, add:"
welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob
bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them."
bob <## "Captcha verification is enabled. Use /'filter 1' to change it."
-- putStrLn "*** update profile without link"
updateGroupProfile bob "Welcome!"
bob <# "'SimpleX Directory'> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message."
(superUser </)
-- putStrLn "*** update profile so that it has link"
updateGroupProfile bob welcomeWithLink
bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
approvalRequested superUser welcomeWithLink (1 :: Int)
-- putStrLn "*** update profile so that it still has link"
let welcomeWithLink' = "Welcome! " <> welcomeWithLink
updateGroupProfile bob welcomeWithLink'
bob <# "'SimpleX Directory'> The group ID 1 (PSA) is updated!"
bob <## "It is hidden from the directory until approved."
superUser <# "'SimpleX Directory'> The group ID 1 (PSA) is updated."
approvalRequested superUser welcomeWithLink' (2 :: Int)
-- putStrLn "*** try approving with the old registration code"
bob #> "@'SimpleX Directory' /approve 1:PSA 1"
bob <# "'SimpleX Directory'> > /approve 1:PSA 1"
bob <## " You are not allowed to use this command"
superUser #> "@'SimpleX Directory' /approve 1:PSA 1"
superUser <# "'SimpleX Directory'> > /approve 1:PSA 1"
superUser <## " Incorrect approval code"
-- putStrLn "*** update profile so that it has no link"
updateGroupProfile bob "Welcome!"
bob <# "'SimpleX Directory'> The group link for ID 1 (PSA) is removed from the welcome message."
bob <## ""
bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (PSA), de-listed."
superUser #> "@'SimpleX Directory' /approve 1:PSA 2"
superUser <# "'SimpleX Directory'> > /approve 1:PSA 2"
superUser <## " Error: the group ID 1 (PSA) is not pending approval."
-- putStrLn "*** update profile so that it has link again"
updateGroupProfile bob welcomeWithLink'
bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message."
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
approvalRequested superUser welcomeWithLink' (1 :: Int)
superUser #> "@'SimpleX Directory' /pending"
superUser <# "'SimpleX Directory'> > /pending"
superUser <## " 1 registered group(s)"
superUser <# "'SimpleX Directory'> 1. PSA (Privacy, Security & Anonymity)"
superUser <## "Welcome message:"
superUser <##. "Welcome! Link to join the group PSA: "
superUser <## "Owner: bob"
superUser <## "2 members"
superUser <## "Status: pending admin approval"
superUser <## "/'role 1', /'filter 1'"
superUser #> "@'SimpleX Directory' /approve 1:PSA 1"
superUser <# "'SimpleX Directory'> > /approve 1:PSA 1"
superUser <## " Group approved!"
bob <# "'SimpleX Directory'> The group ID 1 (PSA) is approved and listed in directory - please moderate it!"
bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
bob <## ""
bob <## "Supported commands:"
bob <## "/'filter 1' - to configure anti-spam filter."
bob <## "/'role 1' - to set default member role."
bob <## "/'link 1' - to view/upgrade group link."
search bob "privacy" welcomeWithLink'
search bob "security" welcomeWithLink'
cath `connectVia` dsLink
search cath "privacy" welcomeWithLink'
bob #> "@'SimpleX Directory' /exec /contacts"
bob <# "'SimpleX Directory'> > /exec /contacts"
bob <## " You are not allowed to use this command"
superUser #> "@'SimpleX Directory' /exec /contacts"
superUser <# "'SimpleX Directory'> > /exec /contacts"
superUser <## " alice (Alice)"
superUser <## "bob (Bob)"
superUser <## "cath (Catherine)"
where
search u s welcome = do
u #> ("@'SimpleX Directory' " <> s)
u <# ("'SimpleX Directory'> > " <> s)
u <## " Found 1 group(s)."
u <# "'SimpleX Directory'> PSA (Privacy, Security & Anonymity)"
u <## "Welcome message:"
u <## welcome
u <## "2 members"
updateGroupProfile u welcome = do
u ##> ("/set welcome #PSA " <> welcome)
u <## "welcome message changed to:"
u <## welcome
approvalRequested su welcome grId = do
su <# "'SimpleX Directory'> bob submitted the group ID 1:"
su <## "PSA (Privacy, Security & Anonymity)"
su <## "Welcome message:"
su <## welcome
su <## "2 members"
su <## ""
su <## "To approve send:"
su <# ("'SimpleX Directory'> /approve 1:PSA " <> show grId)
testSuspendResume :: HasCallStack => TestParams -> IO ()
testSuspendResume ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
groupFound bob "privacy"
superUser #> "@'SimpleX Directory' /suspend 1:privacy"
superUser <# "'SimpleX Directory'> > /suspend 1:privacy"
superUser <## " Group suspended!"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is suspended and hidden from directory. Please contact the administrators."
groupNotFound bob "privacy"
superUser #> "@'SimpleX Directory' /resume 1:privacy"
superUser <# "'SimpleX Directory'> > /resume 1:privacy"
superUser <## " Group listing resumed!"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is listed in the directory again!"
groupFound bob "privacy"
superUser #> "@'SimpleX Directory' privacy"
groupFoundN_ "" (Just 1) 2 superUser "privacy"
superUser #> "@'SimpleX Directory' /link 1:privacy"
superUser <# "'SimpleX Directory'> > /link 1:privacy"
superUser <## " The link to join the group ID 1 (privacy):"
superUser <##. "https://localhost/g#"
superUser <## "New member role: member"
-- get and change the link to the equivalent - should not ask to re-approve
bob #> "@'SimpleX Directory' /link 1"
bob <# "'SimpleX Directory'> > /link 1"
bob <## " The link to join the group ID 1 (privacy):"
gLink <- getTermLine bob
gLink `shouldStartWith` "https://localhost/g#"
bob <## "New member role: member"
bob ##> "/show welcome #privacy"
bob <## "Welcome message:"
bob <## ("Link to join the group privacy: " <> gLink)
bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink <> "?same_link=true")
bob <## "welcome message changed to:"
bob <## ("Link to join the group privacy: " <> gLink <> "?same_link=true")
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
bob <## "The group is listed in directory."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes."
superUser <## "The group remained listed in directory."
#if !defined(dbPostgres)
-- upgrade link
-- make it upgradeable first
superUser #> "@'SimpleX Directory' /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL"
superUser <# "'SimpleX Directory'> > /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL"
superUser <## ""
bob #> "@'SimpleX Directory' /link 1"
bob <# "'SimpleX Directory'> > /link 1"
bob <## " The link to join the group ID 1 (privacy):"
bob <##. "https://simplex.chat/contact#/"
bob <## "New member role: member"
bob <## "The link is being upgraded..."
bob <# "'SimpleX Directory'> Please replace the old link in welcome message of your group ID 1 (privacy)"
bob <## "If this is the only change, the group will remain listed in directory without re-approval."
bob <## ""
bob <## "The new link:"
gLink' <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob
bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink')
bob <## "welcome message changed to:"
bob <## ("Link to join the group privacy: " <> gLink')
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
bob <## "The group is listed in directory."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes."
superUser <## "The group remained listed in directory."
-- send message to group owner
superUser #> "@'SimpleX Directory' /owner 1:privacy hello there"
superUser <# "'SimpleX Directory'> > /owner 1:privacy hello there"
superUser <## " Forwarded to @bob, the owner of the group ID 1 (privacy)"
bob <# "'SimpleX Directory'> hello there"
#endif
testDeleteGroup :: HasCallStack => TestParams -> IO ()
testDeleteGroup ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
groupFound bob "privacy"
bob #> "@'SimpleX Directory' /delete 1:privacy"
bob <# "'SimpleX Directory'> > /delete 1:privacy"
bob <## " Your group privacy is deleted from the directory"
groupNotFound bob "privacy"
testDeleteGroupAdmin :: HasCallStack => TestParams -> IO ()
testDeleteGroupAdmin ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
cath `connectVia` dsLink
registerGroupId superUser cath "security" "Security" 2 1
groupFound bob "privacy"
groupFound bob "security"
listUserGroup bob "privacy" "Privacy"
listUserGroup cath "security" "Security"
superUser #> "@'SimpleX Directory' /last"
superUser <# "'SimpleX Directory'> > /last"
superUser <## " 2 registered group(s)"
memberGroupListing superUser bob 1 "privacy" "Privacy" 2 "active"
memberGroupListing superUser cath 2 "security" "Security" 2 "active"
-- trying to register group with the same name
submitGroup bob "security" "Security"
bob <# "'SimpleX Directory'> The group security (Security) is already listed in the directory, please choose another name."
bob ##> "/d #security"
bob <## "#security: you deleted the group"
-- admin can delete the group
superUser #> "@'SimpleX Directory' /delete 2:security"
superUser <# "'SimpleX Directory'> > /delete 2:security"
superUser <## " The group security is deleted from the directory"
groupFound cath "privacy"
listUserGroup bob "privacy" "Privacy"
groupNotFound bob "security"
sendListCommand cath 0
-- another user can register the group with the same name
registerGroupId superUser bob "security" "Security" 4 2
testSetRole :: HasCallStack => TestParams -> IO ()
testSetRole ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
groupFound bob "privacy"
bob #> "@'SimpleX Directory' /role 1:privacy observer"
bob <# "'SimpleX Directory'> > /role 1:privacy observer"
bob <## " The initial member role for the group privacy is set to observer"
bob <## ""
note <- getTermLine bob
let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note
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
cath #> ("#privacy (support) " <> captcha)
cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
cath <## " Correct, you joined the group privacy"
cath <## "#privacy: you joined the group"
cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://localhost/g#"
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"
bob ##> "/ms #privacy"
bob <## "bob (Bob): owner, you, created group"
bob <## "'SimpleX Directory': admin, invited, connected"
bob <## "cath (Catherine): observer, connected"
cath ##> "#privacy hello"
cath <## "#privacy: you don't have permission to send messages"
testJoinGroup :: HasCallStack => TestParams -> IO ()
testJoinGroup ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
withNewTestChat ps "cath" cathProfile $ \cath ->
withNewTestChat ps "dan" danProfile $ \dan -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
cath `connectVia` dsLink
cath #> "@'SimpleX Directory' privacy"
cath <# "'SimpleX Directory'> > privacy"
cath <## " Found 1 group(s)."
cath <# "'SimpleX Directory'> privacy (Privacy)"
cath <## "Welcome message:"
welcomeMsg <- getTermLine cath
let groupLink = dropStrPrefix "Link to join the group privacy: " welcomeMsg
cath <## "2 members"
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_1'> Captcha is generated by SimpleX Directory service."
cath <## ""
cath <## "Send captcha text to join the group privacy."
captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory_1'> " . dropTime <$> getTermLine cath
cath #> ("#privacy (support) " <> captcha)
cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
cath <## "use @'SimpleX Directory' <message> to send messages"
cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
cath <## " Correct, you joined the group privacy"
cath <## "#privacy: you joined the group"
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"
bob ##> "/create link #privacy"
bobLink <- getGroupLink bob "privacy" GRMember True
dan ##> ("/c " <> bobLink)
dan <## "connection request sent!"
concurrentlyN_
[ do
bob <## "dan (Daniel): accepting request to join group #privacy..."
bob <## "#privacy: dan joined the group",
do
dan <## "#privacy: joining the group..."
dan <## "#privacy: you joined the group"
dan <# ("#privacy bob> " <> welcomeMsg)
dan
<### [ "#privacy: member 'SimpleX Directory' is connected",
"#privacy: member cath (Catherine) is connected"
],
do
cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)"
cath <## "#privacy: new member dan is connected"
]
testGroupNameWithSpaces :: HasCallStack => TestParams -> IO ()
testGroupNameWithSpaces ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
bob `connectVia` dsLink
registerGroup superUser bob "Privacy & Security" ""
groupFound bob "Privacy & Security"
superUser #> "@'SimpleX Directory' /suspend 1:'Privacy & Security'"
superUser <# "'SimpleX Directory'> > /suspend 1:'Privacy & Security'"
superUser <## " Group suspended!"
bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is suspended and hidden from directory. Please contact the administrators."
groupNotFound bob "privacy"
superUser #> "@'SimpleX Directory' /resume 1:'Privacy & Security'"
superUser <# "'SimpleX Directory'> > /resume 1:'Privacy & Security'"
superUser <## " Group listing resumed!"
bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is listed in the directory again!"
groupFound bob "Privacy & Security"
testSearchGroups :: HasCallStack => TestParams -> IO ()
testSearchGroups ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
cath `connectVia` dsLink
forM_ [1..8 :: Int] $ \i -> registerGroupId superUser bob (groups !! (i - 1)) "" i i
connectUsers bob cath
fullAddMember "MyGroup" "" bob cath GRMember
joinGroup "MyGroup" cath bob
cath <## "#MyGroup: member 'SimpleX Directory_1' is connected"
cath <## "contact and member are merged: 'SimpleX Directory', #MyGroup 'SimpleX Directory_1'"
cath <## "use @'SimpleX Directory' <message> to send messages"
cath #> "@'SimpleX Directory' MyGroup"
cath <# "'SimpleX Directory'> > MyGroup"
cath <## " Found 7 group(s), sending top 3."
receivedGroup cath 0 3
receivedGroup cath 1 2
receivedGroup cath 2 2
cath <# "'SimpleX Directory'> Send /next for 4 more result(s)."
cath #> "@'SimpleX Directory' /next"
cath <# "'SimpleX Directory'> > /next"
cath <## " Sending 3 more group(s)."
receivedGroup cath 3 2
receivedGroup cath 4 2
receivedGroup cath 5 2
cath <# "'SimpleX Directory'> Send /next for 1 more result(s)."
-- search of another user does not affect the search of the first user
groupFound bob "Another"
cath #> "@'SimpleX Directory' ."
cath <# "'SimpleX Directory'> > ."
cath <## " Sending 1 more group(s)."
receivedGroup cath 6 2
cath #> "@'SimpleX Directory' /all"
cath <# "'SimpleX Directory'> > /all"
cath <## " 8 group(s) listed, sending top 3."
receivedGroup cath 0 3
receivedGroup cath 1 2
receivedGroup cath 2 2
cath <# "'SimpleX Directory'> Send /next for 5 more result(s)."
cath #> "@'SimpleX Directory' /new"
cath <# "'SimpleX Directory'> > /new"
cath <## " 8 group(s) listed, sending the most recent 3."
receivedGroup cath 7 2
receivedGroup cath 6 2
receivedGroup cath 5 2
cath <# "'SimpleX Directory'> Send /next for 5 more result(s)."
cath #> "@'SimpleX Directory' term3"
cath <# "'SimpleX Directory'> > term3"
cath <## " Found 3 group(s)."
receivedGroup cath 4 2
receivedGroup cath 5 2
receivedGroup cath 6 2
cath #> "@'SimpleX Directory' term1"
cath <# "'SimpleX Directory'> > term1"
cath <## " Found 6 group(s), sending top 3."
receivedGroup cath 1 2
receivedGroup cath 2 2
receivedGroup cath 3 2
cath <# "'SimpleX Directory'> Send /next for 3 more result(s)."
cath #> "@'SimpleX Directory' ."
cath <# "'SimpleX Directory'> > ."
cath <## " Sending 3 more group(s)."
receivedGroup cath 4 2
receivedGroup cath 5 2
receivedGroup cath 6 2
where
groups :: [String]
groups =
[ "MyGroup",
"MyGroup term1 1",
"MyGroup term1 2",
"MyGroup term1 term2",
"MyGroup term1 term2 term3",
"MyGroup term1 term2 term3 term4",
"MyGroup term1 term2 term3 term4 term5",
"Another"
]
receivedGroup :: TestCC -> Int -> Int -> IO ()
receivedGroup u ix count = do
u <#. ("'SimpleX Directory'> " <> groups !! ix)
u <## "Welcome message:"
u <##. "Link to join the group "
u <## (show count <> " members")
testInviteToOwnersGroup :: HasCallStack => TestParams -> IO ()
testInviteToOwnersGroup ps =
withDirectoryServiceCfgOwnersGroup ps testCfg True Nothing $ \superUser dsLink ->
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> do
bob `connectVia` dsLink
registerGroupId superUser bob "privacy" "Privacy" 2 1
bob <## "#owners: 'SimpleX Directory' invites you to join the group as member"
bob <## "use /j owners to accept"
superUser <## "Invited @bob, the owner of the group ID 2 (privacy) to owners' group owners"
bob ##> "/j owners"
bob <## "#owners: you joined the group"
bob <## "#owners: member alice (Alice) is connected"
superUser <## "#owners: 'SimpleX Directory' added bob (Bob) to the group (connecting...)"
superUser <## "#owners: new member bob is connected"
-- second group
registerGroupId superUser bob "security" "Security" 3 2
superUser <## "Owner is already a member of owners' group"
testInviteOwnerAfterLeavingOwnersGroup :: HasCallStack => TestParams -> IO ()
testInviteOwnerAfterLeavingOwnersGroup ps =
withDirectoryServiceCfgOwnersGroup ps testCfg True Nothing $ \superUser dsLink ->
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> do
bob `connectVia` dsLink
registerGroupId superUser bob "privacy" "Privacy" 2 1
bob <## "#owners: 'SimpleX Directory' invites you to join the group as member"
bob <## "use /j owners to accept"
superUser <## "Invited @bob, the owner of the group ID 2 (privacy) to owners' group owners"
bob ##> "/j owners"
bob <## "#owners: you joined the group"
bob <## "#owners: member alice (Alice) is connected"
superUser <## "#owners: 'SimpleX Directory' added bob (Bob) to the group (connecting...)"
superUser <## "#owners: new member bob is connected"
-- owner leaves owners' group; GroupMember row keeps status GSMemLeft
leaveGroup "owners" bob
superUser <## "#owners: bob left the group"
-- owners' group has no GroupReg, so directory service notifies admins on contact left
superUser <# "'SimpleX Directory'> Error: contact left, group: 1 owners, group registration not found"
-- super-user re-invites via /invite — must send a fresh invitation, not "already a member"
superUser #> "@'SimpleX Directory' /invite 2:privacy"
superUser <# "'SimpleX Directory'> > /invite 2:privacy"
superUser <## " you invited @bob, the owner of the group ID 2 (privacy) to owners' group owners"
bob <## "#owners_1: 'SimpleX Directory' invites you to join the group as member"
bob <## "use /j owners_1 to accept"
testDelistedOwnerLeaves :: HasCallStack => TestParams -> IO ()
testDelistedOwnerLeaves ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
leaveGroup "privacy" bob
cath <## "#privacy: bob left the group"
bob <# "'SimpleX Directory'> You left the group ID 1 (privacy)."
bob <## ""
bob <## "The group is no longer listed in the directory."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner left)."
cath `connectVia` dsLink
cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
cath <## "use @'SimpleX Directory' <message> to send messages"
groupNotFound cath "privacy"
testDelistedOwnerRemoved :: HasCallStack => TestParams -> IO ()
testDelistedOwnerRemoved ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
removeMember "privacy" cath bob
bob <# "'SimpleX Directory'> You are removed from the group ID 1 (privacy)."
bob <## ""
bob <## "The group is no longer listed in the directory."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner is removed)."
cath `connectVia` dsLink
cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
cath <## "use @'SimpleX Directory' <message> to send messages"
groupNotFound cath "privacy"
testNotDelistedMemberLeaves :: HasCallStack => TestParams -> IO ()
testNotDelistedMemberLeaves ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
leaveGroup "privacy" cath
bob <## "#privacy: cath left the group"
(superUser </)
cath `connectVia` dsLink
cath #> "@'SimpleX Directory_1' privacy"
groupFoundN_ "_1" Nothing 2 cath "privacy"
testNotDelistedMemberRemoved :: HasCallStack => TestParams -> IO ()
testNotDelistedMemberRemoved ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
removeMember "privacy" bob cath
(superUser </)
cath `connectVia` dsLink
cath #> "@'SimpleX Directory_1' privacy"
groupFoundN_ "_1" Nothing 2 cath "privacy"
testDelistedServiceRemoved :: HasCallStack => TestParams -> IO ()
testDelistedServiceRemoved ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
bob ##> "/rm #privacy 'SimpleX Directory'"
bob <## "#privacy: you removed 'SimpleX Directory' from the group"
cath <## "#privacy: bob removed 'SimpleX Directory' from the group"
bob <# "'SimpleX Directory'> SimpleX Directory is removed from the group ID 1 (privacy)."
bob <## ""
bob <## "The group is no longer listed in the directory."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (directory service is removed)."
cath `connectVia` dsLink
groupNotFound_ "_1" cath "privacy"
testDelistedGroupDeleted :: HasCallStack => TestParams -> IO ()
testDelistedGroupDeleted ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
cath `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
connectUsers bob cath
fullAddMember "privacy" "Privacy" bob cath GROwner
joinGroup "privacy" cath bob
cath <## "#privacy: member 'SimpleX Directory_1' is connected"
cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
cath <## "use @'SimpleX Directory' <message> to send messages"
bob ##> "/d #privacy"
bob <## "#privacy: you deleted the group"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is deleted."
bob <## ""
bob <## "The group is no longer listed in the directory."
cath <## "#privacy: bob deleted the group"
cath <## "use /d #privacy to delete the local copy of the group"
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group is deleted)."
groupNotFound cath "privacy"
testDelistedRoleChanges :: HasCallStack => TestParams -> IO ()
testDelistedRoleChanges ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
cath `connectVia` dsLink
cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
cath <## "use @'SimpleX Directory' <message> to send messages"
groupFoundN 3 cath "privacy"
-- de-listed if service role changed
bob ##> "/mr privacy 'SimpleX Directory' member"
bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
cath <## "#privacy: bob changed the role of 'SimpleX Directory' from admin to member"
bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to member."
bob <## ""
bob <## "The group is no longer listed in the directory."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (SimpleX Directory role is changed to member)."
groupNotFound cath "privacy"
-- re-listed if service role changed back without profile changes
cath ##> "/mr privacy 'SimpleX Directory' admin"
cath <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
bob <## "#privacy: cath changed the role of 'SimpleX Directory' from member to admin"
bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
bob <## ""
bob <## "The group is listed in the directory again."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (SimpleX Directory role is changed to admin)."
groupFoundN 3 cath "privacy"
-- de-listed if owner role changed
cath ##> "/mr privacy bob admin"
cath <## "#privacy: you changed the role of bob to admin"
bob <## "#privacy: cath changed your role from owner to admin"
bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to admin."
bob <## ""
bob <## "The group is no longer listed in the directory."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (user role is set to admin)."
groupNotFound cath "privacy"
-- re-listed if owner role changed back without profile changes
cath ##> "/mr privacy bob owner"
cath <## "#privacy: you changed the role of bob to owner"
bob <## "#privacy: cath changed your role from admin to owner"
bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to owner."
bob <## ""
bob <## "The group is listed in the directory again."
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (user role is set to owner)."
groupFoundN 3 cath "privacy"
testNotDelistedMemberRoleChanged :: HasCallStack => TestParams -> IO ()
testNotDelistedMemberRoleChanged ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
cath `connectVia` dsLink
cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
cath <## "use @'SimpleX Directory' <message> to send messages"
groupFoundN 3 cath "privacy"
bob ##> "/mr privacy cath member"
bob <## "#privacy: you changed the role of cath to member"
cath <## "#privacy: bob changed your role from owner to member"
groupFoundN 3 cath "privacy"
testNotSentApprovalBadRoles :: HasCallStack => TestParams -> IO ()
testNotSentApprovalBadRoles ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
cath `connectVia` dsLink
submitGroup bob "privacy" "Privacy"
welcomeWithLink <- groupAccepted bob "privacy" 1
bob ##> "/mr privacy 'SimpleX Directory' member"
bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
updateProfileWithLink bob "privacy" welcomeWithLink 1
bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group"
bob ##> "/mr privacy 'SimpleX Directory' admin"
bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
bob <## ""
bob <## "The group is submitted for approval."
notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
groupNotFound cath "privacy"
approveRegistration superUser bob "privacy" 1
groupFound cath "privacy"
testNotApprovedBadRoles :: HasCallStack => TestParams -> IO ()
testNotApprovedBadRoles ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
cath `connectVia` dsLink
submitGroup bob "privacy" "Privacy"
welcomeWithLink <- groupAccepted bob "privacy" 1
updateProfileWithLink bob "privacy" welcomeWithLink 1
notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
bob ##> "/mr privacy 'SimpleX Directory' member"
bob <## "#privacy: you changed the role of 'SimpleX Directory' to member"
let approve = "/approve 1:privacy 1"
superUser #> ("@'SimpleX Directory' " <> approve)
superUser <# ("'SimpleX Directory'> > " <> approve)
superUser <## " Group is not approved: SimpleX Directory is not an admin."
groupNotFound cath "privacy"
bob ##> "/mr privacy 'SimpleX Directory' admin"
bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin"
bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin."
bob <## ""
bob <## "The group is submitted for approval."
notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1
approveRegistration superUser bob "privacy" 1
groupFound cath "privacy"
testRegOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
testRegOwnerChangedProfile ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
bob ##> "/gp privacy privacy Privacy and Security"
bob <## "description changed to: Privacy and Security"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
bob <## "It is hidden from the directory until approved."
cath <## "bob updated group #privacy:"
cath <## "description changed to: Privacy and Security"
cath `connectVia` dsLink
cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
cath <## "use @'SimpleX Directory' <message> to send messages"
groupNotFound cath "privacy"
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated."
reapproveGroup 3 superUser bob
groupFoundN 3 cath "privacy"
testAnotherOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
testAnotherOwnerChangedProfile ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
cath `connectVia` dsLink
cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
cath <## "use @'SimpleX Directory' <message> to send messages"
cath ##> "/gp privacy privacy Privacy and Security"
cath <## "description changed to: Privacy and Security"
bob <## "cath updated group #privacy:"
bob <## "description changed to: Privacy and Security"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!"
bob <## "It is hidden from the directory until approved."
groupNotFound cath "privacy"
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath."
reapproveGroup 3 superUser bob
groupFoundN 3 cath "privacy"
testNotConnectedOwnerChangedProfile :: HasCallStack => TestParams -> IO ()
testNotConnectedOwnerChangedProfile ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
withNewTestChat ps "dan" danProfile $ \dan -> do
bob `connectVia` dsLink
dan `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
cath ##> "/gp privacy privacy Privacy and Security"
cath <## "description changed to: Privacy and Security"
bob <## "cath updated group #privacy:"
bob <## "description changed to: Privacy and Security"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!"
bob <## "It is hidden from the directory until approved."
groupNotFound dan "privacy"
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath."
reapproveGroup 3 superUser bob
groupFoundN 3 dan "privacy"
testRegOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
testRegOwnerRemovedLink ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
bob ##> "/show welcome #privacy"
bob <## "Welcome message:"
welcomeWithLink <- getTermLine bob
bob ##> "/set welcome #privacy Welcome!"
bob <## "welcome message changed to:"
bob <## "Welcome!"
bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message."
bob <## ""
bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
cath <## "bob updated group #privacy:"
cath <## "welcome message changed to:"
cath <## "Welcome!"
superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
cath `connectVia` dsLink
cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
cath <## "use @'SimpleX Directory' <message> to send messages"
groupNotFound cath "privacy"
let withChangedLink = T.unpack $ T.replace "contact#/?v=2-7&" "contact#/?v=3-7&" $ T.pack welcomeWithLink
bob ##> ("/set welcome #privacy " <> withChangedLink)
bob <## "welcome message changed to:"
bob <## withChangedLink
bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message."
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
cath <## "bob updated group #privacy:"
cath <## "welcome message changed to:"
cath <## withChangedLink
reapproveGroup 3 superUser bob
groupFoundN 3 cath "privacy"
testAnotherOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
testAnotherOwnerRemovedLink ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
cath `connectVia` dsLink
cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'"
cath <## "use @'SimpleX Directory' <message> to send messages"
bob ##> "/show welcome #privacy"
bob <## "Welcome message:"
welcomeWithLink <- getTermLine bob
cath ##> "/set welcome #privacy Welcome!"
cath <## "welcome message changed to:"
cath <## "Welcome!"
bob <## "cath updated group #privacy:"
bob <## "welcome message changed to:"
bob <## "Welcome!"
bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath."
bob <## ""
bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
groupNotFound cath "privacy"
cath ##> ("/set welcome #privacy " <> welcomeWithLink)
cath <## "welcome message changed to:"
cath <## welcomeWithLink
bob <## "cath updated group #privacy:"
bob <## "welcome message changed to:"
bob <## welcomeWithLink
bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath."
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
reapproveGroup 3 superUser bob
groupFoundN 3 cath "privacy"
testNotConnectedOwnerRemovedLink :: HasCallStack => TestParams -> IO ()
testNotConnectedOwnerRemovedLink ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
withNewTestChat ps "dan" danProfile $ \dan -> do
bob `connectVia` dsLink
dan `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
addCathAsOwner bob cath
bob ##> "/show welcome #privacy"
bob <## "Welcome message:"
welcomeWithLink <- getTermLine bob
cath ##> "/set welcome #privacy Welcome!"
cath <## "welcome message changed to:"
cath <## "Welcome!"
bob <## "cath updated group #privacy:"
bob <## "welcome message changed to:"
bob <## "Welcome!"
bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath."
bob <## ""
bob <## "The group is hidden from the directory until the group link is added and the group is re-approved."
superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed."
groupNotFound dan "privacy"
cath ##> ("/set welcome #privacy " <> welcomeWithLink)
cath <## "welcome message changed to:"
cath <## welcomeWithLink
bob <## "cath updated group #privacy:"
bob <## "welcome message changed to:"
bob <## welcomeWithLink
-- bob <# "'SimpleX Directory'> The group link is added by another group member, your registration will not be processed."
-- bob <## ""
-- bob <## "Please update the group profile yourself."
-- bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!")
-- bob <## "welcome message changed to:"
-- bob <## (welcomeWithLink <> " - welcome!")
bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath."
bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
-- cath <## "bob updated group #privacy:"
-- cath <## "welcome message changed to:"
-- cath <## (welcomeWithLink <> " - welcome!")
reapproveGroup 3 superUser bob
groupFoundN 3 dan "privacy"
testDuplicateAskConfirmation :: HasCallStack => TestParams -> IO ()
testDuplicateAskConfirmation ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
submitGroup bob "privacy" "Privacy"
_ <- groupAccepted bob "privacy" 1
cath `connectVia` dsLink
submitGroup cath "privacy" "Privacy"
cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
cath <## "To confirm the registration, please send:"
cath <# "'SimpleX Directory'> /confirm 1:privacy"
cath #> "@'SimpleX Directory' /confirm 1:privacy"
welcomeWithLink <- groupAccepted cath "privacy" 1
groupNotFound bob "privacy"
completeRegistrationId superUser cath "privacy" "Privacy" welcomeWithLink 2 1
groupFound bob "privacy"
testDuplicateProhibitRegistration :: HasCallStack => TestParams -> IO ()
testDuplicateProhibitRegistration ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
cath `connectVia` dsLink
groupFound cath "privacy"
_ <- submitGroup cath "privacy" "Privacy"
cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."
testDuplicateProhibitConfirmation :: HasCallStack => TestParams -> IO ()
testDuplicateProhibitConfirmation ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
submitGroup bob "privacy" "Privacy"
welcomeWithLink <- groupAccepted bob "privacy" 1
cath `connectVia` dsLink
submitGroup cath "privacy" "Privacy"
cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
cath <## "To confirm the registration, please send:"
cath <# "'SimpleX Directory'> /confirm 1:privacy"
groupNotFound cath "privacy"
completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
groupFound cath "privacy"
cath #> "@'SimpleX Directory' /confirm 1:privacy"
cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."
testDuplicateProhibitWhenUpdated :: HasCallStack => TestParams -> IO ()
testDuplicateProhibitWhenUpdated ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
submitGroup bob "privacy" "Privacy"
welcomeWithLink <- groupAccepted bob "privacy" 1
cath `connectVia` dsLink
submitGroup cath "privacy" "Privacy"
cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
cath <## "To confirm the registration, please send:"
cath <# "'SimpleX Directory'> /confirm 1:privacy"
cath #> "@'SimpleX Directory' /confirm 1:privacy"
welcomeWithLink' <- groupAccepted cath "privacy" 1
groupNotFound cath "privacy"
completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
groupFound cath "privacy"
cath ##> ("/set welcome privacy " <> welcomeWithLink')
cath <## "welcome message changed to:"
cath <## welcomeWithLink'
cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name."
cath ##> "/gp privacy security Security"
cath <## "changed to #security (Security)"
cath <# "'SimpleX Directory'> Thank you! The group link for ID 1 (security) is added to the welcome message."
cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2
approveRegistrationId superUser cath "security" 2 1
groupFound bob "security"
groupFound cath "security"
testDuplicateProhibitApproval :: HasCallStack => TestParams -> IO ()
testDuplicateProhibitApproval ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
submitGroup bob "privacy" "Privacy"
welcomeWithLink <- groupAccepted bob "privacy" 1
cath `connectVia` dsLink
submitGroup cath "privacy" "Privacy"
cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory."
cath <## "To confirm the registration, please send:"
cath <# "'SimpleX Directory'> /confirm 1:privacy"
cath #> "@'SimpleX Directory' /confirm 1:privacy"
welcomeWithLink' <- groupAccepted cath "privacy" 1
updateProfileWithLink cath "privacy" welcomeWithLink' 1
notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2
groupNotFound cath "privacy"
completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1
groupFound cath "privacy"
-- fails at approval, as already listed
let approve = "/approve 2:privacy 1"
superUser #> ("@'SimpleX Directory' " <> approve)
superUser <# ("'SimpleX Directory'> > " <> approve)
superUser <## " The group ID 2 (privacy) is already listed in the directory."
testListUserGroups :: HasCallStack => Bool -> TestParams -> IO ()
testListUserGroups promote ps =
withDirectoryServiceCfgOwnersGroup ps testCfg False (Just "./tests/tmp/web") $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
cath `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
checkListings ["privacy"] []
connectUsers bob cath
fullAddMember "privacy" "Privacy" bob cath GRMember
joinGroup "privacy" cath bob
cath <## "#privacy: member 'SimpleX Directory_1' is connected"
cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'"
cath <## "use @'SimpleX Directory' <message> to send messages"
registerGroupId superUser bob "security" "Security" 2 2
checkListings ["privacy", "security"] []
registerGroupId superUser cath "anonymity" "Anonymity" 3 1
checkListings ["privacy", "security", "anonymity"] []
listUserGroup cath "anonymity" "Anonymity"
-- with de-listed group
groupFound cath "anonymity"
cath ##> "/mr anonymity 'SimpleX Directory' member"
cath <## "#anonymity: you changed the role of 'SimpleX Directory' to member"
cath <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (anonymity) is changed to member."
cath <## ""
cath <## "The group is no longer listed in the directory."
superUser <# "'SimpleX Directory'> The group ID 3 (anonymity) is de-listed (SimpleX Directory role is changed to member)."
checkListings ["privacy", "security"] []
groupNotFound cath "anonymity"
listGroups superUser bob cath
when promote $ do
superUser #> "@'SimpleX Directory' /promote 1:privacy on"
superUser <# "'SimpleX Directory'> > /promote 1:privacy on"
superUser <## " Group promotion enabled."
checkListings ["privacy", "security"] ["privacy"]
bob ##> "/gp privacy privacy"
bob <## "description removed"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!"
bob <## "It is hidden from the directory until approved."
cath <## "bob updated group #privacy:"
cath <## "description removed"
superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated."
superUser <# "'SimpleX Directory'> bob submitted the group ID 1:"
superUser <## "privacy"
superUser <## "Welcome message:"
superUser <##. "Link to join the group privacy: https://localhost/g#"
superUser <## "3 members"
superUser <## ""
superUser <## "To approve send:"
superUser <# "'SimpleX Directory'> /approve 1:privacy 1 promote=on"
checkListings ["security"] []
superUser #> "@'SimpleX Directory' /approve 1:privacy 1"
superUser <# "'SimpleX Directory'> > /approve 1:privacy 1"
superUser <## " Group approved (promoted)!"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!"
bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
bob <## ""
bob <## "Supported commands:"
bob <## "/'filter 1' - to configure anti-spam filter."
bob <## "/'role 1' - to set default member role."
bob <## "/'link 1' - to view/upgrade group link."
checkListings ["privacy", "security"] ["privacy"]
checkListings :: HasCallStack => [T.Text] -> [T.Text] -> IO ()
checkListings listed promoted = do
threadDelay 100000
checkListing listingFileName listed
checkListing promotedFileName promoted
where
checkListing f expected = do
Just (DirectoryListing gs) <- J.decodeFileStrict $ "./tests/tmp/web/data" </> f
map groupName gs `shouldBe` expected
groupName DirectoryEntry {displayName} = displayName
testAlwaysCaptcha :: HasCallStack => TestParams -> IO ()
testAlwaysCaptcha ps =
withDirectoryServiceOpts ps (\o -> o {alwaysCaptcha = True}) $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
-- disable the per-group captcha filter; --always-captcha must still force it
bob #> "@'SimpleX Directory' /filter 1 off"
bob <# "'SimpleX Directory'> > /filter 1 off"
bob <## " Spam filter settings for group privacy set to:"
bob <## "- reject long/inappropriate names: disabled"
bob <## "- pass captcha to join: disabled"
bob <## ""
bob <## "/'filter 1 name' - enable name filter"
bob <## "/'filter 1 captcha' - enable captcha challenge"
bob <## "/'filter 1 name captcha' - enable both"
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
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
cath #> ("#privacy (support) " <> captcha)
cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
cath <## " Correct, you joined the group privacy"
cath <## "#privacy: you joined the group"
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"
testKnocking :: HasCallStack => TestParams -> IO ()
testKnocking ps =
withDirectoryServiceOpts ps (\o -> o {knocking = True}) $ \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
cath ##> ("/c " <> groupLink)
cath <## "connection request sent!"
cath <## "#privacy: joining the group..."
cath <## "#privacy: you joined the group, connecting to group moderators for admission to group"
cath <## "#privacy: 'SimpleX Directory' accepted you to the group, pending review"
bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting and pending review...), use /_accept member #1 3 <role> to accept member"
testCaptchaByDefault :: HasCallStack => TestParams -> IO ()
testCaptchaByDefault ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
-- the owner never ran /filter; captcha is on by default for new groups
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
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
cath #> ("#privacy (support) " <> captcha)
cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
cath <## " Correct, you joined the group privacy"
cath <## "#privacy: you joined the group"
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"
testCapthaScreening :: HasCallStack => TestParams -> IO ()
testCapthaScreening ps =
withDirectoryService ps $ \superUser dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob ->
withNewTestChat ps "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
registerGroup superUser bob "privacy" "Privacy"
-- check default role
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
-- connect with captcha screen
_ <- join cath groupLink
cath #> "#privacy (support) 123" -- sending incorrect captcha
cath <# "#privacy (support) 'SimpleX Directory'!> > cath 123"
cath <## " Incorrect text, please try again."
captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
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"
cath #> "#privacy hello"
bob <# "#privacy cath> hello"
cath ##> "/l privacy"
cath <## "#privacy: you left the group"
cath <## "use /d #privacy to delete the group"
bob <## "#privacy: cath left the group"
cath ##> "/d #privacy"
cath <## "#privacy: you deleted the group"
-- change default role to observer
bob #> "@'SimpleX Directory' /role 1 observer"
bob <# "'SimpleX Directory'> > /role 1 observer"
bob <## " The initial member role for the group privacy is set to observer"
bob <## ""
bob <##. "Please note: it applies only to members joining via this link: https://"
-- connect with captcha screen again, as observer
captcha' <- join cath groupLink
sendCaptcha cath captcha'
-- message from cath that left
pastMember <- dropStrPrefix "#privacy: 'SimpleX Directory' forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath
cath <# ("#privacy " <> pastMember <> "> hello [>>]")
cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://"
cath <## "#privacy: member bob (Bob) is connected"
bob <## "#privacy: 'SimpleX Directory' added cath_1 (Catherine) to the group (connecting...)"
bob <## "#privacy: new member cath_1 is connected"
cath ##> "#privacy hello"
cath <## "#privacy: you don't have permission to send messages"
(bob </)
cath ##> "/ms privacy"
cath <## "cath (Catherine): observer, you, connected"
cath <## "'SimpleX Directory': admin, host, connected"
cath <## "bob (Bob): owner, connected"
cath <## (pastMember <> ": author, status unknown")
where
join cath groupLink = do
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."
dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
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"
testVoiceCaptchaScreening :: HasCallStack => TestParams -> IO ()
testVoiceCaptchaScreening ps@TestParams {tmpPath} = do
let mockScript = tmpPath </> "mock_voice_gen.py"
-- Mock script writes a dummy audio file, prints path and duration
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"
-- get group link
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
-- cath joins, receives text captcha with /audio hint
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
-- cath requests audio captcha
cath #> "#privacy (support) /audio"
cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)"
cath <#. "#privacy (support) 'SimpleX Directory'> sends file "
cath <##. "use /fr 1"
-- cath sends /audio again, already enabled
cath #> "#privacy (support) /audio"
cath <# "#privacy (support) 'SimpleX Directory'!> > cath /audio"
cath <## " Audio captcha is already enabled."
-- send correct captcha
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"
testVoiceCaptchaRetry :: HasCallStack => TestParams -> IO ()
testVoiceCaptchaRetry ps@TestParams {tmpPath} = do
let mockScript = tmpPath </> "mock_voice_gen_retry.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
-- cath joins, receives text captcha with /audio hint
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."
_ <- getTermLine cath -- captcha image/text
-- cath requests audio captcha
cath #> "#privacy (support) /audio"
cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)"
cath <#. "#privacy (support) 'SimpleX Directory'> sends file "
cath <##. "use /fr 1"
-- cath sends WRONG answer after switching to audio mode
cath #> "#privacy (support) wrong_answer"
cath <# "#privacy (support) 'SimpleX Directory'!> > cath wrong_answer"
cath <## " Incorrect text, please try again."
-- KEY ASSERTION: retry sends BOTH image and voice because captchaMode=CMAudio
_ <- getTermLine cath -- captcha image/text
cath <# "#privacy (support) 'SimpleX Directory'> voice message (00:05)"
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
-- 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 testCfg {chatVRange = (chatVRange testCfg) {maxVersion = prevVersion memberSupportVoiceVersion}} "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
-- 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"
withDirectoryServiceOpts :: HasCallStack => TestParams -> (DirectoryOpts -> DirectoryOpts) -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryServiceOpts ps modOpts test = do
dsLink <-
withNewTestChatCfg ps testCfg serviceDbPrefix directoryProfile $ \ds ->
withNewTestChatCfg ps testCfg "super_user" aliceProfile $ \superUser -> do
connectUsers ds superUser
ds ##> "/ad"
getContactLink ds True
let opts = modOpts $ mkDirectoryOpts ps [KnownContact 2 "alice"] Nothing Nothing
runDirectory testCfg opts $
withTestChatCfg ps testCfg "super_user" $ \superUser -> do
superUser <## "subscribed 1 connections on server localhost"
test superUser dsLink
withDirectoryServiceVoiceCaptcha :: HasCallStack => TestParams -> FilePath -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryServiceVoiceCaptcha ps voiceScript =
withDirectoryServiceOpts ps (\o -> o {voiceCaptchaGenerator = Just voiceScript})
testRestoreDirectory :: HasCallStack => TestParams -> IO ()
testRestoreDirectory ps = do
testListUserGroups False ps
restoreDirectoryService ps 11 $ \superUser _dsLink ->
withTestChat ps "bob" $ \bob ->
withTestChat ps "cath" $ \cath -> do
bob <## "subscribed 5 connections on server localhost"
cath <## "subscribed 5 connections on server localhost"
listGroups superUser bob cath
groupFoundN 3 bob "privacy"
groupFound bob "security"
groupFoundN 3 cath "privacy"
cath #> "@'SimpleX Directory' security"
groupFoundN' 2 cath "security"
testCaptcha :: HasCallStack => TestParams -> IO ()
testCaptcha _ps = do
let captcha = "23456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghijkmnpqrty"
matchCaptchaStr captcha captcha `shouldBe` True
matchCaptchaStr captcha "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" `shouldBe` True
matchCaptchaStr "23456789ABcDEFGH1JKLMNoPQRsTuvwxYzabdefghijkmnpqrty" captcha `shouldBe` True
matchCaptchaStr "OOIICPSUVWXZ" "OOIICPSUVWXZ" `shouldBe` True
matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxz" `shouldBe` True
matchCaptchaStr "0o1lcpsuvwxz" "OOIICPSUVWXZ" `shouldBe` True
matchCaptchaStr "OOIICPSUVWXZ" "" `shouldBe` False
matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwx" `shouldBe` False
matchCaptchaStr "OOIICPSUVWXZ" "0o1lcpsuvwxzz" `shouldBe` False
listGroups :: HasCallStack => TestCC -> TestCC -> TestCC -> IO ()
listGroups superUser bob cath = do
sendListCommand bob 2
groupListing bob 1 "privacy" "Privacy" 3 "active"
groupListing bob 2 "security" "Security" 2 "active"
sendListCommand cath 1
groupListing cath 1 "anonymity" "Anonymity" 2 "suspended because roles changed"
-- superuser lists all groups
bob #> "@'SimpleX Directory' /last"
bob <# "'SimpleX Directory'> > /last"
bob <## " You are not allowed to use this command"
superUser #> "@'SimpleX Directory' /last"
superUser <# "'SimpleX Directory'> > /last"
superUser <## " 3 registered group(s)"
memberGroupListing superUser bob 1 "privacy" "Privacy" 3 "active"
memberGroupListing superUser bob 2 "security" "Security" 2 "active"
memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed"
-- showing last 1 group
superUser #> "@'SimpleX Directory' /last 1"
superUser <# "'SimpleX Directory'> > /last 1"
superUser <## " 3 registered group(s), showing the last 1"
memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed"
listUserGroup :: HasCallStack => TestCC -> String -> String -> IO ()
listUserGroup u n fn = do
sendListCommand u 1
groupListing u 1 n fn 2 "active"
sendListCommand :: HasCallStack => TestCC -> Int -> IO ()
sendListCommand u count = do
u #> "@'SimpleX Directory' /list"
u <# "'SimpleX Directory'> > /list"
u <## (" " <> show count <> " registered group(s)")
groupListing :: HasCallStack => TestCC -> Int -> String -> String -> Int -> String -> IO ()
groupListing u = groupListing_ u Nothing
memberGroupListing :: HasCallStack => TestCC -> TestCC -> Int -> String -> String -> Int -> String -> IO ()
memberGroupListing su owner = groupListing_ su (Just owner)
groupListing_ :: HasCallStack => TestCC -> Maybe TestCC -> Int -> String -> String -> Int -> String -> IO ()
groupListing_ su owner_ gId n fn count status = do
su <# ("'SimpleX Directory'> " <> show gId <> ". " <> n <> " (" <> fn <> ")")
su <## "Welcome message:"
su <##. ("Link to join the group " <> n <> ": ")
forM_ owner_ $ \owner -> do
ownerName <- userName owner
su <## ("Owner: " <> ownerName)
su <## (show count <> " members")
su <## ("Status: " <> status)
su <## ("/'role " <> show gId <> "', /'filter " <> show gId <> "'")
reapproveGroup :: HasCallStack => Int -> TestCC -> TestCC -> IO ()
reapproveGroup count superUser bob = do
superUser <# "'SimpleX Directory'> bob submitted the group ID 1:"
superUser <##. "privacy ("
superUser <## "Welcome message:"
superUser <##. "Link to join the group privacy: "
superUser <## (show count <> " members")
superUser <## ""
superUser <## "To approve send:"
superUser <# "'SimpleX Directory'> /approve 1:privacy 1"
superUser #> "@'SimpleX Directory' /approve 1:privacy 1"
superUser <# "'SimpleX Directory'> > /approve 1:privacy 1"
superUser <## " Group approved!"
bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!"
bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
bob <## ""
bob <## "Supported commands:"
bob <## "/'filter 1' - to configure anti-spam filter."
bob <## "/'role 1' - to set default member role."
bob <## "/'link 1' - to view/upgrade group link."
addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO ()
addCathAsOwner bob cath = do
connectUsers bob cath
fullAddMember "privacy" "Privacy" bob cath GROwner
joinGroup "privacy" cath bob
cath <## "#privacy: member 'SimpleX Directory' is connected"
withDirectoryService :: HasCallStack => TestParams -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryService ps = withDirectoryServiceCfg ps testCfg
withDirectoryServiceCfg :: HasCallStack => TestParams -> ChatConfig -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryServiceCfg ps cfg = withDirectoryServiceCfgOwnersGroup ps cfg False Nothing
withDirectoryServiceCfgOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> Bool -> Maybe FilePath -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup webFolder test = do
dsLink <-
withNewTestChatCfg ps cfg serviceDbPrefix directoryProfile $ \ds ->
withNewTestChatCfg ps cfg "super_user" aliceProfile $ \superUser -> do
connectUsers ds superUser
when createOwnersGroup $ do
superUser ##> "/g owners"
superUser <## "group #owners is created"
superUser <## "to add members use /a owners <name> or /create link #owners"
superUser ##> "/a owners 'SimpleX Directory' admin"
superUser <## "invitation to join the group #owners sent to 'SimpleX Directory'"
ds <## "#owners: alice invites you to join the group as admin"
ds <## "use /j owners to accept"
ds ##> "/j owners"
ds <## "#owners: you joined the group"
superUser <## "#owners: 'SimpleX Directory' joined the group"
ds ##> "/ad"
getContactLink ds True
withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test
restoreDirectoryService :: HasCallStack => TestParams -> Int -> (TestCC -> String -> IO ()) -> IO ()
restoreDirectoryService ps connCount test = do
dsLink <-
withTestChat ps serviceDbPrefix $ \ds -> do
ds .<## ("subscribed " <> show connCount <> " connections on server localhost")
ds ##> "/sa"
dsLink <- getContactLink ds False
ds <## "auto_accept on"
pure dsLink
withDirectory ps testCfg dsLink test
withDirectory :: HasCallStack => TestParams -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO ()
withDirectory ps cfg dsLink = withDirectoryOwnersGroup ps cfg dsLink False Nothing
withDirectoryOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> String -> Bool -> Maybe FilePath -> (TestCC -> String -> IO ()) -> IO ()
withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test = do
let opts = mkDirectoryOpts ps [KnownContact 2 "alice"] (if createOwnersGroup then Just $ KnownGroup 1 "owners" else Nothing) webFolder
runDirectory cfg opts $
withTestChatCfg ps cfg "super_user" $ \superUser -> do
if createOwnersGroup
then superUser <## "subscribed 2 connections on server localhost"
else superUser <## "subscribed 1 connections on server localhost"
test superUser dsLink
runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO ()
runDirectory cfg opts@DirectoryOpts {directoryLog} action = do
st <- openDirectoryLog directoryLog
t <- forkIO $ directoryService st opts cfg
threadDelay 500000
action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t)
registerGroup :: TestCC -> TestCC -> String -> String -> IO ()
registerGroup su u n fn = registerGroupId su u n fn 1 1
registerGroupId :: TestCC -> TestCC -> String -> String -> Int -> Int -> IO ()
registerGroupId su u n fn gId ugId = do
submitGroup u n fn
welcomeWithLink <- groupAccepted u n ugId
completeRegistrationId su u n fn welcomeWithLink gId ugId
submitGroup :: TestCC -> String -> String -> IO ()
submitGroup u n fn = do
u ##> ("/g " <> viewName n <> if null fn then "" else " " <> fn)
u <## ("group #" <> viewName n <> (if null fn then "" else " (" <> fn <> ")") <> " is created")
u <## ("to add members use /a " <> viewName n <> " <name> or /create link #" <> viewName n)
u ##> ("/a " <> viewName n <> " 'SimpleX Directory' admin")
u <## ("invitation to join the group #" <> viewName n <> " sent to 'SimpleX Directory'")
groupAccepted :: TestCC -> String -> Int -> IO String
groupAccepted u n ugId = do
u <###
[ WithTime ("'SimpleX Directory'> Joining the group " <> n <> "…"),
ConsoleString ("#" <> viewName n <> ": 'SimpleX Directory' joined the group")
]
u <# ("'SimpleX Directory'> Joined the group " <> n <> ", creating the link…")
u <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online."
u <## ""
u <## "Please add it to the group welcome message."
u <## "For example, add:"
welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u
u <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them."
u <## ("Captcha verification is enabled. Use /'filter " <> show ugId <> "' to change it.")
pure welcomeWithLink
completeRegistration :: TestCC -> TestCC -> String -> String -> String -> Int -> IO ()
completeRegistration su u n fn welcomeWithLink gId =
completeRegistrationId su u n fn welcomeWithLink gId gId
completeRegistrationId :: TestCC -> TestCC -> String -> String -> String -> Int -> Int -> IO ()
completeRegistrationId su u n fn welcomeWithLink gId ugId = do
updateProfileWithLink u n welcomeWithLink ugId
notifySuperUser su u n fn welcomeWithLink gId
approveRegistrationId su u n gId ugId
updateProfileWithLink :: TestCC -> String -> String -> Int -> IO ()
updateProfileWithLink u n welcomeWithLink ugId = do
u ##> ("/set welcome " <> viewName n <> " " <> welcomeWithLink)
u <## "welcome message changed to:"
u <## welcomeWithLink
u <# ("'SimpleX Directory'> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.")
u <## "You will be notified once the group is added to the directory - it may take up to 48 hours."
notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO ()
notifySuperUser su u n fn welcomeWithLink gId = do
uName <- userName u
su <# ("'SimpleX Directory'> " <> uName <> " submitted the group ID " <> show gId <> ":")
su <## (n <> if null fn then "" else " (" <> fn <> ")")
su <## "Welcome message:"
su <## welcomeWithLink
su .<## "members"
su <## ""
su <## "To approve send:"
let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1"
su <# ("'SimpleX Directory'> " <> approve)
approveRegistration :: TestCC -> TestCC -> String -> Int -> IO ()
approveRegistration su u n gId =
approveRegistrationId su u n gId gId
approveRegistrationId :: TestCC -> TestCC -> String -> Int -> Int -> IO ()
approveRegistrationId su u n gId ugId = do
let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1"
su #> ("@'SimpleX Directory' " <> approve)
su <# ("'SimpleX Directory'> > " <> approve)
su <## " Group approved!"
u <# ("'SimpleX Directory'> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory - please moderate it!")
u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved."
u <## ""
u <## "Supported commands:"
u <## ("/'filter " <> show ugId <> "' - to configure anti-spam filter.")
u <## ("/'role " <> show ugId <> "' - to set default member role.")
u <## ("/'link " <> show ugId <> "' - to view/upgrade group link.")
connectVia :: TestCC -> String -> IO ()
u `connectVia` dsLink = do
u ##> ("/c " <> dsLink)
u <## "connection request sent!"
u .<## ": contact is connected"
u .<# "> Welcome to SimpleX Directory!"
u <## ""
u <## "🔍 Send search string to find groups - try security."
u <## "/help - how to submit your group or channel."
u <## "/new - recent groups."
u <## ""
u <## "[Directory rules](https://simplex.chat/docs/directory.html)."
joinGroup :: String -> TestCC -> TestCC -> IO ()
joinGroup gName member host = do
let gn = "#" <> gName
memberName <- userName member
hostName <- userName host
member ##> ("/j " <> gName)
member <## (gn <> ": you joined the group")
member <#. (gn <> " " <> hostName <> "> Link to join the group " <> gName <> ": ")
host <## (gn <> ": " <> memberName <> " joined the group")
leaveGroup :: String -> TestCC -> IO ()
leaveGroup gName member = do
let gn = "#" <> gName
member ##> ("/l " <> gName)
member <## (gn <> ": you left the group")
member <## ("use /d " <> gn <> " to delete the group")
removeMember :: String -> TestCC -> TestCC -> IO ()
removeMember gName admin removed = do
let gn = "#" <> gName
adminName <- userName admin
removedName <- userName removed
admin ##> ("/rm " <> gName <> " " <> removedName)
admin <## (gn <> ": you removed " <> removedName <> " from the group")
removed <## (gn <> ": " <> adminName <> " removed you from the group")
removed <## ("use /d " <> gn <> " to delete the group")
groupFound :: TestCC -> String -> IO ()
groupFound = groupFoundN 2
groupFoundN :: Int -> TestCC -> String -> IO ()
groupFoundN count u name = do
u #> ("@'SimpleX Directory' " <> name)
groupFoundN' count u name
groupFoundN' :: Int -> TestCC -> String -> IO ()
groupFoundN' = groupFoundN_ "" Nothing
groupFoundN_ :: String -> Maybe Int -> Int -> TestCC -> String -> IO ()
groupFoundN_ suffix shownId_ count u name = do
u <# ("'SimpleX Directory" <> suffix <> "'> > " <> name)
u <## " Found 1 group(s)."
u <#. ("'SimpleX Directory" <> suffix <> "'> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name)
u <## "Welcome message:"
u <##. "Link to join the group "
u <## (show count <> " members")
groupNotFound :: TestCC -> String -> IO ()
groupNotFound = groupNotFound_ ""
groupNotFound_ :: String -> TestCC -> String -> IO ()
groupNotFound_ suffix u s = do
u #> ("@'SimpleX Directory" <> suffix <> "' " <> s)
u <# ("'SimpleX Directory" <> suffix <> "'> > " <> s)
u <## " No groups found"
testCaptchaTooManyAttempts :: HasCallStack => TestParams -> IO ()
testCaptchaTooManyAttempts ps =
withDirectoryService ps $ \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
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."
_ <- getTermLine cath
forM_ [1 :: Int .. 4] $ \i -> do
cath #> "#privacy (support) wrong"
cath <# "#privacy (support) 'SimpleX Directory'!> > cath wrong"
if i == 4
then cath <## " Incorrect text, please try again - this is your last attempt."
else cath <## " Incorrect text, please try again."
_ <- getTermLine cath
pure ()
cath #> "#privacy (support) wrong"
cath <# "#privacy (support) 'SimpleX Directory'> Too many failed attempts, you can't join group."
-- member removal produces multiple messages
_ <- getTermLine cath
_ <- getTermLine cath
_ <- getTermLine cath
pure ()
testCaptchaUnknownCommand :: HasCallStack => TestParams -> IO ()
testCaptchaUnknownCommand ps =
withDirectoryService ps $ \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
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."
_ <- getTermLine cath
cath #> "#privacy (support) /help"
cath <# "#privacy (support) 'SimpleX Directory'!> > cath /help"
cath <## " Unknown command, please enter captcha text."
testHelpNoAudio :: HasCallStack => TestParams -> IO ()
testHelpNoAudio ps =
withDirectoryService ps $ \_ dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
bob `connectVia` dsLink
-- commands help should not mention /audio
bob #> "@'SimpleX Directory' /help commands"
bob <# "'SimpleX Directory'> /'help commands' - receive this help message."
bob <## "/help - how to register your group or channel to be added to directory."
bob <## "/list - list the groups you registered."
bob <## "`/role <ID>` - view and set default member role for your group."
bob <## "`/filter <ID>` - view and set spam filter settings for group."
bob <## "`/link <ID>` - view and upgrade group link."
bob <## "`/delete <ID>:<NAME>` - remove the group you submitted from directory, with ID and name as shown by /list command."
bob <## ""
bob <## "To search for groups, send the search text."
testAudioCommandInDM :: HasCallStack => TestParams -> IO ()
testAudioCommandInDM ps =
withDirectoryService ps $ \_ dsLink ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
bob `connectVia` dsLink
bob #> "@'SimpleX Directory' /audio"
bob <# "'SimpleX Directory'> > /audio"
bob <## " Unknown command"
testRegisterChannelViaCard :: HasCallStack => TestParams -> IO ()
testRegisterChannelViaCard ps =
withDirectoryServiceCfg ps testCfg $ \superUser dsLink ->
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob ->
withRelay ps $ \relay -> do
-- bob connects to directory service first
bob `connectVia` dsLink
-- bob creates a channel with a relay
(_shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay
-- bob shares the channel card with directory bot
bob ##> "/share chat #news @'SimpleX Directory'"
bob <# "@'SimpleX Directory' link to join channel #news (signed):"
_ <- getTermLine bob -- short link
_ <- getTermLine bob -- ownerSig JSON
-- directory bot validates and joins via relay
bob <# "'SimpleX Directory'> Joining the channel news…"
concurrentlyN_
[ do
relay <## "'SimpleX Directory': accepting request to join group #news..."
relay <## "#news: 'SimpleX Directory' joined the group",
bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel"
]
-- owner sends a message to trigger member introduction
bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours."
bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them."
bob <## "Captcha verification is enabled. Use /'filter 1' to change it."
superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:"
superUser <## "news"
superUser <##. "Link to join channel: "
superUser <## "You need SimpleX Chat app v6.5 to join."
superUser <## "1 subscribers"
superUser <## ""
superUser <## "To approve send:"
superUser <# "'SimpleX Directory'> /approve 1:news 1"
-- superuser approves
let approve = "/approve 1:news 1"
superUser #> ("@'SimpleX Directory' " <> approve)
superUser <# ("'SimpleX Directory'> > " <> approve)
superUser <## " Channel approved!"
bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!")
bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved."
-- owner updates channel profile, triggering re-approval
bob ##> "/gp news news News and Updates"
bob <## "description changed to: News and Updates"
bob <# "'SimpleX Directory'> The channel ID 1 (news) is updated."
bob <## "It is hidden from the directory until approved."
relay <## "bob updated group #news: (signed)"
relay <## "description changed to: News and Updates"
superUser <# "'SimpleX Directory'> The channel ID 1 (news) is updated."
superUser <# ("'SimpleX Directory'> bob submitted the channel ID 1:")
superUser <## "news (News and Updates)"
superUser <##. "Link to join channel: "
superUser <## "You need SimpleX Chat app v6.5 to join."
superUser <## "2 subscribers"
superUser <## ""
superUser <## "To approve send:"
superUser <# "'SimpleX Directory'> /approve 1:news 1"
-- re-approve after profile update
let approve2 = "/approve 1:news 1"
superUser #> ("@'SimpleX Directory' " <> approve2)
superUser <# ("'SimpleX Directory'> > " <> approve2)
superUser <## " Channel approved!"
bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!")
bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved."
-- owner leaves channel, triggering de-listing and bot leaving
bob ##> "/leave #news"
concurrentlyN_
[ do
bob <## "#news: you left the group"
bob <## "use /d #news to delete the group",
relay <## "#news: bob left the group (signed)"
]
bob <# "'SimpleX Directory'> You left the channel ID 1 (news)."
bob <## ""
bob <## "The channel is no longer listed in the directory."
superUser <# "'SimpleX Directory'> The channel ID 1 (news) is de-listed (channel owner left)."
relay <## "#news: 'SimpleX Directory' left the group (signed)"
testLinkAsTextSearch :: HasCallStack => TestParams -> IO ()
testLinkAsTextSearch ps =
withDirectoryServiceCfg ps testCfg $ \_superUser dsLink ->
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob ->
withRelay ps $ \relay -> do
bob `connectVia` dsLink
(shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay
bob #> ("@'SimpleX Directory' " <> shortLink)
bob <# ("'SimpleX Directory'> > " <> shortLink)
bob <## " No groups found."
bob <## "To register a group or a channel, please use \"Share via chat\" feature."
testNonOwnerSharesCard :: HasCallStack => TestParams -> IO ()
testNonOwnerSharesCard ps =
withDirectoryServiceCfg ps testCfg $ \_superUser dsLink ->
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob ->
withRelay ps $ \relay ->
withNewTestChatCfg ps testCfg "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
cath `connectVia` dsLink
(shortLink, fullLink) <- prepareChannel1Relay "news" bob relay
memberJoinChannel "news" [relay] [bob] shortLink fullLink cath
cath ##> "/share chat #news @'SimpleX Directory'"
cath <# "@'SimpleX Directory' link to join channel #news:"
_ <- getTermLine cath -- short link
cath <# "'SimpleX Directory'> To add a channel to directory you must be the owner."
testDeleteChannelRegistration :: HasCallStack => TestParams -> IO ()
testDeleteChannelRegistration ps =
withDirectoryServiceCfg ps testCfg $ \superUser dsLink ->
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob ->
withRelay ps $ \relay -> do
bob `connectVia` dsLink
(_shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay
bob ##> "/share chat #news @'SimpleX Directory'"
bob <# "@'SimpleX Directory' link to join channel #news (signed):"
_ <- getTermLine bob -- short link
_ <- getTermLine bob -- ownerSig JSON
bob <# "'SimpleX Directory'> Joining the channel news…"
concurrentlyN_
[ do
relay <## "'SimpleX Directory': accepting request to join group #news..."
relay <## "#news: 'SimpleX Directory' joined the group",
bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel"
]
bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours."
bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them."
bob <## "Captcha verification is enabled. Use /'filter 1' to change it."
superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:"
superUser <## "news"
superUser <##. "Link to join channel: "
superUser <## "You need SimpleX Chat app v6.5 to join."
superUser <## "1 subscribers"
superUser <## ""
superUser <## "To approve send:"
superUser <# "'SimpleX Directory'> /approve 1:news 1"
let approve = "/approve 1:news 1"
superUser #> ("@'SimpleX Directory' " <> approve)
superUser <# ("'SimpleX Directory'> > " <> approve)
superUser <## " Channel approved!"
bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!")
bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved."
-- owner deletes registration
bob #> "@'SimpleX Directory' /delete 1:news"
bob
<###
[ WithTime "'SimpleX Directory'> > /delete 1:news",
" Your channel news is deleted from the directory",
"#news: 'SimpleX Directory_1' left the group (signed)"
]
relay <## "#news: 'SimpleX Directory' left the group (signed)"
testReregistrationAlreadyListed :: HasCallStack => TestParams -> IO ()
testReregistrationAlreadyListed ps =
withDirectoryServiceCfg ps testCfg $ \superUser dsLink ->
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob ->
withRelay ps $ \relay -> do
bob `connectVia` dsLink
(_shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay
-- register and approve
bob ##> "/share chat #news @'SimpleX Directory'"
bob <# "@'SimpleX Directory' link to join channel #news (signed):"
_ <- getTermLine bob -- short link
_ <- getTermLine bob -- ownerSig JSON
bob <# "'SimpleX Directory'> Joining the channel news…"
concurrentlyN_
[ do
relay <## "'SimpleX Directory': accepting request to join group #news..."
relay <## "#news: 'SimpleX Directory' joined the group",
bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel"
]
bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours."
bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them."
bob <## "Captcha verification is enabled. Use /'filter 1' to change it."
superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:"
superUser <## "news"
superUser <##. "Link to join channel: "
superUser <## "You need SimpleX Chat app v6.5 to join."
superUser <## "1 subscribers"
superUser <## ""
superUser <## "To approve send:"
superUser <# "'SimpleX Directory'> /approve 1:news 1"
let approve = "/approve 1:news 1"
superUser #> ("@'SimpleX Directory' " <> approve)
superUser <# ("'SimpleX Directory'> > " <> approve)
superUser <## " Channel approved!"
bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!")
bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved."
-- search finds the channel with its link
bob #> "@'SimpleX Directory' news"
bob <# "'SimpleX Directory'> > news"
bob <## " Found 1 group(s)."
bob <# "'SimpleX Directory'> news"
bob <##. "Link to join channel: "
bob <## "You need SimpleX Chat app v6.5 to join."
bob <## "1 subscribers"
-- owner re-shares card while already listed
bob ##> "/share chat #news @'SimpleX Directory'"
bob <# "@'SimpleX Directory' link to join channel #news (signed):"
_ <- getTermLine bob -- short link
_ <- getTermLine bob -- ownerSig JSON
bob <# "'SimpleX Directory'> Channel is already listed in the directory."
testLinkCheckUpdatesCount :: HasCallStack => TestParams -> IO ()
testLinkCheckUpdatesCount ps = do
dsLink <-
withNewTestChatCfg ps testCfg serviceDbPrefix directoryProfile $ \ds ->
withNewTestChatCfg ps testCfg "super_user" aliceProfile $ \superUser -> do
connectUsers ds superUser
ds ##> "/ad"
getContactLink ds True
let opts = (mkDirectoryOpts ps [KnownContact 2 "alice"] Nothing Nothing) {linkCheckInterval = 1}
runDirectory testCfg opts $
withTestChatCfg ps testCfg "super_user" $ \superUser -> do
superUser <## "subscribed 1 connections on server localhost"
withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob ->
withRelay ps $ \relay ->
withNewTestChatCfg ps testCfg "cath" cathProfile $ \cath -> do
bob `connectVia` dsLink
(shortLink, fullLink) <- prepareChannel1Relay "news" bob relay
-- register and approve
bob ##> "/share chat #news @'SimpleX Directory'"
bob <# "@'SimpleX Directory' link to join channel #news (signed):"
_ <- getTermLine bob -- short link
_ <- getTermLine bob -- ownerSig JSON
bob <# "'SimpleX Directory'> Joining the channel news…"
concurrentlyN_
[ do
relay <## "'SimpleX Directory': accepting request to join group #news..."
relay <## "#news: 'SimpleX Directory' joined the group",
bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel"
]
bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours."
bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them."
bob <## "Captcha verification is enabled. Use /'filter 1' to change it."
superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:"
superUser <## "news"
superUser <##. "Link to join channel: "
superUser <## "You need SimpleX Chat app v6.5 to join."
superUser <## "1 subscribers"
superUser <## ""
superUser <## "To approve send:"
superUser <# "'SimpleX Directory'> /approve 1:news 1"
let approve = "/approve 1:news 1"
superUser #> ("@'SimpleX Directory' " <> approve)
superUser <# ("'SimpleX Directory'> > " <> approve)
superUser <## " Channel approved!"
bob <# ("'SimpleX Directory'> The channel ID 1 (news) is approved and listed in directory - please moderate it!")
bob <## "Please note: if you change the channel profile it will be hidden from directory until it is re-approved."
-- link check updates count (bot joined)
threadDelay 1000000
bob #> "@'SimpleX Directory' news"
bob <# "'SimpleX Directory'> > news"
bob <## " Found 1 group(s)."
bob <# "'SimpleX Directory'> news"
bob <##. "Link to join channel: "
bob <## "You need SimpleX Chat app v6.5 to join."
bob <## "2 subscribers"
-- second subscriber joins
memberJoinChannel "news" [relay] [bob] shortLink fullLink cath
-- link check updates count again
threadDelay 1000000
bob #> "@'SimpleX Directory' news"
bob <# "'SimpleX Directory'> > news"
bob <## " Found 1 group(s)."
bob <# "'SimpleX Directory'> news"
bob <##. "Link to join channel: "
bob <## "You need SimpleX Chat app v6.5 to join."
bob <## "3 subscribers"
testGetCaptchaStr :: HasCallStack => TestParams -> IO ()
testGetCaptchaStr _ps = do
s0 <- getCaptchaStr 0 ""
s0 `shouldBe` ""
s7 <- getCaptchaStr 7 ""
length s7 `shouldBe` 7
all (`elem` ("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" :: String)) s7 `shouldBe` True