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>
This commit is contained in:
sh
2026-07-03 12:51:52 +01:00
committed by GitHub
co-authored by Evgeny Poberezkin Evgeny @ SimpleX Chat
parent 2c2337b07c
commit 10a814694c
92 changed files with 3532 additions and 815 deletions
+30 -10
View File
@@ -84,6 +84,7 @@ enum ChatCommand: ChatCmdProtocol {
case apiLeaveGroup(groupId: Int64)
case apiListMembers(groupId: Int64)
case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
case apiSetPublicGroupAccess(groupId: Int64, access: PublicGroupAccess)
case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole)
case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole)
case apiDeleteGroupLink(groupId: Int64)
@@ -131,8 +132,8 @@ enum ChatCommand: ChatCmdProtocol {
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
case apiChangeConnectionUser(connId: Int64, userId: Int64)
case apiConnectPlan(userId: Int64, connLink: String, linkOwnerSig: LinkOwnerSig?)
case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData)
case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData)
case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain?)
case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain?)
case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64)
case apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64)
case apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?)
@@ -155,6 +156,9 @@ enum ChatCommand: ChatCmdProtocol {
case apiAddMyAddressShortLink(userId: Int64)
case apiSetProfileAddress(userId: Int64, on: Bool)
case apiSetAddressSettings(userId: Int64, addressSettings: AddressSettings)
case apiSetUserDomain(userId: Int64, simplexDomain: String?)
case apiVerifyContactDomain(contactId: Int64)
case apiVerifyGroupDomain(groupId: Int64)
case apiAcceptContact(incognito: Bool, contactReqId: Int64)
case apiRejectContact(contactReqId: Int64)
// WebRTC calls
@@ -346,8 +350,8 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiConnectPlan(userId, connLink, linkOwnerSig):
let sigStr = if let linkOwnerSig { " sig=\(encodeJSON(linkOwnerSig))" } else { "" }
return "/_connect plan \(userId) \(connLink)\(sigStr)"
case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))"
case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") direct=\(onOff(directLink)) \(encodeJSON(groupShortLinkData))"
case let .apiPrepareContact(userId, connLink, contactShortLinkData, verifiedDomain): return "/_prepare contact \(userId) \(connLink.cmdString)\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(contactShortLinkData))"
case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData, verifiedDomain): return "/_prepare group \(userId) \(connLink.cmdString) direct=\(onOff(directLink))\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(groupShortLinkData))"
case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)"
case let .apiChangePreparedGroupUser(groupId, newUserId): return "/_set group user #\(groupId) \(newUserId)"
case let .apiConnectPreparedContact(contactId, incognito, mc): return "/_connect contact @\(contactId) incognito=\(onOff(incognito))\(maybeContent(mc))"
@@ -370,6 +374,10 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiAddMyAddressShortLink(userId): return "/_short_link_address \(userId)"
case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))"
case let .apiSetAddressSettings(userId, addressSettings): return "/_address_settings \(userId) \(encodeJSON(addressSettings))"
case let .apiSetUserDomain(userId, simplexDomain): return "/_set domain \(userId)" + (simplexDomain.map { " " + $0 } ?? "")
case let .apiSetPublicGroupAccess(groupId, access): return "/_public group access #\(groupId) \(encodeJSON(access))"
case let .apiVerifyContactDomain(contactId): return "/_verify domain @\(contactId)"
case let .apiVerifyGroupDomain(groupId): return "/_verify domain #\(groupId)"
case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)"
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
@@ -481,6 +489,7 @@ enum ChatCommand: ChatCmdProtocol {
case .apiLeaveGroup: return "apiLeaveGroup"
case .apiListMembers: return "apiListMembers"
case .apiUpdateGroupProfile: return "apiUpdateGroupProfile"
case .apiSetPublicGroupAccess: return "apiSetPublicGroupAccess"
case .apiCreateGroupLink: return "apiCreateGroupLink"
case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole"
case .apiDeleteGroupLink: return "apiDeleteGroupLink"
@@ -551,6 +560,9 @@ enum ChatCommand: ChatCmdProtocol {
case .apiAddMyAddressShortLink: return "apiAddMyAddressShortLink"
case .apiSetProfileAddress: return "apiSetProfileAddress"
case .apiSetAddressSettings: return "apiSetAddressSettings"
case .apiSetUserDomain: return "apiSetUserDomain"
case .apiVerifyContactDomain: return "apiVerifyContactDomain"
case .apiVerifyGroupDomain: return "apiVerifyGroupDomain"
case .apiAcceptContact: return "apiAcceptContact"
case .apiRejectContact: return "apiRejectContact"
case .apiSendCallInvitation: return "apiSendCallInvitation"
@@ -960,6 +972,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole)
case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool)
case groupUpdated(user: UserRef, toGroup: GroupInfo)
case contactDomainVerified(user: UserRef, contact: Contact, verificationFailure: String?)
case groupDomainVerified(user: UserRef, groupInfo: GroupInfo, verificationFailure: String?)
case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink)
case groupLink(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink)
case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo)
@@ -1015,6 +1029,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case .membersRoleUser: "membersRoleUser"
case .membersBlockedForAllUser: "membersBlockedForAllUser"
case .groupUpdated: "groupUpdated"
case .contactDomainVerified: "contactDomainVerified"
case .groupDomainVerified: "groupDomainVerified"
case .groupLinkCreated: "groupLinkCreated"
case .groupLink: "groupLink"
case .groupLinkDeleted: "groupLinkDeleted"
@@ -1066,6 +1082,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)")
case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)")
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
case let .contactDomainVerified(u, contact, verificationFailure): return withUser(u, "contact: \(contact)\nverificationFailure: \(verificationFailure ?? "ok")")
case let .groupDomainVerified(u, groupInfo, verificationFailure): return withUser(u, "groupInfo: \(groupInfo)\nverificationFailure: \(verificationFailure ?? "ok")")
case let .groupLinkCreated(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)")
case let .groupLink(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)")
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
@@ -1383,7 +1401,7 @@ enum InvitationLinkPlan: Decodable, Hashable {
}
enum ContactAddressPlan: Decodable, Hashable {
case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?)
case ok(contactSLinkData_: ContactShortLinkData?, ownerVerification: OwnerVerification?, verifiedDomain: SimplexDomain?)
case ownLink
case connectingConfirmReconnect
case connectingProhibit(contact: Contact)
@@ -1398,7 +1416,7 @@ public struct GroupShortLinkInfo: Decodable, Hashable {
}
enum GroupLinkPlan: Decodable, Hashable {
case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?)
case ok(groupSLinkInfo_: GroupShortLinkInfo?, groupSLinkData_: GroupShortLinkData?, ownerVerification: OwnerVerification?, verifiedDomain: SimplexDomain?)
case ownLink(groupInfo: GroupInfo)
case connectingConfirmReconnect
case connectingProhibit(groupInfo_: GroupInfo?)
@@ -1766,14 +1784,15 @@ struct ServerOperator: Identifiable, Equatable, Codable {
serverDomains: ["simplex.im"],
conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false),
enabled: true,
smpRoles: ServerRoles(storage: true, proxy: true),
xftpRoles: ServerRoles(storage: true, proxy: true)
smpRoles: ServerRoles(storage: true, proxy: true, names: true),
xftpRoles: ServerRoles(storage: true, proxy: true, names: false)
)
}
struct ServerRoles: Equatable, Codable {
var storage: Bool
var proxy: Bool
var names: Bool
}
struct UserOperatorServers: Identifiable, Equatable, Codable {
@@ -1800,8 +1819,8 @@ struct UserOperatorServers: Identifiable, Equatable, Codable {
serverDomains: [],
conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false),
enabled: false,
smpRoles: ServerRoles(storage: true, proxy: true),
xftpRoles: ServerRoles(storage: true, proxy: true)
smpRoles: ServerRoles(storage: true, proxy: true, names: true),
xftpRoles: ServerRoles(storage: true, proxy: true, names: false)
)
}
set { `operator` = newValue }
@@ -1824,6 +1843,7 @@ struct UserOperatorServers: Identifiable, Equatable, Codable {
public enum UserServersWarning: Decodable {
case noChatRelays(user: UserRef?)
case noNamesServers(user: UserRef?)
}
enum UserServersError: Decodable {
+84 -4
View File
@@ -1083,6 +1083,24 @@ private func apiConnectResponseAlert<R>(_ r: APIResult<R>) -> Alert {
title: "Unsupported connection link",
message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link."
)
case let .error(.simplexDomainNotReady(domain, err)):
switch err {
case .noValidLink:
mkAlert(
title: "No valid link",
message: "The SimpleX name \(domain.fullDomainName) is registered, but it has no valid link."
)
case .unknownDomain:
mkAlert(
title: "Unconfirmed name",
message: "The SimpleX name \(domain.fullDomainName) is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner."
)
}
case .errorAgent(.NO_NAME_SERVERS):
mkAlert(
title: "SimpleX name error",
message: "None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link."
)
case .errorAgent(.SMP(_, .AUTH)):
mkAlert(
title: "Connection error (AUTH)",
@@ -1113,6 +1131,24 @@ private func apiConnectResponseAlert<R>(_ r: APIResult<R>) -> Alert {
} else {
connectionErrorAlert(r)
}
case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))):
switch nameErr {
case .NOT_FOUND:
mkAlert(
title: "Name not found",
message: "This SimpleX name is not registered. Please check the name."
)
case .NO_RESOLVER:
mkAlert(
title: "SimpleX name error",
message: "Server \(serverAddress) does not support name resolution. Configure servers, or use a connection link."
)
case let .RESOLVER(resolverErr):
mkAlert(
title: "SimpleX name error",
message: "Resolver error: \(resolverErr)"
)
}
default: connectionErrorAlert(r)
}
}
@@ -1156,16 +1192,16 @@ private func connectionErrorAlert<R>(_ r: APIResult<R>) -> Alert {
}
}
func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) async throws -> ChatData {
func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData {
let userId = try currentUserId("apiPrepareContact")
let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData))
let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain))
if case let .newPreparedChat(_, chat) = r { return chat }
throw r.unexpected
}
func apiPrepareGroup(connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData) async throws -> ChatData {
func apiPrepareGroup(connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData {
let userId = try currentUserId("apiPrepareGroup")
let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, directLink: directLink, groupShortLinkData: groupShortLinkData))
let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, directLink: directLink, groupShortLinkData: groupShortLinkData, verifiedDomain: verifiedDomain))
if case let .newPreparedChat(_, chat) = r { return chat }
throw r.unexpected
}
@@ -1329,6 +1365,43 @@ func apiSetProfileAddress(on: Bool) async throws -> User? {
}
}
// name is the encoded SimplexName (e.g. "@alice.simplex"); nil clears it
// owner-specific SNENoValidLink wording; everything else reuses the general apiConnectResponseAlert
func showSetSimplexNameError<R>(_ r: APIResult<R>, isChannel: Bool) {
if case let .error(.simplexDomainNotReady(domain, .noValidLink)) = r.unexpected {
let format = isChannel
? NSLocalizedString("The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page.", comment: "alert message")
: NSLocalizedString("The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.", comment: "alert message")
showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName))
} else {
AlertManager.shared.showAlert(apiConnectResponseAlert(r))
}
}
func apiSetUserDomain(_ simplexDomain: String?) async throws -> User {
let userId = try currentUserId("apiSetUserDomain")
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiSetUserDomain(userId: userId, simplexDomain: simplexDomain))
switch r {
case let .result(.userProfileUpdated(user, _, _, _)): return user
case let .result(.userProfileNoChange(user)): return user
default:
showSetSimplexNameError(r, isChannel: false)
throw r.unexpected
}
}
func apiVerifyContactDomain(_ contactId: Int64) async throws -> (Contact, String?) {
let r: ChatResponse2 = try await chatSendCmd(.apiVerifyContactDomain(contactId: contactId))
if case let .contactDomainVerified(_, contact, verificationFailure) = r { return (contact, verificationFailure) }
throw r.unexpected
}
func apiVerifyGroupDomain(_ groupId: Int64) async throws -> (GroupInfo, String?) {
let r: ChatResponse2 = try await chatSendCmd(.apiVerifyGroupDomain(groupId: groupId))
if case let .groupDomainVerified(_, groupInfo, verificationFailure) = r { return (groupInfo, verificationFailure) }
throw r.unexpected
}
func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? {
let r: ChatResponse1 = try await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences))
if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact }
@@ -1995,6 +2068,13 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
throw r.unexpected
}
func apiSetPublicGroupAccess(_ groupId: Int64, access: PublicGroupAccess) async throws -> GroupInfo {
let r: APIResult<ChatResponse2> = await chatApiSendCmd(.apiSetPublicGroupAccess(groupId: groupId, access: access))
if case let .result(.groupUpdated(_, toGroup)) = r { return toGroup }
showSetSimplexNameError(r, isChannel: true)
throw r.unexpected
}
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink? {
let r: APIResult<ChatResponse2>? = await chatApiSendCmdWithRetry(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole))
if case let .result(.groupLinkCreated(_, _, groupLink)) = r { return groupLink }
@@ -392,6 +392,26 @@ struct ChatInfoView: View {
.lineLimit(3)
.padding(.bottom, 2)
}
if let domain = contact.profile.contactDomain,
contact.profile.contactDomainVerified != nil || domain.proof != nil {
SimplexNameView(
simplexName: "@\(domain.shortName)",
verified: contact.profile.contactDomainVerified,
verify: {
do {
let (ct, reason) = try await apiVerifyContactDomain(contact.contactId)
await MainActor.run {
chatModel.updateContact(ct)
contact = ct
}
return (ct.profile.contactDomainVerified, reason)
} catch {
logger.error("apiVerifyContactDomain: \(responseError(error))")
return nil
}
}
)
}
if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" {
let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background)
msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true)
@@ -1361,6 +1381,64 @@ private func deleteNotReadyContact(
))
}
struct SimplexNameView: View {
@EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) var autoVerify = false
let simplexName: String
let verified: Bool?
let verify: () async -> (Bool?, String?)?
@State private var inFlight = false
@State private var showSpinner = false
var body: some View {
HStack(spacing: 6) {
Text(simplexName)
.font(verified == true ? .subheadline : .system(.subheadline, design: .monospaced))
.foregroundColor(verified == true ? theme.colors.primary : theme.colors.secondary)
indicator()
}
.padding(.bottom, 2)
.onAppear { if autoVerify && verified == nil { runVerify(manual: false) } }
}
@ViewBuilder private func indicator() -> some View {
if showSpinner {
ProgressView()
} else if verified == true {
Image(systemName: "checkmark")
} else if verified == false {
Image(systemName: "xmark")
.foregroundColor(.red)
.onTapGesture { runVerify(manual: true) }
} else {
Button { runVerify(manual: true) } label: {
Text("Verify name").font(.subheadline).foregroundColor(theme.colors.primary)
}
}
}
private func runVerify(manual: Bool) {
if inFlight { return }
inFlight = true
// delay the spinner so a fast result on appear doesn't flash it
Task {
try? await Task.sleep(nanoseconds: 300_000000)
await MainActor.run { if inFlight { showSpinner = true } }
}
Task {
let res = await verify()
await MainActor.run {
inFlight = false
showSpinner = false
// show the reason on a manual run, or on an inconclusive auto run (state stayed nil)
if let (newV, reason) = res, let reason, manual || newV == nil {
showAlert(NSLocalizedString("SimpleX name not verified", comment: "alert title"), message: reason)
}
}
}
}
}
struct ChatInfoView_Previews: PreviewProvider {
static var previews: some View {
ChatInfoView(
@@ -214,8 +214,8 @@ private func handleTextTaps(
var simplex: Bool = false
s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in
if index >= range.location && index < range.location + range.length {
if let nameInfo = attrs[nameAttrKey] as? SimplexNameInfo {
showUnsupportedNameAlert(nameInfo)
if attrs[nameAttrKey] is SimplexNameInfo {
planAndConnect(s.attributedSubstring(from: range).string, theme: AppTheme.shared, dismiss: false)
} else if let url = attrs[linkAttrKey] as? String {
linkURL = url
browser = attrs[webLinkAttrKey] != nil
@@ -148,7 +148,7 @@ struct ChannelWebAccessView: View {
let existingAccess = pg.publicGroupAccess
pg.publicGroupAccess = PublicGroupAccess(
groupWebPage: trimmedPage.isEmpty ? nil : trimmedPage,
groupDomain: existingAccess?.groupDomain,
groupDomainClaim: existingAccess?.groupDomainClaim,
domainWebPage: existingAccess?.domainWebPage ?? false,
allowEmbedding: allowEmbedding
)
@@ -112,6 +112,7 @@ struct GroupChatInfoView: View {
// TODO [relays] allow other owners to manage channel link (requires protocol changes to share link ownership)
if groupInfo.isOwner && groupLink != nil {
channelLinkButton()
channelSimplexNameButton()
} else if let link = groupInfo.groupProfile.publicGroup?.groupLink {
SimpleXLinkQRCode(uri: link)
Button {
@@ -331,6 +332,27 @@ struct GroupChatInfoView: View {
.lineLimit(4)
.fixedSize(horizontal: false, vertical: true)
}
if let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess,
let domain = access.groupDomainClaim?.shortName,
groupInfo.groupDomainVerified != nil || access.groupDomainClaim?.proof != nil {
SimplexNameView(
simplexName: "#\(domain)",
verified: groupInfo.groupDomainVerified,
verify: {
do {
let (gInfo, reason) = try await apiVerifyGroupDomain(groupInfo.groupId)
await MainActor.run {
chatModel.updateGroup(gInfo)
groupInfo = gInfo
}
return (gInfo.groupDomainVerified, reason)
} catch {
logger.error("apiVerifyGroupDomain: \(responseError(error))")
return nil
}
}
)
}
if let webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage,
let url = URL(string: webPage) {
Link(destination: url) {
@@ -674,6 +696,34 @@ struct GroupChatInfoView: View {
}
}
private func channelSimplexNameButton() -> some View {
NavigationLink {
let domain = if let d = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName { "#\(d)" } else { "" }
SetSimplexDomainView(
title: "SimpleX name",
footer: "Let people join via name registered with this channel link.",
prompt: "#channelname.testing",
simplexName: domain,
save: { domain in
do {
var access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?? PublicGroupAccess()
access.groupDomainClaim = domain.map { SimplexDomainClaim(domain: $0) }
let gInfo = try await apiSetPublicGroupAccess(groupInfo.groupId, access: access)
await MainActor.run {
chatModel.updateGroup(gInfo)
groupInfo = gInfo
}
return true
} catch {
return false
}
}
)
} label: {
Label("SimpleX name", systemImage: "number")
}
}
private func groupLinkDestinationView() -> some View {
GroupLinkView(
groupId: groupInfo.groupId,
@@ -683,8 +683,17 @@ struct ChatListSearchBar: View {
searchShowingSimplexLink = true
searchChatFilteredBySimplexLink = nil
connect(text)
case let .name(nameInfo):
showUnsupportedNameAlert(nameInfo)
case let .name(text, _):
searchFocussed = false
planAndConnect(
text,
theme: theme,
dismiss: false,
cleanup: {
searchText = ""
searchFocussed = false
}
)
case .none:
if t != "" {
searchFocussed = true
@@ -389,8 +389,17 @@ struct ContactsListSearchBar: View {
searchShowingSimplexLink = true
searchChatFilteredBySimplexLink = nil
connect(text)
case let .name(nameInfo):
showUnsupportedNameAlert(nameInfo)
case let .name(text, _):
searchFocussed = false
planAndConnect(
text,
theme: theme,
dismiss: true,
cleanup: {
searchText = ""
searchFocussed = false
}
)
case .none:
if t != "" {
searchFocussed = true
+24 -29
View File
@@ -669,8 +669,9 @@ private struct ConnectView: View {
case let .link(text, _, _):
pastedLink = text
connect(pastedLink)
case let .name(nameInfo):
showUnsupportedNameAlert(nameInfo)
case let .name(text, _):
pastedLink = text
connect(pastedLink)
case .none:
alert = .newChatSomeAlert(alert: SomeAlert(
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
@@ -869,7 +870,7 @@ func strIsSimplexLink(_ str: String) -> Bool {
enum ConnectTarget {
case link(text: String, linkType: SimplexLinkType, linkText: String)
case name(SimplexNameInfo)
case name(text: String, nameInfo: SimplexNameInfo)
}
func strConnectTarget(_ str: String) -> ConnectTarget? {
@@ -878,28 +879,14 @@ func strConnectTarget(_ str: String) -> ConnectTarget? {
return if links.count == 1, case let .simplexLink(_, linkType, _, smpHosts) = links[0].format {
.link(text: links[0].text, linkType: linkType, linkText: simplexLinkText(linkType, smpHosts))
} else if links.isEmpty,
case let .simplexName(nameInfo) = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } })?.format {
.name(nameInfo)
let nameFt = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } }),
case let .simplexName(nameInfo) = nameFt.format {
.name(text: nameFt.text, nameInfo: nameInfo)
} else {
nil
}
}
func showUnsupportedNameAlert(_ nameInfo: SimplexNameInfo) {
let upgrade = " " + NSLocalizedString("Please upgrade the app.", comment: "alert message")
if nameInfo.nameType == .contact {
showAlert(
NSLocalizedString("Unsupported contact name", comment: "alert title"),
message: NSLocalizedString("Connecting via contact name requires a newer app version.", comment: "alert message") + upgrade
)
} else {
showAlert(
NSLocalizedString("Unsupported channel name", comment: "alert title"),
message: NSLocalizedString("Connecting via channel name requires a newer app version.", comment: "alert message") + upgrade
)
}
}
struct IncognitoToggle: View {
@EnvironmentObject var theme: AppTheme
@Binding var incognitoEnabled: Bool
@@ -1145,6 +1132,7 @@ private func showPrepareContactAlert(
connectionLink: CreatedConnLink,
contactShortLinkData: ContactShortLinkData,
ownerVerification: OwnerVerification? = nil,
verifiedDomain: SimplexDomain? = nil,
theme: AppTheme,
dismiss: Bool,
cleanup: (() -> Void)?
@@ -1171,7 +1159,7 @@ private func showPrepareContactAlert(
onConfirm: {
Task {
do {
let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData)
let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain)
await MainActor.run {
ChatModel.shared.addChat(Chat(chat))
openKnownChat(chat.id, dismiss: dismiss, cleanup: cleanup)
@@ -1193,6 +1181,7 @@ private func showPrepareGroupAlert(
groupShortLinkInfo: GroupShortLinkInfo?,
groupShortLinkData: GroupShortLinkData,
ownerVerification: OwnerVerification? = nil,
verifiedDomain: SimplexDomain? = nil,
theme: AppTheme,
dismiss: Bool,
cleanup: (() -> Void)?
@@ -1221,7 +1210,7 @@ private func showPrepareGroupAlert(
onConfirm: {
Task {
do {
let chat = try await apiPrepareGroup(connLink: connectionLink, directLink: groupShortLinkInfo?.direct ?? true, groupShortLinkData: groupShortLinkData)
let chat = try await apiPrepareGroup(connLink: connectionLink, directLink: groupShortLinkInfo?.direct ?? true, groupShortLinkData: groupShortLinkData, verifiedDomain: verifiedDomain)
await MainActor.run {
if let relays = groupShortLinkInfo?.groupRelays, !relays.isEmpty,
case let .group(gInfo, _) = chat.chatInfo {
@@ -1319,10 +1308,6 @@ func planAndConnect(
filterKnownGroup: ((GroupInfo) -> Void)? = nil
) {
switch strConnectTarget(shortOrFullLink) {
case let .name(nameInfo):
showUnsupportedNameAlert(nameInfo)
cleanup?()
return
case let .link(_, linkType, _):
if linkType == .relay {
showAlert(
@@ -1332,7 +1317,9 @@ func planAndConnect(
cleanup?()
return
}
case .none: break
// A SimplexName falls through to apiConnectPlan, which resolves it on the
// core (the /_connect plan command accepts a name target, not only a link).
case .name, .none: break
}
ConnectProgressManager.shared.cancelConnectProgress()
let inProgress = BoxedValue(true)
@@ -1416,7 +1403,7 @@ func planAndConnect(
}
case let .contactAddress(cap):
switch cap {
case let .ok(contactSLinkData_, ownerVerification):
case let .ok(contactSLinkData_, ownerVerification, verifiedDomain):
if let contactSLinkData = contactSLinkData_ {
logger.debug("planAndConnect, .contactAddress, .ok, short link data present")
await MainActor.run {
@@ -1424,6 +1411,7 @@ func planAndConnect(
connectionLink: connectionLink,
contactShortLinkData: contactSLinkData,
ownerVerification: ownerVerification,
verifiedDomain: verifiedDomain,
theme: theme,
dismiss: dismiss,
cleanup: cleanup
@@ -1478,6 +1466,9 @@ func planAndConnect(
case let .known(contact):
logger.debug("planAndConnect, .contactAddress, .known")
await MainActor.run {
if ChatModel.shared.getContactChat(contact.contactId) == nil {
ChatModel.shared.addChat(Chat(chatInfo: .direct(contact: contact)))
}
if let f = filterKnownContact {
f(contact)
} else {
@@ -1496,7 +1487,7 @@ func planAndConnect(
}
case let .groupLink(glp):
switch glp {
case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification):
case let .ok(groupShortLinkInfo_, groupSLinkData_, ownerVerification, verifiedDomain):
if let groupSLinkData = groupSLinkData_ {
logger.debug("planAndConnect, .groupLink, .ok, short link data present")
await MainActor.run {
@@ -1505,6 +1496,7 @@ func planAndConnect(
groupShortLinkInfo: groupShortLinkInfo_,
groupShortLinkData: groupSLinkData,
ownerVerification: ownerVerification,
verifiedDomain: verifiedDomain,
theme: theme,
dismiss: dismiss,
cleanup: cleanup
@@ -1557,6 +1549,9 @@ func planAndConnect(
case let .known(groupInfo):
logger.debug("planAndConnect, .groupLink, .known")
await MainActor.run {
if ChatModel.shared.getGroupChat(groupInfo.groupId) == nil {
ChatModel.shared.addChat(Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil)))
}
if let f = filterKnownGroup {
f(groupInfo)
} else {
@@ -111,13 +111,16 @@ struct NetworkAndServers: View {
Button("Save servers", action: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) })
.disabled(!serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors))
} footer: {
if let errStr = globalServersError(ss.servers.serverErrors) {
ServersErrorView(errStr: errStr)
let errs = globalServersErrors(ss.servers.serverErrors)
if !errs.isEmpty {
ForEach(errs, id: \.self) { err in
ServersErrorView(errStr: err)
}
} else if !ss.servers.serverErrors.isEmpty {
ServersErrorView(errStr: NSLocalizedString("Errors in servers configuration.", comment: "servers error"))
}
if let warnStr = globalServersWarning(ss.servers.serverWarnings) {
ServersWarningView(warnStr: warnStr)
ForEach(globalServersWarnings(ss.servers.serverWarnings), id: \.self) { warn in
ServersWarningView(warnStr: warn)
}
}
@@ -397,17 +400,12 @@ struct ServersWarningView: View {
}
}
func globalServersError(_ serverErrors: [UserServersError]) -> String? {
for err in serverErrors {
if let errStr = err.globalError {
return errStr
}
}
return nil
func globalServersErrors(_ serverErrors: [UserServersError]) -> [String] {
serverErrors.compactMap { $0.globalError }
}
func globalServersWarning(_ serverWarnings: [UserServersWarning]) -> String? {
for warn in serverWarnings {
func globalServersWarnings(_ serverWarnings: [UserServersWarning]) -> [String] {
serverWarnings.map { warn in
switch warn {
case let .noChatRelays(user):
let text = NSLocalizedString("No chat relays enabled.", comment: "servers warning")
@@ -417,9 +415,16 @@ func globalServersWarning(_ serverWarnings: [UserServersWarning]) -> String? {
user.localDisplayName
) + " " + text
} else { return text }
case let .noNamesServers(user):
let text = NSLocalizedString("No servers to resolve names.", comment: "servers warning")
if let user = user {
return String.localizedStringWithFormat(
NSLocalizedString("For chat profile %@:", comment: "servers warning"),
user.localDisplayName
) + " " + text
} else { return text }
}
}
return nil
}
func bindingForChatRelays(_ userServers: Binding<[UserOperatorServers]>, _ opIndex: Int) -> Binding<[UserChatRelay]> {
@@ -52,10 +52,16 @@ struct OperatorView: View {
Text("Operator")
.foregroundColor(theme.colors.secondary)
} footer: {
if let errStr = globalServersError(serverErrors) {
ServersErrorView(errStr: errStr)
} else if let warnStr = globalServersWarning(serverWarnings) {
ServersWarningView(warnStr: warnStr)
let errs = globalServersErrors(serverErrors)
let warns = globalServersWarnings(serverWarnings)
if !errs.isEmpty {
ForEach(errs, id: \.self) { err in
ServersErrorView(errStr: err)
}
} else if !warns.isEmpty {
ForEach(warns, id: \.self) { warn in
ServersWarningView(warnStr: warn)
}
} else {
switch (userServers[operatorIndex].operator_.conditionsAcceptance) {
case let .accepted(acceptedAt, _):
@@ -105,6 +111,10 @@ struct OperatorView: View {
.onChange(of: userServers[operatorIndex].operator_.smpRoles.proxy) { _ in
validateServers_($userServers, $serverErrors, $serverWarnings)
}
Toggle("To resolve names", isOn: $userServers[operatorIndex].operator_.smpRoles.names)
.onChange(of: userServers[operatorIndex].operator_.smpRoles.names) { _ in
validateServers_($userServers, $serverErrors, $serverWarnings)
}
} header: {
Text("Use for messages")
.foregroundColor(theme.colors.secondary)
@@ -169,10 +169,16 @@ struct YourServersView: View {
.hidden()
}
} footer: {
if let errStr = globalServersError(serverErrors) {
ServersErrorView(errStr: errStr)
} else if let warnStr = globalServersWarning(serverWarnings) {
ServersWarningView(warnStr: warnStr)
let errs = globalServersErrors(serverErrors)
let warns = globalServersWarnings(serverWarnings)
if !errs.isEmpty {
ForEach(errs, id: \.self) { err in
ServersErrorView(errStr: err)
}
} else if !warns.isEmpty {
ForEach(warns, id: \.self) { warn in
ServersWarningView(warnStr: warn)
}
}
}
@@ -16,6 +16,7 @@ struct PrivacySettings: View {
@AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS, store: groupDefaults) private var useLinkPreviews = true
@AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
@AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) private var verifySimplexNames = false
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
@@ -193,6 +194,9 @@ struct PrivacySettings: View {
m.draftChatId = nil
}
}
settingsRow("number", color: theme.colors.secondary) {
Toggle("Verify SimpleX names", isOn: $verifySimplexNames)
}
} header: {
Text("Chats")
.foregroundColor(theme.colors.secondary)
@@ -32,6 +32,7 @@ let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_D
let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved to app group
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
let DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES = "privacyVerifySimplexNames"
let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft"
let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen"
let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet"
@@ -99,6 +100,7 @@ let appDefaults: [String: Any] = [
DEFAULT_PRIVACY_LINK_PREVIEWS: true,
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue,
DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true,
DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES: false,
DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true,
DEFAULT_PRIVACY_PROTECT_SCREEN: false,
DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false,
@@ -191,6 +191,29 @@ struct UserAddressView: View {
}
}
Section {
NavigationLink {
let simplexName = if let d = chatModel.currentUser?.profile.contactDomain?.shortName { "@\(d)" } else { "" }
SetSimplexDomainView(
title: "Your SimpleX name",
footer: "Let people connect to you via name registered with your SimpleX address.",
prompt: "@yourname.testing",
simplexName: simplexName,
save: { simplexDomain in
do {
let u = try await apiSetUserDomain(simplexDomain)
await MainActor.run { chatModel.updateUser(u) }
return true
} catch {
return false
}
}
)
} label: {
Label("Your SimpleX name", systemImage: "at")
}
}
Section {
createOneTimeLinkButton()
} header: {
@@ -688,6 +711,59 @@ private func saveAddressSettings(_ settings: AddressSettingsState, _ savedSettin
}
}
struct SetSimplexDomainView: View {
let title: LocalizedStringKey
let footer: LocalizedStringKey
let prompt: String
@State var simplexName: String
let save: (String?) async -> Bool
@Environment(\.dismiss) var dismiss
@EnvironmentObject var theme: AppTheme
@State private var saving = false
var body: some View {
List {
Section {
TextField(prompt, text: $simplexName)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
} header: {
Text(verbatim: "")
} footer: {
Text(footer).foregroundColor(theme.colors.secondary)
}
Section {
Button {
saving = true
Task {
let ok = await save(normalized())
await MainActor.run {
saving = false
if ok { dismiss() }
}
}
} label: {
Text("Save")
}
.disabled(saving)
}
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.large)
}
private func normalized() -> String? {
let s = simplexName.trimmingCharacters(in: .whitespacesAndNewlines)
return s.isEmpty
? nil
: addSimplexTLD(s.hasPrefix("@") || s.hasPrefix("#") ? String(s.dropFirst()) : s)
}
private func addSimplexTLD(_ d: String) -> String {
if d.contains(".") { d } else { "\(d).simplex" }
}
}
struct UserAddressView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
+8 -8
View File
@@ -183,8 +183,8 @@
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a */; };
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
@@ -563,8 +563,8 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
@@ -735,8 +735,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -822,8 +822,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-67j0xDFDAaz6bMQcxTdG4Z.a */,
);
path = Libraries;
sourceTree = "<group>";
+58 -28
View File
@@ -84,10 +84,8 @@ extension ChatAPIResult {
// Spec: spec/api.md#decodeAPIResult
public func decodeAPIResult<R: ChatAPIResult>(_ d: Data) -> APIResult<R> {
// print("decodeAPIResult \(String(describing: R.self))")
do {
// return try withStackSizeLimit { try jsonDecoder.decode(APIResult<R>.self, from: d) }
return try jsonDecoder.decode(APIResult<R>.self, from: d)
return try withLargeStack { try jsonDecoder.decode(APIResult<R>.self, from: d) }
} catch {}
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
if let (_, jErr) = getOWSF(j, "error") {
@@ -106,31 +104,50 @@ public func decodeAPIResult<R: ChatAPIResult>(_ d: Data) -> APIResult<R> {
// Default stack size for the main thread is 1mb, for secondary threads - 512 kb.
// This function can be used to test what size is used (or to increase available stack size).
// Stack size must be a multiple of system page size (16kb).
//private let stackSizeLimit: Int = 256 * 1024
//
//private func withStackSizeLimit<T>(_ f: @escaping () throws -> T) throws -> T {
// let semaphore = DispatchSemaphore(value: 0)
// var result: Result<T, Error>?
// let thread = Thread {
// do {
// result = .success(try f())
// } catch {
// result = .failure(error)
// }
// semaphore.signal()
// }
//
// thread.stackSize = stackSizeLimit
// thread.qualityOfService = Thread.current.qualityOfService
// thread.start()
//
// semaphore.wait()
//
// switch result! {
// case let .success(r): return r
// case let .failure(e): throw e
// }
//}
private let stackSizeLimit: Int = 16 * 1024 * 1024
private final class LargeStackRunner: NSObject {
static let shared = LargeStackRunner()
private let serialize = NSLock() // serialize submitters
private let jobReady = DispatchSemaphore(value: 0)
private let jobDone = DispatchSemaphore(value: 0)
private var job: (() -> Void)?
private var thread: Thread? = nil
private override init() {
super.init()
let t = Thread(target: self, selector: #selector(loop), object: nil)
t.stackSize = stackSizeLimit
t.name = "chat.simplex.decoding"
t.qualityOfService = .default
t.start()
thread = t
}
@objc private func loop() {
while true {
jobReady.wait()
job?()
jobDone.signal()
}
}
func run<T>(_ f: @escaping () throws -> T) throws -> T {
serialize.lock()
defer { serialize.unlock() }
var result: Result<T, Error>!
job = { result = Result(catching: f) }
jobReady.signal()
jobDone.wait()
job = nil
return try result.get()
}
}
func withLargeStack<T>(_ work: @escaping () throws -> T) throws -> T {
try LargeStackRunner.shared.run(work)
}
public func parseApiChats(_ jResp: NSDictionary) -> (user: UserRef, chats: [ChatData])? {
if let jApiChats = jResp["apiChats"] as? NSDictionary,
@@ -167,6 +184,10 @@ public struct CreatedConnLink: Decodable, Hashable {
public func simplexChatUri(short: Bool = true) -> String {
short ? (connShortLink ?? simplexChatLink(connFullLink)) : simplexChatLink(connFullLink)
}
public var cmdString: String {
connFullLink + (connShortLink.map { " \($0)"} ?? "")
}
}
public func simplexChatLink(_ uri: String) -> String {
@@ -741,6 +762,7 @@ public enum ChatErrorType: Decodable, Hashable {
case chatNotStopped
case chatStoreChanged
case invalidConnReq
case simplexDomainNotReady(simplexDomain: SimplexDomain, simplexDomainError: SimplexDomainError)
case unsupportedConnReq
case invalidChatMessage(connection: Connection, message: String)
case connReqMessageProhibited
@@ -896,6 +918,13 @@ public enum AgentErrorType: Decodable, Hashable {
case INTERNAL(internalErr: String)
case CRITICAL(offerRestart: Bool, criticalErr: String)
case INACTIVE
case NO_NAME_SERVERS
}
public enum NameErrorType: Decodable, Hashable {
case NO_RESOLVER
case NOT_FOUND
case RESOLVER(resolverErr: String)
}
public enum CommandErrorType: Decodable, Hashable {
@@ -937,6 +966,7 @@ public enum ProtocolErrorType: Decodable, Hashable {
case LARGE_MSG
case EXPIRED
case INTERNAL
case NAME(nameErr: NameErrorType)
}
public enum ProxyError: Decodable, Hashable {
+71 -11
View File
@@ -119,7 +119,8 @@ public struct Profile: Codable, NamedChat, Hashable {
image: String? = nil,
contactLink: String? = nil,
preferences: Preferences? = nil,
peerType: ChatPeerType? = nil
peerType: ChatPeerType? = nil,
contactDomain: SimplexDomainClaim? = nil
) {
self.displayName = displayName
self.fullName = fullName
@@ -127,6 +128,7 @@ public struct Profile: Codable, NamedChat, Hashable {
self.image = image
self.contactLink = contactLink
self.preferences = preferences
self.contactDomain = contactDomain
}
public var displayName: String
@@ -139,6 +141,7 @@ public struct Profile: Codable, NamedChat, Hashable {
// the badge proof from the wire profile - opaque to the UI, round-tripped to the core (apiPrepareContact)
public var badge: BadgeProof?
public var localAlias: String { get { "" } }
public var contactDomain: SimplexDomainClaim?
var profileViewName: String {
(fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))"
@@ -161,7 +164,9 @@ public struct LocalProfile: Codable, NamedChat, Hashable {
preferences: Preferences? = nil,
peerType: ChatPeerType? = nil,
localBadge: LocalBadge? = nil,
localAlias: String
localAlias: String,
contactDomain: SimplexDomainClaim? = nil,
contactDomainVerified: Bool? = nil
) {
self.profileId = profileId
self.displayName = displayName
@@ -173,6 +178,8 @@ public struct LocalProfile: Codable, NamedChat, Hashable {
self.peerType = peerType
self.localBadge = localBadge
self.localAlias = localAlias
self.contactDomain = contactDomain
self.contactDomainVerified = contactDomainVerified
}
public var profileId: Int64
@@ -185,6 +192,8 @@ public struct LocalProfile: Codable, NamedChat, Hashable {
public var peerType: ChatPeerType?
public var localBadge: LocalBadge?
public var localAlias: String
public var contactDomain: SimplexDomainClaim?
public var contactDomainVerified: Bool?
var profileViewName: String {
localAlias == ""
@@ -2507,7 +2516,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
public var groupId: Int64
public var useRelays: Bool
public var relayOwnStatus: RelayStatus? = nil
var localDisplayName: GroupName
public var localDisplayName: GroupName
public var groupProfile: GroupProfile
public var businessChat: BusinessChatInfo?
public var fullGroupPreferences: FullGroupPreferences
@@ -2534,6 +2543,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
public var chatTags: [Int64]
public var chatItemTTL: Int64?
public var localAlias: String
public var groupDomainVerified: Bool?
public var isOwner: Bool {
return membership.memberRole == .owner && membership.memberCurrent
@@ -2614,19 +2624,37 @@ public enum GroupType: Codable, Hashable {
}
public struct PublicGroupAccess: Codable, Hashable {
public init(groupWebPage: String? = nil, groupDomain: String? = nil, domainWebPage: Bool = false, allowEmbedding: Bool = false) {
public init(groupWebPage: String? = nil, groupDomainClaim: SimplexDomainClaim? = nil, domainWebPage: Bool = false, allowEmbedding: Bool = false) {
self.groupWebPage = groupWebPage
self.groupDomain = groupDomain
self.groupDomainClaim = groupDomainClaim
self.domainWebPage = domainWebPage
self.allowEmbedding = allowEmbedding
}
public var groupWebPage: String?
public var groupDomain: String?
public var groupDomainClaim: SimplexDomainClaim?
public var domainWebPage: Bool = false
public var allowEmbedding: Bool = false
}
public struct SimplexDomainClaim: Codable, Hashable {
public init(domain: String, proof: SimplexDomainProof? = nil) {
self.domain = domain
self.proof = proof
}
public var domain: String
public var proof: SimplexDomainProof?
public var shortName: String {
domain.hasSuffix(".simplex") ? String(domain.dropLast(".simplex".count)) : domain
}
}
public enum SimplexDomainError: Decodable, Hashable {
case noValidLink
case unknownDomain
}
public struct RelayCapabilities: Codable, Hashable {
public var webDomain: String?
}
@@ -5258,28 +5286,60 @@ public enum SimplexLinkType: String, Decodable, Hashable {
}
}
public struct SimplexNameInfo: Decodable, Equatable, Hashable {
public struct SimplexNameInfo: Codable, Equatable, Hashable {
public var nameType: SimplexNameType
public var nameDomain: SimplexNameDomain
public var nameDomain: SimplexDomain
public init(nameType: SimplexNameType, nameDomain: SimplexDomain) {
self.nameType = nameType
self.nameDomain = nameDomain
}
}
public struct SimplexNameDomain: Decodable, Equatable, Hashable {
public struct SimplexDomain: Codable, Equatable, Hashable {
public var nameTLD: SimplexTLD
public var domain: String
public var subDomain: [String]
// mirrors backend fullDomainName: reverse(subDomain) ++ [domain] ++ tld
public var fullDomainName: String {
let tld: [String]
switch nameTLD {
case .simplex: tld = ["simplex"]
case .testing: tld = ["testing"]
case .web: tld = []
}
return (subDomain.reversed() + [domain] + tld).joined(separator: ".")
}
public var cmdString: String {
"domain=\(fullDomainName)"
}
public init(nameTLD: SimplexTLD, domain: String, subDomain: [String]) {
self.nameTLD = nameTLD
self.domain = domain
self.subDomain = subDomain
}
}
public enum SimplexTLD: String, Decodable, Hashable {
public enum SimplexTLD: String, Codable, Hashable {
case simplex
case testing
case web
}
public enum SimplexNameType: String, Decodable, Hashable {
public enum SimplexNameType: String, Codable, Hashable {
case publicGroup
case contact
}
public struct SimplexDomainProof: Codable, Hashable {
public var linkOwnerId: String?
public var presHeader: String
public var signature: String
}
public enum FormatColor: String, Decodable, Hashable {
case red = "red"
case green = "green"
@@ -2039,14 +2039,15 @@ data class Profile(
val peerType: ChatPeerType? = null,
// the badge proof from the wire profile: not interpreted by the UI (display uses crypto-free LocalBadge),
// but preserved so passing a link profile back to the core (apiPrepareContact) keeps the proof
val badge: BadgeProof? = null
val badge: BadgeProof? = null,
val contactDomain: SimplexDomainClaim? = null
): NamedChat {
val profileViewName: String
get() {
return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)"
}
fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType)
fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain)
companion object {
val sampleData = Profile(
@@ -2068,11 +2069,13 @@ data class LocalProfile(
val contactLink: String? = null,
val preferences: ChatPreferences? = null,
val peerType: ChatPeerType? = null,
val localBadge: LocalBadge? = null
val localBadge: LocalBadge? = null,
val contactDomain: SimplexDomainClaim? = null,
val contactDomainVerified: Boolean? = null
): NamedChat {
val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" }
fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType)
fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain)
companion object {
val sampleData = LocalProfile(
@@ -2198,6 +2201,7 @@ data class GroupInfo (
val chatTags: List<Long>,
val chatItemTTL: Long?,
override val localAlias: String,
val groupDomainVerified: Boolean? = null,
): SomeChat, NamedChat {
override val chatType get() = ChatType.Group
override val id get() = "#$groupId"
@@ -2319,10 +2323,18 @@ object GroupTypeSerializer : KSerializer<GroupType> {
}
}
@Serializable
data class SimplexDomainClaim(
val domain: String,
val proof: SimplexDomainProof? = null
) {
val shortName: String get() = domain.removeSuffix(".simplex")
}
@Serializable
data class PublicGroupAccess(
val groupWebPage: String? = null,
val groupDomain: String? = null,
val groupDomainClaim: SimplexDomainClaim? = null,
val domainWebPage: Boolean = false,
val allowEmbedding: Boolean = false
)
@@ -4874,15 +4886,27 @@ enum class SimplexLinkType(val linkType: String) {
@Serializable
data class SimplexNameInfo(
val nameType: SimplexNameType,
val nameDomain: SimplexNameDomain
val nameDomain: SimplexDomain
)
@Serializable
data class SimplexNameDomain(
data class SimplexDomain(
val nameTLD: SimplexTLD,
val domain: String,
val subDomain: List<String>
)
) {
// mirrors backend fullDomainName: reverse(subDomain) + [domain] + tld
val fullDomainName: String get() {
val tld = when (nameTLD) {
SimplexTLD.simplex -> listOf("simplex")
SimplexTLD.testing -> listOf("testing")
SimplexTLD.web -> emptyList()
}
return (subDomain.reversed() + domain + tld).joinToString(".")
}
val cmdString: String get() = "domain=$fullDomainName"
}
@Serializable
enum class SimplexTLD {
@@ -4897,6 +4921,14 @@ enum class SimplexNameType {
@SerialName("contact") contact
}
// peer's signed name claim; UI only checks presence
@Serializable
data class SimplexDomainProof(
val linkOwnerId: String? = null,
val presHeader: String,
val signature: String
)
@Serializable
enum class FormatColor(val color: String) {
red("red"),
@@ -122,6 +122,7 @@ class AppPreferences {
val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true)
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
val privacyVerifySimplexNames = mkBoolPreference(SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES, false)
val privacyLinkPreviewsShowAlert = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, true)
val privacySanitizeLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SANITIZE_LINKS, false)
// TODO remove
@@ -397,6 +398,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages"
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
private const val SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES = "PrivacyVerifySimplexNames"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "PrivacyLinkPreviewsShowAlert"
private const val SHARED_PREFS_PRIVACY_SANITIZE_LINKS = "PrivacySanitizeLinks"
private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" // TODO remove
@@ -1555,6 +1557,46 @@ object ChatController {
generalGetString(MR.strings.link_requires_newer_app_version_please_upgrade)
)
}
r is API.Error && r.err is ChatError.ChatErrorChat
&& r.err.errorType is ChatErrorType.SimplexDomainNotReady -> {
val domain = r.err.errorType.simplexDomain.fullDomainName
if (r.err.errorType.simplexDomainError is SimplexDomainError.NoValidLink) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.simplex_name_no_valid_link),
generalGetString(MR.strings.simplex_name_no_valid_link_desc).format(domain)
)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.simplex_name_unconfirmed),
generalGetString(MR.strings.simplex_name_unconfirmed_desc).format(domain)
)
}
}
r is API.Error && r.err is ChatError.ChatErrorAgent
&& r.err.agentError is AgentErrorType.NO_NAME_SERVERS -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.simplex_name_error),
generalGetString(MR.strings.simplex_name_no_servers_desc)
)
}
r is API.Error && r.err is ChatError.ChatErrorAgent
&& r.err.agentError is AgentErrorType.SMP
&& r.err.agentError.smpErr is SMPErrorType.NAME -> {
when (val nameErr = r.err.agentError.smpErr.nameErr) {
is NameErrorType.NOT_FOUND -> AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.simplex_name_not_found),
generalGetString(MR.strings.simplex_name_not_found_desc)
)
is NameErrorType.NO_RESOLVER -> AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.simplex_name_error),
generalGetString(MR.strings.simplex_name_server_no_resolver_desc).format(r.err.agentError.serverAddress)
)
is NameErrorType.RESOLVER -> AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.simplex_name_error),
generalGetString(MR.strings.simplex_name_resolver_error_desc).format(nameErr.resolverErr)
)
}
}
r is API.Error && r.err is ChatError.ChatErrorAgent
&& r.err.agentError is AgentErrorType.SMP
&& r.err.agentError.smpErr is SMPErrorType.AUTH -> {
@@ -1587,11 +1629,29 @@ object ChatController {
}
}
// owner-specific wording for setting one's own/channel name; null for other errors (handled by apiConnectResponseAlert)
fun simplexNameOwnerError(err: ChatError, isChannel: Boolean): String? =
if (err is ChatError.ChatErrorChat && err.errorType is ChatErrorType.SimplexDomainNotReady && err.errorType.simplexDomainError is SimplexDomainError.NoValidLink) {
val domain = err.errorType.simplexDomain.fullDomainName
if (isChannel) generalGetString(MR.strings.simplex_name_owner_no_channel_link).format(domain)
else generalGetString(MR.strings.simplex_name_owner_no_address).format(domain)
} else null
fun connErrorText(e: ChatError): String = when {
e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.InvalidConnReq ->
generalGetString(MR.strings.invalid_connection_link)
e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.UnsupportedConnReq ->
generalGetString(MR.strings.unsupported_connection_link)
e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.SimplexDomainNotReady ->
if (e.errorType.simplexDomainError is SimplexDomainError.NoValidLink)
generalGetString(MR.strings.simplex_name_no_valid_link)
else generalGetString(MR.strings.simplex_name_unconfirmed)
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.NO_NAME_SERVERS ->
generalGetString(MR.strings.simplex_name_error)
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.NAME ->
if (e.agentError.smpErr.nameErr is NameErrorType.NOT_FOUND)
generalGetString(MR.strings.simplex_name_not_found)
else generalGetString(MR.strings.simplex_name_error)
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH ->
generalGetString(MR.strings.connection_error_auth)
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.BLOCKED ->
@@ -1604,18 +1664,18 @@ object ChatController {
"${generalGetString(MR.strings.error_prefix)}: ${e.string}"
}
suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? {
suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = null): Chat? {
val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null }
val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData))
val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData, verifiedDomain))
if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh)
Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData): Chat? {
suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain? = null): Chat? {
val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null }
val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData))
val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData, verifiedDomain))
if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh)
Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}")
@@ -1762,6 +1822,38 @@ object ChatController {
}
}
// name is the encoded SimplexName (e.g. "@alice.simplex"); null clears it. Throws on rejection.
suspend fun apiSetUserDomain(rh: Long?, simplexDomain: String?): User {
val userId = currentUserId("apiSetUserDomain")
val r = sendCmd(rh, CC.ApiSetUserDomain(userId, simplexDomain))
return when {
r is API.Result && r.res is CR.UserProfileUpdated -> r.res.user.updateRemoteHostId(rh)
r is API.Result && r.res is CR.UserProfileNoChange -> r.res.user.updateRemoteHostId(rh)
else -> {
if (r is API.Error) {
val ownerMsg = simplexNameOwnerError(r.err, isChannel = false)
if (ownerMsg != null) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), ownerMsg)
else apiConnectResponseAlert(r)
}
throw Exception("failed to set SimpleX name: ${r.responseType} ${r.details}")
}
}
}
suspend fun apiVerifyContactDomain(rh: Long?, contactId: Long): Pair<Contact, String?>? {
val r = sendCmd(rh, CC.ApiVerifyContactDomain(contactId))
if (r is API.Result && r.res is CR.ContactDomainVerified) return r.res.contact to r.res.verificationFailure
Log.e(TAG, "apiVerifyContactDomain bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiVerifyGroupDomain(rh: Long?, groupId: Long): Pair<GroupInfo, String?>? {
val r = sendCmd(rh, CC.ApiVerifyGroupDomain(groupId))
if (r is API.Result && r.res is CR.GroupDomainVerified) return r.res.groupInfo to r.res.verificationFailure
Log.e(TAG, "apiVerifyGroupDomain bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiSetContactPrefs(rh: Long?, contactId: Long, prefs: ChatPreferences): Contact? {
val r = sendCmd(rh, CC.ApiSetContactPrefs(contactId, prefs))
if (r is API.Result && r.res is CR.ContactPrefsUpdated) return r.res.toContact
@@ -2289,7 +2381,7 @@ object ChatController {
return when {
r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup
r is API.Error -> {
AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "${r.err.string}")
AlertManager.shared.showAlertMsg(generalGetString(errorTitle), r.err.string)
null
}
else -> {
@@ -2303,6 +2395,23 @@ object ChatController {
}
}
suspend fun apiSetPublicGroupAccess(rh: Long?, groupId: Long, access: PublicGroupAccess): GroupInfo? {
val r = sendCmd(rh, CC.ApiSetPublicGroupAccess(groupId, access))
return when {
r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup
r is API.Error -> {
val ownerMsg = simplexNameOwnerError(r.err, isChannel = true)
if (ownerMsg != null) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), ownerMsg)
else apiConnectResponseAlert(r)
null
}
else -> {
Log.e(TAG, "apiSetPublicGroupAccess bad response: ${r.responseType} ${r.details}")
null
}
}
}
suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? {
val r = sendCmdWithRetry(rh, CC.APICreateGroupLink(groupId, memberRole))
if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink
@@ -3705,6 +3814,7 @@ sealed class CC {
class ApiLeaveGroup(val groupId: Long): CC()
class ApiListMembers(val groupId: Long): CC()
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC()
class ApiSetPublicGroupAccess(val groupId: Long, val access: PublicGroupAccess): CC()
class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIDeleteGroupLink(val groupId: Long): CC()
@@ -3752,8 +3862,8 @@ sealed class CC {
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
class APIConnectPlan(val userId: Long, val connLink: String, val linkOwnerSig: LinkOwnerSig? = null): CC()
class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC()
class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData): CC()
class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData, val verifiedDomain: SimplexDomain? = null): CC()
class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData, val verifiedDomain: SimplexDomain? = null): CC()
class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC()
class APIChangePreparedGroupUser(val groupId: Long, val newUserId: Long): CC()
class APIConnectPreparedContact(val contactId: Long, val incognito: Boolean, val msg: MsgContent?): CC()
@@ -3775,6 +3885,9 @@ sealed class CC {
class ApiShowMyAddress(val userId: Long): CC()
class ApiAddMyAddressShortLink(val userId: Long): CC()
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
class ApiSetUserDomain(val userId: Long, val simplexDomain: String?): CC()
class ApiVerifyContactDomain(val contactId: Long): CC()
class ApiVerifyGroupDomain(val groupId: Long): CC()
class ApiSetAddressSettings(val userId: Long, val addressSettings: AddressSettings): CC()
class ApiGetCallInvitations: CC()
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
@@ -3960,8 +4073,8 @@ sealed class CC {
val sigStr = if (linkOwnerSig != null) " sig=${json.encodeToString(linkOwnerSig)}" else ""
"/_connect plan $userId $connLink$sigStr"
}
is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}"
is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} direct=${onOff(directLink)} ${json.encodeToString(groupShortLinkData)}"
is APIPrepareContact -> "/_prepare contact $userId ${connLink.cmdString}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(contactShortLinkData)}"
is APIPrepareGroup -> "/_prepare group $userId ${connLink.cmdString} direct=${onOff(directLink)}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(groupShortLinkData)}"
is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId"
is APIChangePreparedGroupUser -> "/_set group user #$groupId $newUserId"
is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)}${maybeContent(msg)}"
@@ -3983,6 +4096,10 @@ sealed class CC {
is ApiShowMyAddress -> "/_show_address $userId"
is ApiAddMyAddressShortLink -> "/_short_link_address $userId"
is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}"
is ApiSetUserDomain -> "/_set domain $userId" + (if (simplexDomain != null) " $simplexDomain" else "")
is ApiSetPublicGroupAccess -> "/_public group access #$groupId ${json.encodeToString(access)}"
is ApiVerifyContactDomain -> "/_verify domain @$contactId"
is ApiVerifyGroupDomain -> "/_verify domain #$groupId"
is ApiSetAddressSettings -> "/_address_settings $userId ${json.encodeToString(addressSettings)}"
is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId"
is ApiRejectContact -> "/_reject $contactReqId"
@@ -4164,6 +4281,10 @@ sealed class CC {
is ApiShowMyAddress -> "apiShowMyAddress"
is ApiAddMyAddressShortLink -> "apiAddMyAddressShortLink"
is ApiSetProfileAddress -> "apiSetProfileAddress"
is ApiSetUserDomain -> "apiSetUserDomain"
is ApiSetPublicGroupAccess -> "apiSetPublicGroupAccess"
is ApiVerifyContactDomain -> "apiVerifyContactDomain"
is ApiVerifyGroupDomain -> "apiVerifyGroupDomain"
is ApiSetAddressSettings -> "apiSetAddressSettings"
is ApiAcceptContact -> "apiAcceptContact"
is ApiRejectContact -> "apiRejectContact"
@@ -4443,8 +4564,8 @@ data class ServerOperator(
serverDomains = listOf("simplex.im"),
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false),
enabled = true,
smpRoles = ServerRoles(storage = true, proxy = true),
xftpRoles = ServerRoles(storage = true, proxy = true)
smpRoles = ServerRoles(storage = true, proxy = true, names = true),
xftpRoles = ServerRoles(storage = true, proxy = true, names = false)
)
}
@@ -4504,7 +4625,8 @@ data class ServerOperator(
@Serializable
data class ServerRoles(
val storage: Boolean,
val proxy: Boolean
val proxy: Boolean,
val names: Boolean
)
@Serializable
@@ -4526,8 +4648,8 @@ data class UserOperatorServers(
serverDomains = emptyList(),
conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false),
enabled = false,
smpRoles = ServerRoles(storage = true, proxy = true),
xftpRoles = ServerRoles(storage = true, proxy = true)
smpRoles = ServerRoles(storage = true, proxy = true, names = true),
xftpRoles = ServerRoles(storage = true, proxy = true, names = false)
)
companion object {
@@ -4613,6 +4735,7 @@ sealed class UserServersError {
@Serializable
sealed class UserServersWarning {
@Serializable @SerialName("noChatRelays") data class NoChatRelays(val user: UserRef? = null): UserServersWarning()
@Serializable @SerialName("noNamesServers") data class NoNamesServers(val user: UserRef? = null): UserServersWarning()
val globalWarning: String?
get() = when (this) {
@@ -4622,6 +4745,12 @@ sealed class UserServersWarning {
String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + " " + text
} else text
}
is NoNamesServers -> {
val text = generalGetString(MR.strings.no_names_servers_enabled)
if (user != null) {
String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + " " + text
} else text
}
}
}
@@ -6462,6 +6591,8 @@ sealed class CR {
@Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR()
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR()
@Serializable @SerialName("contactDomainVerified") class ContactDomainVerified(val user: UserRef, val contact: Contact, val verificationFailure: String? = null): CR()
@Serializable @SerialName("groupDomainVerified") class GroupDomainVerified(val user: UserRef, val groupInfo: GroupInfo, val verificationFailure: String? = null): CR()
@Serializable @SerialName("groupLinkDataUpdated") class GroupLinkDataUpdated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List<GroupRelay>, val relaysChanged: Boolean): CR()
@Serializable @SerialName("groupRelayUpdated") class GroupRelayUpdated(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val groupRelay: GroupRelay): CR()
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR()
@@ -6653,6 +6784,8 @@ sealed class CR {
is JoinedGroupMember -> "joinedGroupMember"
is ConnectedToGroupMember -> "connectedToGroupMember"
is GroupUpdated -> "groupUpdated"
is ContactDomainVerified -> "contactDomainVerified"
is GroupDomainVerified -> "groupDomainVerified"
is GroupLinkDataUpdated -> "groupLinkDataUpdated"
is GroupRelayUpdated -> "groupRelayUpdated"
is GroupLinkCreated -> "groupLinkCreated"
@@ -6837,6 +6970,8 @@ sealed class CR {
is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact")
is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
is ContactDomainVerified -> withUser(user, "contact: ${json.encodeToString(contact)}\nverificationFailure: $verificationFailure")
is GroupDomainVerified -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nverificationFailure: $verificationFailure")
is GroupLinkDataUpdated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays\nrelaysChanged: $relaysChanged")
is GroupRelayUpdated -> withUser(user, "groupInfo: $groupInfo\nmember: $member\ngroupRelay: $groupRelay")
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink")
@@ -6948,6 +7083,8 @@ data class CreatedConnLink(val connFullLink: String, val connShortLink: String?)
fun simplexChatUri(short: Boolean): String =
if (short) connShortLink ?: simplexChatLink(connFullLink)
else simplexChatLink(connFullLink)
val cmdString: String get() = connFullLink + (if (connShortLink == null) "" else " $connShortLink")
}
fun simplexChatLink(uri: String): String =
@@ -6960,6 +7097,12 @@ sealed class OwnerVerification {
@Serializable @SerialName("failed") class Failed(val reason: String) : OwnerVerification()
}
@Serializable
sealed class SimplexDomainError {
@Serializable @SerialName("noValidLink") object NoValidLink : SimplexDomainError()
@Serializable @SerialName("unknownDomain") object UnknownDomain : SimplexDomainError()
}
@Serializable
sealed class ConnectionPlan {
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
@@ -6978,7 +7121,7 @@ sealed class InvitationLinkPlan {
@Serializable
sealed class ContactAddressPlan {
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null): ContactAddressPlan()
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null, val ownerVerification: OwnerVerification? = null, val verifiedDomain: SimplexDomain? = null): ContactAddressPlan()
@Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan()
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan()
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan()
@@ -6988,7 +7131,7 @@ sealed class ContactAddressPlan {
@Serializable
sealed class GroupLinkPlan {
@Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = null): GroupLinkPlan()
@Serializable @SerialName("ok") class Ok(val groupSLinkInfo_: GroupShortLinkInfo? = null, val groupSLinkData_: GroupShortLinkData? = null, val ownerVerification: OwnerVerification? = null, val verifiedDomain: SimplexDomain? = null): GroupLinkPlan()
@Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan()
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan()
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan()
@@ -7297,6 +7440,7 @@ sealed class ChatErrorType {
is ChatStoreChanged -> "chatStoreChanged"
is ConnectionPlanChatError -> "connectionPlan"
is InvalidConnReq -> "invalidConnReq"
is SimplexDomainNotReady -> "simplexDomainNotReady"
is UnsupportedConnReq -> "unsupportedConnReq"
is InvalidChatMessage -> "invalidChatMessage"
is ConnReqMessageProhibited -> "connReqMessageProhibited"
@@ -7379,6 +7523,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
@Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType()
@Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType()
@Serializable @SerialName("simplexDomainNotReady") class SimplexDomainNotReady(val simplexDomain: SimplexDomain, val simplexDomainError: SimplexDomainError): ChatErrorType()
@Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType()
@Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType()
@Serializable @SerialName("connReqMessageProhibited") object ConnReqMessageProhibited: ChatErrorType()
@@ -7647,6 +7792,7 @@ sealed class AgentErrorType {
is INTERNAL -> "INTERNAL $internalErr"
is CRITICAL -> "CRITICAL $offerRestart $criticalErr"
is INACTIVE -> "INACTIVE"
is NO_NAME_SERVERS -> "NO_NAME_SERVERS"
}
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType, val errContext: String): AgentErrorType()
@@ -7661,6 +7807,19 @@ sealed class AgentErrorType {
@Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
@Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType()
@Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType()
@Serializable @SerialName("NO_NAME_SERVERS") object NO_NAME_SERVERS: AgentErrorType()
}
@Serializable
sealed class NameErrorType {
val string: String get() = when (this) {
is NO_RESOLVER -> "NO_RESOLVER"
is NOT_FOUND -> "NOT_FOUND"
is RESOLVER -> "RESOLVER $resolverErr"
}
@Serializable @SerialName("NO_RESOLVER") object NO_RESOLVER: NameErrorType()
@Serializable @SerialName("NOT_FOUND") object NOT_FOUND: NameErrorType()
@Serializable @SerialName("RESOLVER") class RESOLVER(val resolverErr: String): NameErrorType()
}
@Serializable
@@ -7730,6 +7889,7 @@ sealed class SMPErrorType {
is LARGE_MSG -> "LARGE_MSG"
is EXPIRED -> "EXPIRED"
is INTERNAL -> "INTERNAL"
is NAME -> "NAME ${nameErr.string}"
}
@Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType()
@Serializable @SerialName("SESSION") class SESSION: SMPErrorType()
@@ -7744,6 +7904,7 @@ sealed class SMPErrorType {
@Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType()
@Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType()
@Serializable @SerialName("INTERNAL") class INTERNAL: SMPErrorType()
@Serializable @SerialName("NAME") class NAME(val nameErr: NameErrorType): SMPErrorType()
}
@Serializable
@@ -757,6 +757,20 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName)
)
ChatInfoDescription(cInfo, displayName, copyNameToClipboard)
val domain = contact.profile.contactDomain
if (domain != null && (contact.profile.contactDomainVerified != null || domain.proof != null)) {
SimplexNameView(
simplexName = "@${domain.shortName}",
verified = contact.profile.contactDomainVerified,
verify = {
val rhId = chatModel.remoteHostId()
chatModel.controller.apiVerifyContactDomain(rhId, contact.contactId)?.let { (ct, reason) ->
chatModel.chatsContext.updateContact(rhId, ct)
ct.profile.contactDomainVerified to reason
}
}
)
}
}
}
@@ -0,0 +1,94 @@
package chat.simplex.common.views.chat
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.SimplexNameInfo
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
// Renders a contact's / channel's SimpleX name with its 3-state verification indicator.
// `verification`: null = not attempted, false = failed, true = verified.
// `verify` runs the verify API, updates the model and returns (newVerification, failureReason);
// null on network error. With `autoVerify`, it runs once on open when state is null.
@Composable
fun SimplexNameView(
simplexName: String,
verified: Boolean?,
verify: suspend () -> Pair<Boolean?, String?>?
) {
val scope = rememberCoroutineScope()
val inFlight = remember { mutableStateOf(false) }
val showSpinner = remember { mutableStateOf(false) }
fun runVerify(manual: Boolean) {
if (inFlight.value) return
inFlight.value = true
scope.launch {
// delay the spinner so a fast result on open doesn't flash it
val spinner = launch { delay(300); if (inFlight.value) showSpinner.value = true }
val res = try {
verify()
} catch (e: Exception) {
Log.e(TAG, "verify SimplexName: ${e.stackTraceToString()}")
null
}
spinner.cancel()
inFlight.value = false
showSpinner.value = false
if (res != null) {
val (newV, reason) = res
// show the reason on a manual run, or on an inconclusive auto run (state stayed null)
if (reason != null && (manual || newV == null)) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.simplex_name_not_verified), reason)
}
}
}
}
LaunchedEffect(Unit) {
if (chatModel.controller.appPrefs.privacyVerifySimplexNames.get() && verified == null) runVerify(manual = false)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.padding(top = DEFAULT_PADDING_HALF)
) {
Text(
simplexName,
style = MaterialTheme.typography.body2.copy(
color = if (verified == true) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
fontFamily = if (verified == true) FontFamily.Default else FontFamily.Monospace
)
)
when {
showSpinner.value ->
CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp, color = MaterialTheme.colors.secondary)
verified == true ->
Icon(painterResource(MR.images.ic_check_filled), null, Modifier.size(18.dp), tint = MaterialTheme.colors.onBackground)
verified == false ->
Icon(
painterResource(MR.images.ic_close), null, tint = Color.Red,
modifier = Modifier.size(18.dp).clickable { runVerify(manual = true) }
)
else ->
Text(
stringResource(MR.strings.verify_simplex_name_action),
color = MaterialTheme.colors.primary,
modifier = Modifier.clickable { runVerify(manual = true) }
)
}
}
}
@@ -49,7 +49,7 @@ fun ChannelWebPageView(
val trimmedPage = webPage.value.trim()
val newAccess = PublicGroupAccess(
groupWebPage = trimmedPage.ifEmpty { null },
groupDomain = access?.groupDomain,
groupDomainClaim = access?.groupDomainClaim,
domainWebPage = access?.domainWebPage ?: false,
allowEmbedding = allowEmbedding.value
)
@@ -178,6 +178,27 @@ fun ModalData.GroupChatInfoView(
manageWebPage = {
ModalManager.end.showCustomModal { close -> ChannelWebPageView(rhId, groupInfo, chatModel, close) }
},
setSimplexName = {
ModalManager.end.showCustomModal { close ->
val domain = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName
SetSimplexDomainView(
title = generalGetString(MR.strings.set_simplex_name),
footer = generalGetString(MR.strings.set_channel_simplex_name_footer),
placeholder = "#channelname.testing",
simplexName = if (domain == null) "" else "#$domain",
save = { domain ->
val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?: PublicGroupAccess()
val newAccess = access.copy(groupDomainClaim = domain?.let { SimplexDomainClaim(it) })
val gInfo = chatModel.controller.apiSetPublicGroupAccess(rhId, groupInfo.groupId, newAccess)
if (gInfo != null) {
withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, gInfo) }
true
} else false
},
close = close
)
}
},
onSearchClicked = onSearchClicked,
deletingItems = deletingItems
)
@@ -510,6 +531,7 @@ fun ModalData.GroupChatInfoLayout(
leaveGroup: () -> Unit,
manageGroupLink: () -> Unit,
manageWebPage: () -> Unit,
setSimplexName: () -> Unit,
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
onSearchClicked: () -> Unit,
deletingItems: State<Boolean>
@@ -616,6 +638,12 @@ fun ModalData.GroupChatInfoLayout(
if (groupInfo.isOwner && groupLink != null) {
anyTopSectionRowShow = true
ChannelLinkButton(manageGroupLink)
SettingsActionItem(
painterResource(MR.images.ic_tag),
stringResource(MR.strings.simplex_name),
setSimplexName,
iconColor = MaterialTheme.colors.secondary
)
} else if (channelLink != null) {
anyTopSectionRowShow = true
ChannelLinkQRCodeSection(channelLink)
@@ -945,6 +973,21 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) {
modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName)
)
ChatInfoDescription(cInfo, displayName, copyNameToClipboard)
val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess
val domain = access?.groupDomainClaim?.shortName
if (domain != null && (groupInfo.groupDomainVerified != null || access.groupDomainClaim?.proof != null)) {
SimplexNameView(
simplexName = "#${domain}",
verified = groupInfo.groupDomainVerified,
verify = {
val rhId = chatModel.remoteHostId()
chatModel.controller.apiVerifyGroupDomain(rhId, groupInfo.groupId)?.let { (gInfo, reason) ->
chatModel.chatsContext.updateGroup(rhId, gInfo)
gInfo.groupDomainVerified to reason
}
}
)
}
val webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage
if (webPage != null) {
val uriHandler = LocalUriHandler.current
@@ -1436,6 +1479,7 @@ fun PreviewGroupChatInfoLayout() {
manageGroupLink = {},
manageWebPage = {},
onSearchClicked = {},
setSimplexName = {},
deletingItems = remember { mutableStateOf(true) }
)
}
@@ -338,13 +338,10 @@ fun MarkdownText (
withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) }
withAnnotation("SIMPLEX_NAME") { a ->
val idx = a.item.toIntOrNull()
val nameInfo = (idx?.let { formattedText.getOrNull(it) }?.format as? Format.SimplexName)?.nameInfo
val (title, msg) = if (nameInfo?.nameType == SimplexNameType.contact) {
generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version)
} else {
generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version)
}
AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}")
val nameText = idx?.let { formattedText.getOrNull(it) }?.text
// The name string is routed through the same connect path as a
// link; planAndConnect resolves it on the core (name target).
if (nameText != null) uriHandler.openVerifiedSimplexUri(nameText)
}
}
if (hasSecrets) {
@@ -800,7 +800,20 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState
searchChatFilteredBySimplexLink.value = null
connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() }
}
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
is ConnectTarget.Name -> {
// A name lookup means "take me to this contact": open the chat if
// it's already known (visible prompt), unlike a pasted link which
// filters the list. So no filterKnownContact here.
hideKeyboard(view)
withBGApi {
planAndConnect(
chatModel.remoteHostId(),
target.text,
close = null,
cleanup = { searchText.value = TextFieldValue() },
)
}
}
null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
focusRequester.requestFocus()
@@ -31,11 +31,6 @@ suspend fun planAndConnect(
filterKnownGroup: ((GroupInfo) -> Unit)? = null,
): CompletableDeferred<Boolean> {
when (val target = strConnectTarget(shortOrFullLink.trim())) {
is ConnectTarget.Name -> {
showUnsupportedNameAlert(target.nameInfo)
cleanup?.invoke()
return CompletableDeferred(false)
}
is ConnectTarget.Link -> {
if (target.linkType == SimplexLinkType.relay) {
AlertManager.privacySensitive.showAlertMsg(
@@ -46,7 +41,9 @@ suspend fun planAndConnect(
return CompletableDeferred(false)
}
}
null -> {}
// A SimplexName falls through to apiConnectPlan, which resolves it on the
// core (the /_connect plan command accepts a name target, not only a link).
is ConnectTarget.Name, null -> {}
}
connectProgressManager.cancelConnectProgress()
val inProgress = mutableStateOf(true)
@@ -94,8 +91,8 @@ private suspend fun planAndConnectTask(
connectionLink,
connectionPlan.invitationLinkPlan.contactSLinkData_,
ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification,
close,
cleanup
close = close,
cleanup = cleanup
)
} else {
Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, no short link data")
@@ -157,6 +154,7 @@ private suspend fun planAndConnectTask(
connectionLink,
connectionPlan.contactAddressPlan.contactSLinkData_,
ownerVerification = connectionPlan.contactAddressPlan.ownerVerification,
verifiedDomain = connectionPlan.contactAddressPlan.verifiedDomain,
close,
cleanup
)
@@ -204,6 +202,12 @@ private suspend fun planAndConnectTask(
is ContactAddressPlan.Known -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .Known")
val contact = connectionPlan.contactAddressPlan.contact
// A name-resolved contact is prepared in the store but not yet in the
// chat list (link-prepared chats arrive via NewPreparedChat). Surface it
// so it's visible and openable; no-op if already present.
if (chatModel.getContactChat(contact.contactId) == null) {
chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = emptyList()))
}
if (filterKnownContact != null) {
filterKnownContact(contact)
} else {
@@ -228,6 +232,7 @@ private suspend fun planAndConnectTask(
connectionPlan.groupLinkPlan.groupSLinkInfo_,
connectionPlan.groupLinkPlan.groupSLinkData_,
ownerVerification = connectionPlan.groupLinkPlan.ownerVerification,
verifiedDomain = connectionPlan.groupLinkPlan.verifiedDomain,
close,
cleanup
)
@@ -288,6 +293,11 @@ private suspend fun planAndConnectTask(
is GroupLinkPlan.Known -> {
Log.d(TAG, "planAndConnect, .GroupLink, .Known")
val groupInfo = connectionPlan.groupLinkPlan.groupInfo
// Same as ContactAddress.Known: surface a name-resolved (prepared)
// group in the chat list so it's visible and openable.
if (chatModel.getGroupChat(groupInfo.groupId) == null) {
chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo, groupChatScope = null), chatItems = emptyList()))
}
if (filterKnownGroup != null) {
filterKnownGroup(groupInfo)
} else {
@@ -619,6 +629,7 @@ fun showPrepareContactAlert(
connectionLink: CreatedConnLink,
contactShortLinkData: ContactShortLinkData,
ownerVerification: OwnerVerification? = null,
verifiedDomain: SimplexDomain? = null,
close: (() -> Unit)?,
cleanup: (() -> Unit)?
) {
@@ -642,7 +653,7 @@ fun showPrepareContactAlert(
AlertManager.privacySensitive.hideAlert()
ModalManager.closeAllModalsEverywhere()
withBGApi {
val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData)
val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData, verifiedDomain)
if (chat != null) {
withContext(Dispatchers.Main) {
ChatController.chatModel.chatsContext.addChat(chat)
@@ -664,6 +675,7 @@ fun showPrepareGroupAlert(
groupShortLinkInfo: GroupShortLinkInfo?,
groupShortLinkData: GroupShortLinkData,
ownerVerification: OwnerVerification? = null,
verifiedDomain: SimplexDomain? = null,
close: (() -> Unit)?,
cleanup: (() -> Unit)?
) {
@@ -686,7 +698,7 @@ fun showPrepareGroupAlert(
AlertManager.privacySensitive.hideAlert()
withBGApi {
val directLink = groupShortLinkInfo?.direct ?: true
val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData)
val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData, verifiedDomain)
if (chat != null) {
withContext(Dispatchers.Main) {
val relays = groupShortLinkInfo?.groupRelays
@@ -536,7 +536,20 @@ private fun ContactsSearchBar(
cleanup = { searchText.value = TextFieldValue() }
)
}
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
is ConnectTarget.Name -> {
// A name lookup means "take me to this contact": open the chat if
// it's already known (visible prompt), unlike a pasted link which
// filters the list. So no filterKnownContact here.
hideKeyboard(view)
withBGApi {
planAndConnect(
chatModel.remoteHostId(),
target.text,
close = close,
cleanup = { searchText.value = TextFieldValue() },
)
}
}
null -> if (!searchShowingSimplexLink.value || it.isEmpty()) {
if (it.isNotEmpty()) {
focusRequester.requestFocus()
@@ -679,7 +679,11 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRC
showQRCodeScanner.value = false
withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } }
}
is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo)
is ConnectTarget.Name -> {
pastedLink.value = target.text
showQRCodeScanner.value = false
withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } }
}
null -> AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.invalid_contact_link),
text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link)
@@ -824,7 +828,7 @@ fun strIsSimplexLink(str: String): Boolean {
sealed class ConnectTarget {
class Link(val text: String, val linkType: SimplexLinkType, val linkText: String) : ConnectTarget()
class Name(val nameInfo: SimplexNameInfo) : ConnectTarget()
class Name(val text: String, val nameInfo: SimplexNameInfo) : ConnectTarget()
}
fun strConnectTarget(str: String): ConnectTarget? {
@@ -835,21 +839,13 @@ fun strConnectTarget(str: String): ConnectTarget? {
return ConnectTarget.Link(links[0].text, fmt.linkType, fmt.simplexLinkText)
}
if (links.isEmpty()) {
val nameInfo = parsedMd.firstNotNullOfOrNull { (it.format as? Format.SimplexName)?.nameInfo }
if (nameInfo != null) return ConnectTarget.Name(nameInfo)
val nameFt = parsedMd.firstOrNull { it.format is Format.SimplexName }
val nameInfo = (nameFt?.format as? Format.SimplexName)?.nameInfo
if (nameFt != null && nameInfo != null) return ConnectTarget.Name(nameFt.text, nameInfo)
}
return null
}
fun showUnsupportedNameAlert(nameInfo: SimplexNameInfo) {
val (title, msg) = if (nameInfo.nameType == SimplexNameType.contact) {
generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version)
} else {
generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version)
}
AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}")
}
@Composable
fun IncognitoToggle(
incognitoPref: SharedPreference<Boolean>,
@@ -134,6 +134,11 @@ fun MorePrivacyView(chatModel: ChatModel) {
chatModel.draftChatId.value = null
}
})
SettingsPreferenceItem(
painterResource(MR.images.ic_tag),
stringResource(MR.strings.verify_simplex_names),
chatModel.controller.appPrefs.privacyVerifySimplexNames
)
}
SectionDividerSpaced()
@@ -0,0 +1,79 @@
package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import chat.simplex.common.platform.*
import chat.simplex.common.views.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
// Set the user's own (prefix "@") or a channel's (prefix "#") SimpleX name.
// The field is prefilled with the full prefixed name; `save` receives the encoded name (or null to
// clear) and returns true on success (it shows its own error alert otherwise).
@Composable
fun SetSimplexDomainView(
title: String,
footer: String,
placeholder: String,
simplexName: String,
save: suspend (String?) -> Boolean,
close: () -> Unit
) {
val name = rememberSaveable { mutableStateOf(simplexName) }
val saving = remember { mutableStateOf(false) }
val unchanged = name.value.trim() == simplexName.trim()
fun addSimplexTLD(s: String): String {
return if (s.contains(".")) s else "$s.simplex"
}
fun normalized(): String? {
val s = name.value.trim()
return when {
s.isEmpty() -> null
s.startsWith("@") || s.startsWith("#") -> addSimplexTLD(s.substring(1))
else -> addSimplexTLD(s)
}
}
val doSave: () -> Unit = {
withBGApi {
saving.value = true
val ok = try { save(normalized()) } catch (e: Exception) {
Log.e(TAG, "SetSimplexDomainView save: ${e.stackTraceToString()}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), e.message ?: "")
false
}
saving.value = false
if (ok) withContext(Dispatchers.Main) { close() }
}
}
ModalView(close = close) {
ColumnWithScrollBar {
AppBarTitle(title)
SectionView {
PlainTextEditor(name, placeholder)
}
SectionTextFooter(footer)
SectionDividerSpaced()
SectionView {
SectionItemView(doSave, disabled = unchanged || saving.value) {
Text(
stringResource(MR.strings.save_verb),
color = if (unchanged || saving.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
SectionBottomSpacer()
}
}
}
@@ -34,6 +34,8 @@ import chat.simplex.common.views.chat.*
import chat.simplex.common.views.newchat.*
import chat.simplex.common.BuildConfigCommon
import chat.simplex.res.MR
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun UserAddressView(
@@ -360,6 +362,37 @@ private fun UserAddressLayout(
SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations))
}
SectionDividerSpaced()
SectionView {
SettingsActionItem(
painterResource(MR.images.ic_at),
stringResource(MR.strings.your_simplex_name),
click = {
ModalManager.start.showCustomModal { close ->
val domain = user?.profile?.contactDomain?.shortName
SetSimplexDomainView(
title = generalGetString(MR.strings.set_simplex_name),
footer = generalGetString(MR.strings.set_user_simplex_name_footer),
placeholder = "@yourname.testing",
simplexName = if (domain == null) "" else "@$domain",
save = { simplexDomain ->
try {
val u = chatModel.controller.apiSetUserDomain(user?.remoteHostId, simplexDomain)
withContext(Dispatchers.Main) { chatModel.updateUser(u) }
true
} catch (e: Exception) {
Log.e(TAG, "apiSetUserDomain: ${e.message}")
false
}
},
close = close
)
}
},
iconColor = MaterialTheme.colors.secondary
)
}
SectionDividerSpaced()
SectionView(generalGetString(MR.strings.or_to_share_privately)) {
CreateOneTimeLinkButton()
@@ -268,28 +268,32 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) {
if (currentRemoteHost == null && networkUseSocksProxy.value) {
SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
}
val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)
SectionItemView(
{ scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } },
disabled = saveDisabled,
) {
Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
SectionDividerSpaced()
SectionView {
val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value)
SectionItemView(
{ scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } },
disabled = saveDisabled,
) {
Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
}
}
val serversErr = globalServersError(serverErrors.value)
if (serversErr != null) {
SectionCustomFooter {
ServersErrorFooter(serversErr)
val serversErrs = globalServersErrors(serverErrors.value)
if (serversErrs.isNotEmpty()) {
serversErrs.forEach { err ->
SectionCustomFooter {
ServersErrorFooter(err)
}
}
} else if (serverErrors.value.isNotEmpty()) {
SectionCustomFooter {
ServersErrorFooter(generalGetString(MR.strings.errors_in_servers_configuration))
}
}
val serversWarn = globalServersWarning(serverWarnings.value)
if (serversWarn != null) {
globalServersWarnings(serverWarnings.value).forEach { warn ->
SectionCustomFooter {
ServersWarningFooter(serversWarn)
ServersWarningFooter(warn)
}
}
@@ -951,23 +955,11 @@ fun serversCanBeSaved(
return userServers != currUserServers && serverErrors.isEmpty()
}
fun globalServersError(serverErrors: List<UserServersError>): String? {
for (err in serverErrors) {
if (err.globalError != null) {
return err.globalError
}
}
return null
}
fun globalServersErrors(serverErrors: List<UserServersError>): List<String> =
serverErrors.mapNotNull { it.globalError }
fun globalServersWarning(serverWarnings: List<UserServersWarning>): String? {
for (warn in serverWarnings) {
if (warn.globalWarning != null) {
return warn.globalWarning
}
}
return null
}
fun globalServersWarnings(serverWarnings: List<UserServersWarning>): List<String> =
serverWarnings.mapNotNull { it.globalWarning }
fun globalSMPServersError(serverErrors: List<UserServersError>): String? {
for (err in serverErrors) {
@@ -211,15 +211,19 @@ fun OperatorViewLayout(
rhId = rhId
)
}
val serversErr = globalServersError(serverErrors.value)
val serversWarn = globalServersWarning(serverWarnings.value)
if (serversErr != null) {
SectionCustomFooter {
ServersErrorFooter(serversErr)
val serversErrs = globalServersErrors(serverErrors.value)
val serversWarns = globalServersWarnings(serverWarnings.value)
if (serversErrs.isNotEmpty()) {
serversErrs.forEach { err ->
SectionCustomFooter {
ServersErrorFooter(err)
}
}
} else if (serversWarn != null) {
SectionCustomFooter {
ServersWarningFooter(serversWarn)
} else if (serversWarns.isNotEmpty()) {
serversWarns.forEach { warn ->
SectionCustomFooter {
ServersWarningFooter(warn)
}
}
} else {
val footerText = when (val c = operator.conditionsAcceptance) {
@@ -267,7 +271,7 @@ fun OperatorViewLayout(
userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
operator = this[operatorIndex].operator?.copy(
smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false)
smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false, names = false)
)
)
}
@@ -287,7 +291,27 @@ fun OperatorViewLayout(
userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
operator = this[operatorIndex].operator?.copy(
smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled)
smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled, names = false)
)
)
}
}
)
}
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(
stringResource(MR.strings.operator_use_for_names),
Modifier.padding(end = 24.dp),
color = Color.Unspecified
)
Spacer(Modifier.fillMaxWidth().weight(1f))
DefaultSwitch(
checked = userServers.value[operatorIndex].operator_.smpRoles.names,
onCheckedChange = { enabled ->
userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
operator = this[operatorIndex].operator?.copy(
smpRoles = this[operatorIndex].operator?.smpRoles?.copy(names = enabled) ?: ServerRoles(storage = false, proxy = false, names = enabled)
)
)
}
@@ -371,7 +395,7 @@ fun OperatorViewLayout(
userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
operator = this[operatorIndex].operator?.copy(
xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false)
xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false, names = false)
)
)
}
@@ -185,16 +185,14 @@ fun YourServersViewLayout(
iconColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
val serversErr = globalServersError(serverErrors.value)
if (serversErr != null) {
globalServersErrors(serverErrors.value).forEach { err ->
SectionCustomFooter {
ServersErrorFooter(serversErr)
ServersErrorFooter(err)
}
}
val serversWarn = globalServersWarning(serverWarnings.value)
if (serversWarn != null) {
globalServersWarnings(serverWarnings.value).forEach { warn ->
SectionCustomFooter {
ServersWarningFooter(serversWarn)
ServersWarningFooter(warn)
}
}
SectionDividerSpaced()
@@ -146,6 +146,7 @@
<string name="for_chat_profile">For chat profile %s:</string>
<string name="errors_in_servers_configuration">Errors in servers configuration.</string>
<string name="no_chat_relays_enabled">No chat relays enabled.</string>
<string name="no_names_servers_enabled">No servers to resolve names.</string>
<string name="server_warning">Server warning</string>
<string name="error_accepting_operator_conditions">Error accepting conditions</string>
<string name="blocking_reason_spam">Spam</string>
@@ -199,6 +200,16 @@
<string name="channel_name_requires_newer_app_version">Connecting via channel name requires a newer app version.</string>
<string name="contact_name_requires_newer_app_version">Connecting via contact name requires a newer app version.</string>
<string name="please_upgrade_the_app">Please upgrade the app.</string>
<string name="simplex_name_error">SimpleX name error</string>
<string name="simplex_name_no_servers_desc">None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link.</string>
<string name="simplex_name_server_no_resolver_desc">Server %1$s does not support name resolution. Configure servers, or use a connection link.</string>
<string name="simplex_name_not_found">Name not found</string>
<string name="simplex_name_not_found_desc">This SimpleX name is not registered. Please check the name.</string>
<string name="simplex_name_resolver_error_desc">Resolver error: %1$s</string>
<string name="simplex_name_no_valid_link">No valid link</string>
<string name="simplex_name_no_valid_link_desc">The SimpleX name %1$s is registered, but it has no valid link.</string>
<string name="simplex_name_unconfirmed">Unconfirmed name</string>
<string name="simplex_name_unconfirmed_desc">The SimpleX name %1$s is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.</string>
<string name="channel_temporarily_unavailable">Channel temporarily unavailable</string>
<string name="channel_no_active_relays_try_later">Channel has no active relays. Please try to join later.</string>
<string name="app_update_required">App update required</string>
@@ -930,6 +941,17 @@
<string name="one_time_link">One-time invitation link</string>
<string name="one_time_link_short">1-time link</string>
<string name="simplex_address">SimpleX address</string>
<string name="verify_simplex_name_action">Verify name</string>
<string name="verify_simplex_names">Verify SimpleX names</string>
<string name="simplex_name_not_verified">SimpleX name not verified</string>
<string name="simplex_name">SimpleX name</string>
<string name="your_simplex_name">Your SimpleX name</string>
<string name="set_simplex_name">Set SimpleX name</string>
<string name="error_saving_simplex_name">Error saving name</string>
<string name="simplex_name_owner_no_channel_link">The SimpleX name %1$s is registered without channel link. Add channel link to the name via the registration page.</string>
<string name="simplex_name_owner_no_address">The SimpleX name %1$s is registered without SimpleX address. Add your SimpleX address to the name via the registration page.</string>
<string name="set_user_simplex_name_footer">Let people connect to you via name registered with your SimpleX address.</string>
<string name="set_channel_simplex_name_footer">Let people join via name registered with this channel link.</string>
<string name="or_show_this_qr_code">Or show this code</string>
<string name="full_link_button_text">Full link</string>
<string name="short_link_button_text">Short link</string>
@@ -2153,6 +2175,7 @@
<string name="operator_use_for_messages">Use for messages</string>
<string name="operator_use_for_messages_receiving">To receive</string>
<string name="operator_use_for_messages_private_routing">For private routing</string>
<string name="operator_use_for_names">To resolve names</string>
<string name="operator_added_message_servers">Added message servers</string>
<string name="operator_use_for_files">Use for files</string>
<string name="operator_use_for_sending">To send</string>
@@ -560,7 +560,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
<> ".\nIt is hidden from the directory until approved."
notifyAdminUsers $ "The " <> gt <> " " <> groupRef <> " is updated" <> byMember <> "."
sendToApprove g' gr' n'
sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case
sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True Nothing) >>= \case
Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) ->
case dbOwnerMemberId gr of
Just ownerGMId ->
@@ -818,7 +818,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
forM_ pg_ $ \pg@PublicGroupProfile {groupLink} ->
when (groupRegStatus == GRSActive || pendingApproval groupRegStatus) $ do
let link = ACL SCMContact $ CLShort groupLink
sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case
sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True Nothing) >>= \case
Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated = BoolDef updated, linkOwners = ListDef owners}))) ->
checkValidOwner dbOwnerMemberId owners $ do
when updated $ reapprove pg gr groupRegStatus g'
@@ -954,7 +954,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let link = ACL SCMContact $ CLShort connLink
mId = MemberId oIdBytes
gt' = groupTypeStr gt
sendChatCmd cc (APIConnectPlan userId (Just link) True (Just ownerSig)) >>= \case
sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) True (Just ownerSig)) >>= \case
Right (CRConnectionPlan _ (ACCL SCMContact ccLink) plan) ->
handleGroupLinkPlan ct ccLink mId ownerSig gt' plan
_ -> sendMessage cc ct "Error: could not connect. Please report it to directory admins."
@@ -1000,7 +1000,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let GroupShortLinkData {groupProfile = GroupProfile {displayName}} = groupSLinkData
ownerContact = GroupOwnerContact {contactId = contactId' ct, memberId = mId}
sendMessage cc ct $ "Joining the " <> gt <> " " <> displayName <> ""
sendChatCmd cc (APIPrepareGroup userId ccLink False groupSLinkData) >>= \case
sendChatCmd cc (APIPrepareGroup userId ccLink False Nothing groupSLinkData) >>= \case
Right (CRNewPreparedChat _ (AChat SCTGroup (Chat (GroupChat gInfo _) _ _))) -> do
let gId = groupId' gInfo
addGroupReg notifyAdminUsers st cc user ct gInfo GRSProposed $ \_ -> pure ()
+10 -10
View File
@@ -1363,28 +1363,28 @@ ChatCmdError: Command error (only used in WebSockets API).
### APIConnectPlan
Determine SimpleX link type and if the bot is already connected via this link.
Determine SimpleX link type and if the bot is already connected via this link or name.
*Network usage*: interactive.
**Parameters**:
- userId: int64
- connectionLink: string?
- connectTarget: string?
- resolveKnown: bool
- linkOwnerSig: [LinkOwnerSig](./TYPES.md#linkownersig)?
**Syntax**:
```
/_connect plan <userId> <connectionLink>
/_connect plan <userId> <connectTarget>
```
```javascript
'/_connect plan ' + userId + ' ' + connectionLink // JavaScript
'/_connect plan ' + userId + ' ' + connectTarget // JavaScript
```
```python
'/_connect plan ' + str(userId) + ' ' + connectionLink # Python
'/_connect plan ' + str(userId) + ' ' + connectTarget # Python
```
**Responses**:
@@ -1455,26 +1455,26 @@ ChatCmdError: Command error (only used in WebSockets API).
### Connect
Connect via SimpleX link as string in the active user profile.
Connect via SimpleX link or name as string in the active user profile.
*Network usage*: interactive.
**Parameters**:
- incognito: bool
- connLink_: string?
- connTarget_: string?
**Syntax**:
```
/connect[ <connLink_>]
/connect[ <connTarget_>]
```
```javascript
'/connect' + (connLink_ ? ' ' + connLink_ : '') // JavaScript
'/connect' + (connTarget_ ? ' ' + connTarget_ : '') // JavaScript
```
```python
'/connect' + ((' ' + connLink_) if connLink_ is not None else '') # Python
'/connect' + ((' ' + connTarget_) if connTarget_ is not None else '') # Python
```
**Responses**:
+84 -13
View File
@@ -139,6 +139,7 @@ This file is generated automatically.
- [MsgReaction](#msgreaction)
- [MsgReceiptStatus](#msgreceiptstatus)
- [MsgSigStatus](#msgsigstatus)
- [NameErrorType](#nameerrortype)
- [NetworkError](#networkerror)
- [NewUser](#newuser)
- [NoteFolder](#notefolder)
@@ -172,8 +173,11 @@ This file is generated automatically.
- [SMPAgentError](#smpagenterror)
- [SecurityCode](#securitycode)
- [SimplePreference](#simplepreference)
- [SimplexDomain](#simplexdomain)
- [SimplexDomainClaim](#simplexdomainclaim)
- [SimplexDomainError](#simplexdomainerror)
- [SimplexDomainProof](#simplexdomainproof)
- [SimplexLinkType](#simplexlinktype)
- [SimplexNameDomain](#simplexnamedomain)
- [SimplexNameInfo](#simplexnameinfo)
- [SimplexNameType](#simplexnametype)
- [SimplexTLD](#simplextld)
@@ -313,6 +317,9 @@ FILE:
- type: "FILE"
- fileErr: [FileErrorType](#fileerrortype)
NO_NAME_SERVERS:
- type: "NO_NAME_SERVERS"
PROXY:
- type: "PROXY"
- proxyServer: string
@@ -1104,6 +1111,11 @@ ChatStoreChanged:
InvalidConnReq:
- type: "invalidConnReq"
SimplexDomainNotReady:
- type: "simplexDomainNotReady"
- simplexDomain: [SimplexDomain](#simplexdomain)
- simplexDomainError: [SimplexDomainError](#simplexdomainerror)
UnsupportedConnReq:
- type: "unsupportedConnReq"
@@ -1784,6 +1796,7 @@ Ok:
- type: "ok"
- contactSLinkData_: [ContactShortLinkData](#contactshortlinkdata)?
- ownerVerification: [OwnerVerification](#ownerverification)?
- verifiedDomain: [SimplexDomain](#simplexdomain)?
OwnLink:
- type: "ownLink"
@@ -1976,6 +1989,10 @@ EXPIRED:
INTERNAL:
- type: "INTERNAL"
NAME:
- type: "NAME"
- nameErr: [NameErrorType](#nameerrortype)
DUPLICATE_:
- type: "DUPLICATE_"
@@ -2330,6 +2347,7 @@ MemberSupport:
- membersRequireAttention: int
- viaGroupLinkUri: string?
- groupKeys: [GroupKeys](#groupkeys)?
- groupDomainVerified: bool?
---
@@ -2375,6 +2393,7 @@ Ok:
- groupSLinkInfo_: [GroupShortLinkInfo](#groupshortlinkinfo)?
- groupSLinkData_: [GroupShortLinkData](#groupshortlinkdata)?
- ownerVerification: [OwnerVerification](#ownerverification)?
- verifiedDomain: [SimplexDomain](#simplexdomain)?
OwnLink:
- type: "ownLink"
@@ -2753,6 +2772,8 @@ Unknown:
- peerType: [ChatPeerType](#chatpeertype)?
- localBadge: [LocalBadge](#localbadge)?
- localAlias: string
- contactDomain: [SimplexDomainClaim](#simplexdomainclaim)?
- contactDomainVerified: bool?
---
@@ -2928,6 +2949,23 @@ Unknown:
- "signedNoKey"
---
## NameErrorType
**Discriminated union type**:
NO_RESOLVER:
- type: "NO_RESOLVER"
NOT_FOUND:
- type: "NOT_FOUND"
RESOLVER:
- type: "RESOLVER"
- resolverErr: string
---
## NetworkError
@@ -3098,6 +3136,7 @@ count=<count>
- preferences: [Preferences](#preferences)?
- peerType: [ChatPeerType](#chatpeertype)?
- badge: [BadgeProof](#badgeproof)?
- contactDomain: [SimplexDomainClaim](#simplexdomainclaim)?
---
@@ -3146,7 +3185,7 @@ NO_SESSION:
**Record type**:
- groupWebPage: string?
- groupDomain: string?
- groupDomainClaim: [SimplexDomainClaim](#simplexdomainclaim)?
- domainWebPage: bool
- allowEmbedding: bool
@@ -3530,6 +3569,48 @@ A_QUEUE:
- allow: [FeatureAllowed](#featureallowed)
---
## SimplexDomain
**Record type**:
- nameTLD: [SimplexTLD](#simplextld)
- domain: string
- subDomain: [string]
---
## SimplexDomainClaim
**Record type**:
- domain: string
- proof: [SimplexDomainProof](#simplexdomainproof)?
---
## SimplexDomainError
**Discriminated union type**:
NoValidLink:
- type: "noValidLink"
UnknownDomain:
- type: "unknownDomain"
---
## SimplexDomainProof
**Record type**:
- linkOwnerId: string?
- presHeader: string
- signature: string
---
## SimplexLinkType
@@ -3542,23 +3623,13 @@ A_QUEUE:
- "relay"
---
## SimplexNameDomain
**Record type**:
- nameTLD: [SimplexTLD](#simplextld)
- domain: string
- subDomain: [string]
---
## SimplexNameInfo
**Record type**:
- nameType: [SimplexNameType](#simplexnametype)
- nameDomain: [SimplexNameDomain](#simplexnamedomain)
- nameDomain: [SimplexDomain](#simplexdomain)
---
+7 -3
View File
@@ -135,10 +135,10 @@ chatCommandsDocsData =
( "Connection commands",
"These commands may be used to create connections. Most bots do not need to use them - bot users will connect via bot address with auto-accept enabled.",
[ ("APIAddContact", [], "Create 1-time invitation link.", ["CRInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False)),
-- `Maybe` in `connectionLink :: Maybe AConnectionLink` is used to signal link parsing error to the runtime (the handler returns CEInvalidConnReq on Nothing); it is NOT API-level optionality. The parameter is required from callers.
("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectionLink"),
-- `Maybe` in `connectTarget :: Maybe ConnectTarget` is used to signal parse failure to the runtime (the handler returns CEInvalidConnReq on Nothing); it is NOT API-level optionality. The parameter is required from callers.
("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link or name.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectTarget"),
("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"),
("Connect", [], "Connect via SimpleX link as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connLink_"),
("Connect", [], "Connect via SimpleX link or name as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connTarget_"),
("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest", "CRChatCmdError"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"),
("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected", "CRChatCmdError"], [], Nothing, "/_reject " <> Param "contactReqId")
]
@@ -410,9 +410,11 @@ undocumentedCommands =
"APISetMemberSettings",
"APISetNetworkConfig",
"APISetNetworkInfo",
"APISetPublicGroupAccess",
"APISetServerOperators",
"APISetUserContactReceipts",
"APISetUserGroupReceipts",
"APISetUserDomain",
"APISetUserServers",
"APISetUserUIThemes",
"APIShareChatMsgContent",
@@ -432,7 +434,9 @@ undocumentedCommands =
"APIUserRead",
"APIValidateServers",
"APIVerifyContact",
"APIVerifyContactDomain",
"APIVerifyGroupMember",
"APIVerifyGroupDomain",
"APIVerifyToken",
"CheckChatRunning",
"ConfirmRemoteCtrl",
+2
View File
@@ -148,6 +148,7 @@ undocumentedResponses =
"CRContactAliasUpdated",
"CRContactCode",
"CRContactInfo",
"CRContactDomainVerified",
"CRContactRatchetSyncStarted",
"CRContactSwitchAborted",
"CRContactSwitchStarted",
@@ -167,6 +168,7 @@ undocumentedResponses =
"CRGroupMemberRatchetSyncStarted",
"CRGroupMemberSwitchAborted",
"CRGroupMemberSwitchStarted",
"CRGroupDomainVerified",
"CRGroupProfile",
"CRGroupUserChanged",
"CRItemsReadForChat",
+13 -4
View File
@@ -34,7 +34,8 @@ import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Chat.Operators
import Simplex.Messaging.Agent.Store.Entity (DBStored (..))
import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), BadgeType (..), JSONBadge (..))
import Simplex.Chat.Badges
import Simplex.Chat.Names
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
@@ -45,7 +46,7 @@ import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Client
import Simplex.Messaging.Crypto.File
import Simplex.Messaging.Parsers (dropPrefix, fstToLower)
import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NetworkError (..), ProxyError (..))
import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NameErrorType (..), NetworkError (..), ProxyError (..))
import Simplex.Messaging.Protocol.Types (ClientNotice (..))
import Simplex.Messaging.Transport
import Simplex.RemoteControl.Types
@@ -321,6 +322,7 @@ chatTypesDocsData =
(sti @MsgReaction, STUnion, "MR", [], "", ""),
(sti @MsgReceiptStatus, STEnum, "MR", [], "", ""),
(sti @MsgSigStatus, STEnum, "MSS", [], "", ""),
(sti @NameErrorType, STUnion, "", [], "", ""),
(sti @NetworkError, STUnion, "NE", [], "", ""),
(sti @NewUser, STRecord, "", [], "", ""),
(sti @NoteFolder, STRecord, "", [], "", ""),
@@ -353,8 +355,11 @@ chatTypesDocsData =
(sti @RoleGroupPreference, STRecord, "", [], "", ""),
(sti @SecurityCode, STRecord, "", [], "", ""),
(sti @SimplePreference, STRecord, "", [], "", ""),
(sti @SimplexDomain, STRecord, "", [], "", ""),
(sti @SimplexDomainClaim, STRecord, "", [], "", ""),
(sti @SimplexDomainError, STUnion, "SDE", [], "", ""),
(sti @SimplexDomainProof, STRecord, "", [], "", ""),
(sti @SimplexLinkType, STEnum, "XL", [], "", ""),
(sti @SimplexNameDomain, STRecord, "", [], "", ""),
(sti @SimplexNameInfo, STRecord, "", [], "", ""),
(sti @SimplexNameType, STEnum, "NT", [], "", ""),
(sti @SimplexTLD, STEnum, "TLD", [], "", ""),
@@ -548,6 +553,7 @@ deriving instance Generic MsgFilter
deriving instance Generic MsgReaction
deriving instance Generic MsgReceiptStatus
deriving instance Generic MsgSigStatus
deriving instance Generic NameErrorType
deriving instance Generic NetworkError
deriving instance Generic NewUser
deriving instance Generic NoteFolder
@@ -578,8 +584,11 @@ deriving instance Generic RelayProfile
deriving instance Generic RelayStatus
deriving instance Generic ReportReason
deriving instance Generic SecurityCode
deriving instance Generic SimplexDomain
deriving instance Generic SimplexDomainClaim
deriving instance Generic SimplexDomainError
deriving instance Generic SimplexDomainProof
deriving instance Generic SimplexLinkType
deriving instance Generic SimplexNameDomain
deriving instance Generic SimplexNameInfo
deriving instance Generic SimplexNameType
deriving instance Generic SimplexTLD
+3
View File
@@ -194,6 +194,7 @@ toTypeInfo tr =
primitiveToLower st@(ST t ps) = let t' = fstToLower t in if t' `elem` primitiveTypes then ST t' ps else st
stringTypes =
[ "AConnectionLink",
"AConnectTarget",
"AProtocolType",
"AgentConnId",
"AgentInvId",
@@ -215,11 +216,13 @@ toTypeInfo tr =
"Text",
"MREmojiChar",
"PrivateKey",
"ProofPresHeader",
"PublicKey",
"ProtocolServer",
"SbKey",
"SharedMsgId",
"Signature",
"StrJSON",
"TransportHost",
"UIColor",
"UserPwd",
+1 -1
View File
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 98391fd677f547f610a17c48cc96d8c4b2d5e96e
tag: 836254a4c6bd5e17b40acbcaf77a83802d44919e
source-repository-package
type: git
@@ -495,11 +495,11 @@ export namespace APIAddContact {
}
}
// Determine SimpleX link type and if the bot is already connected via this link.
// Determine SimpleX link type and if the bot is already connected via this link or name.
// Network usage: interactive.
export interface APIConnectPlan {
userId: number // int64
connectionLink?: string
connectTarget?: string
resolveKnown: boolean
linkOwnerSig?: T.LinkOwnerSig
}
@@ -508,7 +508,7 @@ export namespace APIConnectPlan {
export type Response = CR.ConnectionPlan | CR.ChatCmdError
export function cmdString(self: APIConnectPlan): string {
return '/_connect plan ' + self.userId + ' ' + self.connectionLink
return '/_connect plan ' + self.userId + ' ' + self.connectTarget
}
}
@@ -528,18 +528,18 @@ export namespace APIConnect {
}
}
// Connect via SimpleX link as string in the active user profile.
// Connect via SimpleX link or name as string in the active user profile.
// Network usage: interactive.
export interface Connect {
incognito: boolean
connLink_?: string
connTarget_?: string
}
export namespace Connect {
export type Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError
export function cmdString(self: Connect): string {
return '/connect' + (self.connLink_ ? ' ' + self.connLink_ : '')
return '/connect' + (self.connTarget_ ? ' ' + self.connTarget_ : '')
}
}
@@ -66,6 +66,7 @@ export type AgentErrorType =
| AgentErrorType.NTF
| AgentErrorType.XFTP
| AgentErrorType.FILE
| AgentErrorType.NO_NAME_SERVERS
| AgentErrorType.PROXY
| AgentErrorType.RCP
| AgentErrorType.BROKER
@@ -84,6 +85,7 @@ export namespace AgentErrorType {
| "NTF"
| "XFTP"
| "FILE"
| "NO_NAME_SERVERS"
| "PROXY"
| "RCP"
| "BROKER"
@@ -136,6 +138,10 @@ export namespace AgentErrorType {
fileErr: FileErrorType
}
export interface NO_NAME_SERVERS extends Interface {
type: "NO_NAME_SERVERS"
}
export interface PROXY extends Interface {
type: "PROXY"
proxyServer: string
@@ -1036,6 +1042,7 @@ export type ChatErrorType =
| ChatErrorType.ChatNotStopped
| ChatErrorType.ChatStoreChanged
| ChatErrorType.InvalidConnReq
| ChatErrorType.SimplexDomainNotReady
| ChatErrorType.UnsupportedConnReq
| ChatErrorType.ConnReqMessageProhibited
| ChatErrorType.ContactNotReady
@@ -1113,6 +1120,7 @@ export namespace ChatErrorType {
| "chatNotStopped"
| "chatStoreChanged"
| "invalidConnReq"
| "simplexDomainNotReady"
| "unsupportedConnReq"
| "connReqMessageProhibited"
| "contactNotReady"
@@ -1267,6 +1275,12 @@ export namespace ChatErrorType {
type: "invalidConnReq"
}
export interface SimplexDomainNotReady extends Interface {
type: "simplexDomainNotReady"
simplexDomain: SimplexDomain
simplexDomainError: SimplexDomainError
}
export interface UnsupportedConnReq extends Interface {
type: "unsupportedConnReq"
}
@@ -2035,6 +2049,7 @@ export namespace ContactAddressPlan {
type: "ok"
contactSLinkData_?: ContactShortLinkData
ownerVerification?: OwnerVerification
verifiedDomain?: SimplexDomain
}
export interface OwnLink extends Interface {
@@ -2157,6 +2172,7 @@ export type ErrorType =
| ErrorType.LARGE_MSG
| ErrorType.EXPIRED
| ErrorType.INTERNAL
| ErrorType.NAME
| ErrorType.DUPLICATE_
export namespace ErrorType {
@@ -2175,6 +2191,7 @@ export namespace ErrorType {
| "LARGE_MSG"
| "EXPIRED"
| "INTERNAL"
| "NAME"
| "DUPLICATE_"
interface Interface {
@@ -2241,6 +2258,11 @@ export namespace ErrorType {
type: "INTERNAL"
}
export interface NAME extends Interface {
type: "NAME"
nameErr: NameErrorType
}
export interface DUPLICATE_ extends Interface {
type: "DUPLICATE_"
}
@@ -2608,6 +2630,7 @@ export interface GroupInfo {
membersRequireAttention: number // int
viaGroupLinkUri?: string
groupKeys?: GroupKeys
groupDomainVerified?: boolean
}
export interface GroupKeys {
@@ -2658,6 +2681,7 @@ export namespace GroupLinkPlan {
groupSLinkInfo_?: GroupShortLinkInfo
groupSLinkData_?: GroupShortLinkData
ownerVerification?: OwnerVerification
verifiedDomain?: SimplexDomain
}
export interface OwnLink extends Interface {
@@ -2984,6 +3008,8 @@ export interface LocalProfile {
peerType?: ChatPeerType
localBadge?: LocalBadge
localAlias: string
contactDomain?: SimplexDomainClaim
contactDomainVerified?: boolean
}
export enum MemberCriteria {
@@ -3177,6 +3203,29 @@ export enum MsgSigStatus {
SignedNoKey = "signedNoKey",
}
export type NameErrorType = NameErrorType.NO_RESOLVER | NameErrorType.NOT_FOUND | NameErrorType.RESOLVER
export namespace NameErrorType {
export type Tag = "NO_RESOLVER" | "NOT_FOUND" | "RESOLVER"
interface Interface {
type: Tag
}
export interface NO_RESOLVER extends Interface {
type: "NO_RESOLVER"
}
export interface NOT_FOUND extends Interface {
type: "NOT_FOUND"
}
export interface RESOLVER extends Interface {
type: "RESOLVER"
resolverErr: string
}
}
export type NetworkError =
| NetworkError.ConnectError
| NetworkError.TLSError
@@ -3335,6 +3384,7 @@ export interface Profile {
preferences?: Preferences
peerType?: ChatPeerType
badge?: BadgeProof
contactDomain?: SimplexDomainClaim
}
export type ProxyClientError =
@@ -3395,7 +3445,7 @@ export namespace ProxyError {
export interface PublicGroupAccess {
groupWebPage?: string
groupDomain?: string
groupDomainClaim?: SimplexDomainClaim
domainWebPage: boolean
allowEmbedding: boolean
}
@@ -3898,6 +3948,41 @@ export interface SimplePreference {
allow: FeatureAllowed
}
export interface SimplexDomain {
nameTLD: SimplexTLD
domain: string
subDomain: string[]
}
export interface SimplexDomainClaim {
domain: string
proof?: SimplexDomainProof
}
export type SimplexDomainError = SimplexDomainError.NoValidLink | SimplexDomainError.UnknownDomain
export namespace SimplexDomainError {
export type Tag = "noValidLink" | "unknownDomain"
interface Interface {
type: Tag
}
export interface NoValidLink extends Interface {
type: "noValidLink"
}
export interface UnknownDomain extends Interface {
type: "unknownDomain"
}
}
export interface SimplexDomainProof {
linkOwnerId?: string
presHeader: string
signature: string
}
export enum SimplexLinkType {
Contact = "contact",
Invitation = "invitation",
@@ -3906,15 +3991,9 @@ export enum SimplexLinkType {
Relay = "relay",
}
export interface SimplexNameDomain {
nameTLD: SimplexTLD
domain: string
subDomain: string[]
}
export interface SimplexNameInfo {
nameType: SimplexNameType
nameDomain: SimplexNameDomain
nameDomain: SimplexDomain
}
export enum SimplexNameType {
@@ -434,17 +434,17 @@ def APIAddContact_cmd_string(self: APIAddContact) -> str:
APIAddContact_Response = CR.Invitation | CR.ChatCmdError
# Determine SimpleX link type and if the bot is already connected via this link.
# Determine SimpleX link type and if the bot is already connected via this link or name.
# Network usage: interactive.
class APIConnectPlan(TypedDict):
userId: int # int64
connectionLink: NotRequired[str]
connectTarget: NotRequired[str]
resolveKnown: bool
linkOwnerSig: NotRequired["T.LinkOwnerSig"]
def APIConnectPlan_cmd_string(self: APIConnectPlan) -> str:
return '/_connect plan ' + str(self['userId']) + ' ' + self.get('connectionLink')
return '/_connect plan ' + str(self['userId']) + ' ' + self.get('connectTarget')
APIConnectPlan_Response = CR.ConnectionPlan | CR.ChatCmdError
@@ -463,15 +463,15 @@ def APIConnect_cmd_string(self: APIConnect) -> str:
APIConnect_Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError
# Connect via SimpleX link as string in the active user profile.
# Connect via SimpleX link or name as string in the active user profile.
# Network usage: interactive.
class Connect(TypedDict):
incognito: bool
connLink_: NotRequired[str]
connTarget_: NotRequired[str]
def Connect_cmd_string(self: Connect) -> str:
return '/connect' + ((' ' + self.get('connLink_')) if self.get('connLink_') is not None else '')
return '/connect' + ((' ' + self.get('connTarget_')) if self.get('connTarget_') is not None else '')
Connect_Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError
@@ -78,6 +78,9 @@ class AgentErrorType_FILE(TypedDict):
type: Literal["FILE"]
fileErr: "FileErrorType"
class AgentErrorType_NO_NAME_SERVERS(TypedDict):
type: Literal["NO_NAME_SERVERS"]
class AgentErrorType_PROXY(TypedDict):
type: Literal["PROXY"]
proxyServer: str
@@ -123,6 +126,7 @@ AgentErrorType = (
| AgentErrorType_NTF
| AgentErrorType_XFTP
| AgentErrorType_FILE
| AgentErrorType_NO_NAME_SERVERS
| AgentErrorType_PROXY
| AgentErrorType_RCP
| AgentErrorType_BROKER
@@ -133,7 +137,7 @@ AgentErrorType = (
| AgentErrorType_INACTIVE
)
AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"]
AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "NO_NAME_SERVERS", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"]
class AutoAccept(TypedDict):
acceptIncognito: bool
@@ -784,6 +788,11 @@ class ChatErrorType_chatStoreChanged(TypedDict):
class ChatErrorType_invalidConnReq(TypedDict):
type: Literal["invalidConnReq"]
class ChatErrorType_simplexDomainNotReady(TypedDict):
type: Literal["simplexDomainNotReady"]
simplexDomain: "SimplexDomain"
simplexDomainError: "SimplexDomainError"
class ChatErrorType_unsupportedConnReq(TypedDict):
type: Literal["unsupportedConnReq"]
@@ -1014,6 +1023,7 @@ ChatErrorType = (
| ChatErrorType_chatNotStopped
| ChatErrorType_chatStoreChanged
| ChatErrorType_invalidConnReq
| ChatErrorType_simplexDomainNotReady
| ChatErrorType_unsupportedConnReq
| ChatErrorType_connReqMessageProhibited
| ChatErrorType_contactNotReady
@@ -1070,7 +1080,7 @@ ChatErrorType = (
| ChatErrorType_exception
)
ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"]
ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "simplexDomainNotReady", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"]
ChatFeature = Literal["timedMessages", "fullDelete", "reactions", "voice", "files", "calls", "sessions"]
@@ -1418,6 +1428,7 @@ class ContactAddressPlan_ok(TypedDict):
type: Literal["ok"]
contactSLinkData_: NotRequired["ContactShortLinkData"]
ownerVerification: NotRequired["OwnerVerification"]
verifiedDomain: NotRequired["SimplexDomain"]
class ContactAddressPlan_ownLink(TypedDict):
type: Literal["ownLink"]
@@ -1553,6 +1564,10 @@ class ErrorType_EXPIRED(TypedDict):
class ErrorType_INTERNAL(TypedDict):
type: Literal["INTERNAL"]
class ErrorType_NAME(TypedDict):
type: Literal["NAME"]
nameErr: "NameErrorType"
class ErrorType_DUPLICATE_(TypedDict):
type: Literal["DUPLICATE_"]
@@ -1571,10 +1586,11 @@ ErrorType = (
| ErrorType_LARGE_MSG
| ErrorType_EXPIRED
| ErrorType_INTERNAL
| ErrorType_NAME
| ErrorType_DUPLICATE_
)
ErrorType_Tag = Literal["BLOCK", "SESSION", "CMD", "PROXY", "AUTH", "BLOCKED", "SERVICE", "CRYPTO", "QUOTA", "STORE", "NO_MSG", "LARGE_MSG", "EXPIRED", "INTERNAL", "DUPLICATE_"]
ErrorType_Tag = Literal["BLOCK", "SESSION", "CMD", "PROXY", "AUTH", "BLOCKED", "SERVICE", "CRYPTO", "QUOTA", "STORE", "NO_MSG", "LARGE_MSG", "EXPIRED", "INTERNAL", "NAME", "DUPLICATE_"]
FeatureAllowed = Literal["always", "yes", "no"]
@@ -1828,6 +1844,7 @@ class GroupInfo(TypedDict):
membersRequireAttention: int # int
viaGroupLinkUri: NotRequired[str]
groupKeys: NotRequired["GroupKeys"]
groupDomainVerified: NotRequired[bool]
class GroupKeys(TypedDict):
publicGroupId: str
@@ -1851,6 +1868,7 @@ class GroupLinkPlan_ok(TypedDict):
groupSLinkInfo_: NotRequired["GroupShortLinkInfo"]
groupSLinkData_: NotRequired["GroupShortLinkData"]
ownerVerification: NotRequired["OwnerVerification"]
verifiedDomain: NotRequired["SimplexDomain"]
class GroupLinkPlan_ownLink(TypedDict):
type: Literal["ownLink"]
@@ -2089,6 +2107,8 @@ class LocalProfile(TypedDict):
peerType: NotRequired["ChatPeerType"]
localBadge: NotRequired["LocalBadge"]
localAlias: str
contactDomain: NotRequired["SimplexDomainClaim"]
contactDomainVerified: NotRequired[bool]
MemberCriteria = Literal["all"]
@@ -2221,6 +2241,20 @@ MsgReceiptStatus = Literal["ok", "badMsgHash"]
MsgSigStatus = Literal["verified", "signedNoKey"]
class NameErrorType_NO_RESOLVER(TypedDict):
type: Literal["NO_RESOLVER"]
class NameErrorType_NOT_FOUND(TypedDict):
type: Literal["NOT_FOUND"]
class NameErrorType_RESOLVER(TypedDict):
type: Literal["RESOLVER"]
resolverErr: str
NameErrorType = NameErrorType_NO_RESOLVER | NameErrorType_NOT_FOUND | NameErrorType_RESOLVER
NameErrorType_Tag = Literal["NO_RESOLVER", "NOT_FOUND", "RESOLVER"]
class NetworkError_connectError(TypedDict):
type: Literal["connectError"]
connectError: str
@@ -2340,6 +2374,7 @@ class Profile(TypedDict):
preferences: NotRequired["Preferences"]
peerType: NotRequired["ChatPeerType"]
badge: NotRequired["BadgeProof"]
contactDomain: NotRequired["SimplexDomainClaim"]
class ProxyClientError_protocolError(TypedDict):
type: Literal["protocolError"]
@@ -2381,7 +2416,7 @@ ProxyError_Tag = Literal["PROTOCOL", "BROKER", "BASIC_AUTH", "NO_SESSION"]
class PublicGroupAccess(TypedDict):
groupWebPage: NotRequired[str]
groupDomain: NotRequired[str]
groupDomainClaim: NotRequired["SimplexDomainClaim"]
domainWebPage: bool
allowEmbedding: bool
@@ -2724,16 +2759,35 @@ class SecurityCode(TypedDict):
class SimplePreference(TypedDict):
allow: "FeatureAllowed"
SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"]
class SimplexNameDomain(TypedDict):
class SimplexDomain(TypedDict):
nameTLD: "SimplexTLD"
domain: str
subDomain: list[str]
class SimplexDomainClaim(TypedDict):
domain: str
proof: NotRequired["SimplexDomainProof"]
class SimplexDomainError_noValidLink(TypedDict):
type: Literal["noValidLink"]
class SimplexDomainError_unknownDomain(TypedDict):
type: Literal["unknownDomain"]
SimplexDomainError = SimplexDomainError_noValidLink | SimplexDomainError_unknownDomain
SimplexDomainError_Tag = Literal["noValidLink", "unknownDomain"]
class SimplexDomainProof(TypedDict):
linkOwnerId: NotRequired[str]
presHeader: str
signature: str
SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"]
class SimplexNameInfo(TypedDict):
nameType: "SimplexNameType"
nameDomain: "SimplexNameDomain"
nameDomain: "SimplexDomain"
SimplexNameType = Literal["publicGroup", "contact"]
+506
View File
@@ -0,0 +1,506 @@
# SimpleX names — simplification plan
Status: design agreed while reviewing branch `sh/namespace`. This supersedes the
name handling currently on that branch. Line refs are to the working tree at
review time; re-check before editing.
## 1. Goal
Reduce the feature to the minimum coherent, secure shape:
- **One** name per entity, on the **profile** (the entity's claimed identity),
typed `Maybe SimplexNameInfo`. No second "locally-known" copy on the entity row.
- A local **verification status** as a 3-state `Maybe Bool` (not a timestamp):
not-attempted / failed / verified. A failed check never blocks connecting.
- Connect-by-name requires the entity to claim the name, and the result is created as verified.
- The **proof** (address-key signature, context-bound) is **in scope for link
contexts** — connect-by-name, 1-time invitations, contact addresses, channel
join links — so those names are verifiable in this release. Only a name with
**no link context** (a group member, or a name on an `XInfo` profile over an
established connection) is deferred: stored, but not shown until that gap closes.
Removed from the current branch: the `*.simplex_name` entity columns (the
"locally-known/ct" copy), the `connections.simplex_name` carrier, the four
partial UNIQUE indexes + "newer-claim-wins" clearing, and the `_verified_at`
timestamps.
## 2. Why (trust model)
A name resolves to a link via the agent (`resolveSimplexName`); a `NameRecord`
has **lists** of links (`nrSimplexContact`, `nrSimplexChannel`), so
verification matches against **any** of them. "Match any" is safe **only because
the namespace is an on-chain, ENS-style registry**: `nrOwner`/`nrResolver` are
Ethereum addresses (`Names/Record.hs:22-23,36`), so each name has a **single
owner** who sets all its links. An attacker can register *their* name → your
address (the offensive-name case, handled by the claim check below) but **cannot
add a link to your name** — on-chain ownership blocks impersonation. Everything
here depends on that; if names were not single-owner, "match any" would be exploitable.
The registrant of a name controls what it points to, with no proof that the
target address agreed to it — anyone can publish `@offensive → your address` or
`#offensive → your channel`. Therefore:
- One-directional verification ("does `resolve(name)` equal a stored link") is
insufficient: it confirms the registrant's assertion, not the address owner's.
- A profile **claiming** a name (and even a link) proves nothing — anyone can
copy a link into a profile. **Control of a link is proven only by an actual
connection/join through it.**
- Sound verification is the intersection: `resolve(name) → link`, **and** the
entity advertises that name in its profile (it claims the name), **and** that link is one
we connected/joined through (control-proven).
- Verifying a name without having connected through its link needs a **signature
by the address key over the name, bound to the presentation context** (§4.8).
This is **in scope** for link contexts: a 1-time invitation includes such a proof
bound to the invite, so resolving the name → address → address key verifies it.
Consequence: names are verifiable in this release for **channels** (join link),
**contacts connected via their address/name** (the link connected through), and
**1-time-invite contacts** (the in-scope address-key proof). The only case still
deferred is a name with **no link context at all** — a group member, or a name on
an `XInfo` profile over an established connection — which stays stored-but-not-shown.
## 3. Current branch state (to be changed)
Migration `M20260603_simplex_name` currently adds, on both SQLite and Postgres:
- `contacts.simplex_name`, `contacts.simplex_name_verified_at`
- `groups.simplex_name`, `groups.simplex_name_verified_at`
- `connections.simplex_name`
- `contact_profiles.simplex_name`, `group_profiles.simplex_name`
- UNIQUE indexes `idx_contacts_simplex_name`, `idx_groups_simplex_name`,
`idx_contact_profiles_simplex_name`, `idx_group_profiles_simplex_name`
- `server_operators.smp_role_names` (= 1 for `'simplex'`)
…and threads two names per entity through the types: `Contact.simplexName` /
`GroupInfo.simplexName` (from `*.simplex_name`, "locally known"), and
`LocalProfile.simplexName` (from `*_profiles.simplex_name`, "peer claim"), plus
a `verified_at` timestamp on the entity. `apiVerifySimplexName` verifies the
entity (ct) name against the **profile's self-asserted `contactLink`** for
contacts (wrong link), and against `preparedGroup.connLinkToConnect` for groups
(correct). The `connections.simplex_name` carrier is plumbed through
`createConnection_` but **never written** (all callers pass `Nothing`).
## 4. Target design
### 4.1 Name type and JSON encoding
The name type is **`SimplexNameInfo`** (simplexmq `Simplex/Messaging/SimplexName.hs:37`),
which has:
- `StrEncoding` (`SimplexName.hs:89`) — canonical `simplex:/name@…` / `#…` string,
- a text `ToField` (`SimplexName.hs:146`) — stores as TEXT,
- a JSON **object** instance: `$(J.deriveJSON defaultJSON ''SimplexNameInfo)`
(`SimplexName.hs:154`) → `{nameType, nameDomain}`.
**Keep the object JSON** — the UI/API needs the structured form (it reads the name
off `LocalProfile`, `CRSimplexNameVerified`, …). The conflict is only on the
**wire**: `PublicGroupAccess.groupDomain` is a **released** field typed
`Maybe Text` (a JSON string), so the wire form of the name must stay a string.
Resolve by wrapping **only the wire fields** with simplexmq's generic newtype
`StrJSON` (`Simplex/Messaging/Encoding/String.hs:265`), whose `ToJSON`/`FromJSON`
go through `StrEncoding` → a JSON string (`String.hs:267272`; idiom
`deriving (ToJSON, FromJSON) via (StrJSON "X" T)`):
- wire (`Profile`, `GroupProfile`/`PublicGroupAccess`):
`Maybe (StrJSON "SimplexName" SimplexNameInfo)` → JSON **string**, byte-identical
on the wire to the released `Maybe Text`.
- local/UI (`LocalProfile`): `Maybe SimplexNameInfo`, **unwrapped** → JSON **object**.
Wrap/unwrap at the `Profile``LocalProfile` boundary (`toLocalProfile` /
`fromLocalProfile`). `SimplexNameInfo`'s own JSON instance is untouched, so
`CRSimplexNameVerified` and any other UI-facing use stay object.
Inherent asymmetry: there is no `LocalGroupProfile`, so the channel name reaches
the UI as a **string** (via `GroupProfile`, which is `StrJSON`), while the contact
name reaches it as an **object** (via `LocalProfile`). If the UI needs the channel
name as an object too, add a decoded field on `GroupInfo` (follow-up). The bot-API
binding generator must render `StrJSON`-wrapped fields as `string`.
DB stays TEXT: `ToField` (`SimplexName.hs:146`) on write; on read a **hard**
`FromField SimplexNameInfo` (add it — `SimplexName.hs:141-145` says to define it
"when a consumer requires the row-fail behaviour"), so an invalid stored name fails the
row — matching the wire, where a name that won't `strDecode` fails the profile. **No
soft-decode** (`decodeSimplexName` dropped for the name columns).
### 4.2 Types (field changes)
- `Profile.contactDomain :: Maybe (StrJSON "SimplexName" SimplexNameInfo)` — NEW;
JSON string (§4.1). The entity's advertised contact name. (Replaces the branch's
`Profile.simplexName`.)
- `Profile.contactDomainProof :: Maybe ClaimProof` — NEW; a flat sibling of
`contactDomain`/`contactLink`, a **wire profile field like `Profile.badge`**. The
**stored** own profile (`contact_profiles`) has the name but **no proof**; the
proof is generated and **added to the outgoing profile at send/save** (§4.8), exactly
as the badge is. `PHSimplexLink` (verifiable) when the profile is saved to a contact
address / 1-time invite; `PHTest` over an established connection (the gap).
- `LocalProfile.contactDomain :: Maybe SimplexNameInfo` (unwrapped → object JSON)
and `LocalProfile.contactDomainVerification :: Maybe Bool` — local status, **not**
on wire `Profile`. `toLocalProfile` unwraps the `StrJSON`; `fromLocalProfile`
re-wraps `contactDomain` and drops the status (mirrors how `localBadge`'s status
is dropped).
- `PublicGroupAccess.groupDomain :: Maybe (StrJSON "SimplexName" SimplexNameInfo)`
— RETYPE from `Maybe Text` (`Types.hs:857`); JSON string, wire-identical to the
released text (§4.1). Owner-set, broadcast in `XGrpInfo`. **Only public, relay-backed
groups** (`groups.use_relays`) can have a name — they have a channel join link to
resolve to and an owner key (`groups.member_priv_key`, `chat_schema.sql:193`) to sign
with; p2p groups have neither. (`contactDomain` on a member is a contact attribute, unaffected.)
- `GroupInfo.groupDomainVerification :: Maybe Bool` — local status, read from the
`groups` table (there is no `LocalGroupProfile`, and `GroupProfile` is the wire
type, so the status cannot be sent with the profile).
- DROP `Contact.simplexName`, `GroupInfo.simplexName`, `Connection.simplexName`,
and both `*VerifiedAt` timestamps.
Asymmetry, intentional: contact name + status both live on `contact_profiles`
(exposed via `LocalProfile`); the group name lives on `group_profiles`
(exposed via `GroupProfile`/`PublicGroupAccess`) but its status lives on
`groups` (exposed via `GroupInfo`). This is forced — `group_profiles` is the
shared wire profile with no local columns, while `contact_profiles` already
holds local state (`local_alias`).
### 4.3 Schema — rewrite `M20260603_simplex_name` (branch unreleased)
Add:
- `contact_profiles`: `contact_domain TEXT`, `contact_domain_verification`
(nullable `INTEGER` SQLite / `SMALLINT` Postgres).
- `groups`: `group_domain_verification` (nullable `INTEGER`/`SMALLINT`).
- `server_operators.smp_role_names` (= 1 for `'simplex'`) — keep.
- `user_contact_links`: the contact-address **root signing key** (BLOB, mirroring
`groups.root_priv_key`) — captured from the 2-step short-link creation — so the
contact-address / 1-time-invite proof can be signed chat-side. (Not present today.)
No DB change for `group_profiles.group_domain` — it already exists (from
`M20260515_public_group_access`); only the Haskell type and JSON change.
Remove (vs the current branch migration): `contacts.simplex_name`,
`contacts.simplex_name_verified_at`, `groups.simplex_name`,
`groups.simplex_name_verified_at`, `connections.simplex_name`,
`contact_profiles.simplex_name`, `group_profiles.simplex_name`, and all four
UNIQUE indexes. No name uniqueness is enforced at the DB level — identity comes
from verification, not a constraint.
Verification column decode: `Maybe BoolInt → Maybe Bool` (`NULL` = not attempted,
`0` = failed, `1` = verified).
### 4.4 Storage functions (`Store/Shared.hs`, `Store/Direct.hs`, `Store/Groups.hs`)
- Read the name columns as `Maybe SimplexNameInfo` via the **hard** `FromField`
(§4.1) — an invalid name fails the row; no soft `decodeSimplexName`.
- `toContact` / `toGroupInfo`: read `contact_domain``LocalProfile.contactDomain`
and `contact_domain_verification``LocalProfile.contactDomainVerification`;
`group_profiles.group_domain``PublicGroupAccess.groupDomain` and
`groups.group_domain_verification``GroupInfo.groupDomainVerification`. Delete
the ct/cp split and the entity-`simplex_name` reads.
- `createContact_` / `createGroup_` / `createPreparedContact` / `createPreparedGroup`:
set the name on the **profile** columns only; drop the entity-`simplex_name`
argument and the `connections.simplex_name` carrier param on `createConnection_`.
- `updateContactProfile` / `updateGroupProfile`: write `contact_domain` /
`group_domain` from the received profile. Reset the verification to `NULL`
(not-attempted) **only when the name changes**; an XInfo/XGrpInfo with the
**same** name keeps the existing status (a verified name stays verified), exactly as
a badge does. No conflict clearing (no UNIQUE index).
- `getContactBySimplexName` / `getGroupIdBySimplexName`: look up by the **verified**
profile name (the name column joined with verification = `Just True`); on a miss
or unverified, fall through to resolve-and-connect. Consistent with the existing
by-address lookup `getContactViaShortLinkToConnect` (`Direct.hs:963`), which
matches the link with no verification check — a link is the identity, a name is a
claim that only becomes a usable pointer once verified.
### 4.5 Redaction (`Library/Internal.hs:1246`)
```haskell
redactedMemberProfile :: GroupInfo -> GroupMember -> Profile -> Profile
redactedMemberProfile g m Profile {…, contactLink, contactDomain} =
let allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect
in Profile { …
, shortDescr = removeSimplexLink =<< shortDescr -- via allowSimplexLinks
, contactLink = if allowSimplexLinks then contactLink else Nothing
, contactDomain = if allowDirect then contactDomain else Nothing
, contactDomainProof = Nothing } -- member profiles are contextless; never include a proof
```
- `allowDirect` is the single primitive (the `DirectMessages` permission); it
controls the name and is reused inside `allowSimplexLinks`. No `allowName` flag,
no second lookup.
- **Behavior changes vs current:** `contactLink` flips from unconditionally
dropped to controlled by `allowSimplexLinks` (a member's contact address becomes
visible whenever links+DMs are allowed — the meaning of "links allowed"); the
name follows the looser `allowDirect`. Rationale: a link is one-tap-to-connect
(low friction), a name only resolves if the recipient deliberately looks it up
(higher friction), so a group can forbid links yet allow name discovery, with
"DMs allowed" the floor for both.
- Signature takes `(GroupInfo, GroupMember)` and derives both flags inside, so
the rule lives in one place. Callers pass `(g, m)`; the own-profile path passes
`(g, membership g)` — behavior-preserving because
`groupFeatureUserAllowed f g ≡ groupFeatureMemberAllowed f (membership g) g`
(both reduce to `groupFeatureMemberAllowed' f (memberRole (membership g))
(fullGroupPreferences g)`, `Types.hs:646652`).
- Collapses `groupUserAllowSimplexLinks` (`Types.hs:656`) and the pre-computed
`allowSimplexLinks` wiring at the call sites (`Internal.hs:1244`,
`Subscriber.hs:842,2817,3239`, `Commands.hs:3748,4057`). `Commands.hs:3748`
has `Maybe GroupInfo` → "no group ⇒ no redaction" at the call site.
### 4.6 Resolution + verification (`Library/Commands.hs`)
Two cases that differ in whether verification can *fail*:
**Connect-by-name** (`connectPlanName` / `dispatchResolvedRecord`) — verification is
a **precondition** of connecting. After decoding the resolved short link's embedded
profile, **add the claim check**: require that profile's `contactDomain` /
`groupDomain` to equal the resolved name, else fail with `CESimplexNameNotFound`
("name unknown") and **do not connect**. (The current branch decodes the profile but
never compares the name — this is the missing check.) On success the prepared
contact/group is created with the name on the profile, `connLinkToConnect` = the
resolved link, verification = `Just True` (**created as verified**). There is no "failed"
outcome here — failing to resolve or to claim the name just means no connection.
**Connected NOT by name** (via an address link or a 1-time link) — the peer's profile
may *claim* a name; it starts **unverified** (`Nothing`). Verification is **post-hoc
and non-blocking**: keep `apiVerifySimplexName` (`/_verify simplex name`).
- **Contacts** verify by a **single path** — check `contactDomainProof` (§4.8):
resolve the claimed name → its link, validate the owner chain and **select the key by
the proof's `linkOwnerId`** (that owner's `ownerKey`, or the root key if `Nothing`
the usual contact-address case), check the `ClaimProof` signature over
`name <> presHeader`, and check the proof's `presHeader` link ==
`preparedContact.connLinkToConnect` (**not** `profile.contactLink` — the branch bug).
Contact addresses and 1-time invites both include the proof.
- **Channels** verify by **presence / link-match**: `resolve(#name)` includes
`preparedGroup.connLinkToConnect` (the join link), whose owner-signed data already
has `groupDomain` (no `ClaimProof` — §4.8).
Result is `Just True` (holds) or `Just False` (fails) — and **`Just False` must NOT
prevent the connection**, exactly as a failed *badge* verification doesn't. **This is
why the status must be 3-state** (`Nothing` not attempted / `Just False` failed /
`Just True` verified). Names that stay `Nothing` have **no proof/link context**
group members, and names on an `XInfo` profile over an established connection.
Open option: **auto-verify on connect** (run the same non-blocking check
automatically when connecting via address/1-time link), instead of only on demand.
Either way the status stays 3-state — auto-verification can fail without blocking.
Keep the pure helpers `firstNameLink` (per-type link pick, cross-type rejection) and
`linksMatch` (scheme-normalized compare).
### 4.7 Display (`View.hs`)
A name is shown **when there is a proof to verify**, together with its status —
verified, failed, or not-yet-verified. All three states are shown (a failed or
pending check still shows the name, flagged), so the user sees the name and its
trust level rather than a silent omission. Rendering those three states is **out of
scope for this PR** (UI work), but the core stores the data — the 3-state status and
the proof — to drive it. A name with **no proof to verify** — a group member, or a
name on an `XInfo` profile over an established connection — is **not shown**.
### 4.8 Proof — a flat profile field, signed by the address key, context-bound
A name resolves to a **contact address** (the persistent identity link). The proof
asserts "the owner of that address asserts this name", so it is **signed by the
address's key** (the resolved address's own key) — **not** by the
per-connection or 1-time-link key. Every proof is **tied to the link it's shown
through**, so a proof made for one link can't be reused on another; these are
distinct proofs. Store the link in a presentation header:
**rename `BadgePresHeader` (`Badges.hs:212`) → `ProofPresHeader`** (now shared by
badge and name proofs) and add a **`PHSimplexLink AConnShortLink`** constructor.
`AConnShortLink` (`Agent.Protocol.hs:1536`,
`forall m. ConnectionModeI m => ACSL (SConnectionMode m) (ConnShortLink m)`) is the
existing existential, so the context is either a 1-time invitation or a contact
address. The signed payload includes this header, and the **verifier compares the
header's link against the link the proof is presented through** (the current
invite/address) — that comparison is what makes a proof non-replayable across
links. The tag enum (`Badges.hs:200`), the `StrEncoding` (`:216`), and
`badgePresHeaderAccepted` (`:226`, → `proofPresHeaderAccepted`) each gain the new
variant.
**Type & wire** — a JSON object like `BadgeProof` (`Badges.hs:393`):
data ClaimProof = ClaimProof
{ linkOwnerId :: Maybe OwnerId, -- which owner signed; Nothing = root key (see below)
presHeader :: ProofPresHeader, -- context: PHSimplexLink <this invite> | PHTest
signature :: C.Signature 'C.Ed25519 -- by that owner's key, over: smpEncode name <> smpEncode presHeader
}
The signature is by the key of the signer's **owner identity** in the link's owner
chain (`OwnerAuth`, `Agent/Protocol.hs:1829`); `ClaimProof` has
**`linkOwnerId :: Maybe OwnerId`** (`OwnerId`, `:1827`) to name it, so the verifier
checks exactly that key (no iterating the owner list):
- **Channels** (which can have owners other than the address) sign with the user's **owner key**,
`linkOwnerId = Just oid`**not** the root key — `groups.member_priv_key`
(`chat_schema.sql:193`).
- A **contact address** has a single owner = its creator, so it signs with the **root
key**, `linkOwnerId = Nothing`.
The root key (`ShortLinkCreds.linkPrivSigKey`/`linkRootSigKey`, `:1482-83`) otherwise
only *authorizes* owners (`validateOwners`/`validateLinkOwners`, `:18461859`);
`Nothing` (root) is **allowed at validation**. Both keys live **chat-side** so signing
is in the chat layer — **but only channels store theirs today** (`groups`); a contact
address (`user_contact_links`, `:386`) has **no key column**, so we must add one
(mirroring `groups.root_priv_key`), captured from the 2-step short-link creation, to
sign the contact-address / 1-time-invite proofs. Signed payload =
`smpEncode name <> smpEncode presHeader` (`name` from the profile's `contactDomain`).
`presHeader` serialises as its `StrEncoding` string, `signature` base64url.
Per presentation context:
- **Contact address** (the name's own resolved link): presence in the address link's
owner-signed data already proves the name — but we **include the explicit `ClaimProof`
here too** (bound to the address) so contact verification is
one uniform path (always check the proof) rather than presence-for-addresses /
proof-for-invites. The address can also serve as the **badge** proof's context.
- **1-time invitation**: include a proof **signed by the address key, bound to the
1-time link** as context. Feasible **now** — the 1-time link is a unique,
single-use context; no general mechanism required. This is what makes
a 1-time-invite contact verifiable.
- **`XInfo` over an established connection**: no link context → the step that adds the proof sets
`PHTest` (unbound) → unverifiable → not shown. Left for later, same as the badge's
`PHTest`. (For **group members** the proof is dropped entirely by redaction, §4.5.)
Home: **inside the profile**`Profile.contactDomainProof :: Maybe ClaimProof`, a flat
sibling of `contactDomain`/`contactLink`, exactly like `Profile.badge`. It is **not**
stored on the own profile; like the badge, it is **generated fresh and added to the
outgoing profile at send/save** — when the profile is sent to a peer (`XInfo`) or saved
to a link — by signing `name <> presHeader` with the address root key and setting the
**destination as the `presHeader` context**. Because `presentUserBadge`
(`Internal.hs:2037`) already adds the badge proof to the outgoing profile at *both*
peer-sends and link-data writes, the name-proof step belongs in the **same
function**. The context decides whether a proof is useful: saving to a contact address /
1-time invite sets `PHSimplexLink(that link)` (verifiable — the in-scope cases); an
established-connection peer-send sets `PHTest` (unbound — left for later, same as the
badge). The receiver verifies and stores the 3-state status on
`LocalProfile.contactDomainVerification`, as `localBadge` holds the badge status.
Connect-by-name is **created as verified**.
**Scope:** the **useful** (`PHSimplexLink`) proofs — set when the profile is saved to
a contact address or 1-time invite — are in this change; the verifier resolves the name →
address → address key → checks the signature and the `presHeader` link. Channels use
presence (below). The contextless established-connection case (a `PHTest` name proof) and
group members are left for later.
Badges stay **in the profile** — person-scoped, presented per connection
(`presentUserBadge`, `Internal.hs:2037`), including over established connections via
`XInfo`. They share the presentation-header context type with name proofs but sign
with the **badge credential key**, not the address key. Two scopes, two homes.
The shared step sets **both** proof kinds the same way: `PHSimplexLink(link)`
when the destination is a link (contact address, 1-time invite — verifiable), `PHTest`
over an established connection (unbound — the gap). So name proofs and badges both use
`PHTest` only for the contextless established-connection case.
No group proof field: a channel is always joined via its **join link** (its own
address), whose owner-signed data already has `groupDomain`, and there is no
"advertised link ≠ resolved address" case for channels — so channels verify by
presence/link-match and need no `ClaimProof`. (If full symmetry is wanted later, add
`GroupProfile.groupDomainProof` the same way.)
### 4.9 Set-name API (in scope — currently missing)
Names are **pre-registered out of band** — the app does **not** call RNAME. The API
only **verifies** the name and **adds it to the profile**. The user must be able to
add/change/remove their own name from the UI; the branch has no command for the
**contact** name.
- **Contact name** — add `APISetUserName :: Maybe SimplexNameInfo -> ChatCommand`
(`Nothing` clears). On set:
1. Require an **address** — fail if none (the UI won't offer the action without one).
2. Require it to be a **short link** — if only a long link exists, create the short
link (the name resolves to a short link, and the check below is short-link-based).
3. Ensure that short link is **in the profile** (`contactLink`) — add it if missing.
4. **Verify**: resolve the name and compare the short link it points to against the
profile's `contactLink`; fail if they don't match (name not preregistered to this
address).
5. Set `LocalProfile.contactDomain` and **re-publish the contact-address link data**.
The `ClaimProof` is added when the profile is saved to that link (the send/save
proof-adding step, §4.8) — **not** produced by the API itself. (Steps 14 are the verify;
this step is set + re-publish.)
Needs a `ChatCommand` constructor + parser + handler.
- **Channel name** (public, relay-backed groups only) — a **separate**
`APISetPublicGroupName`, parallel to the contact one (not folded into
`SetPublicGroupAccess`): same require-address / require-short-link
/ link-in-profile / verify / set-`groupDomain` flow against the channel's **join
link**. Rationale: it mirrors the contact API, and the name's fail-able verify +
preconditions don't mix cleanly with the plain `web=`/`embed=`/`domain_page=` writes.
**Drop `domain=` from `SetPublicGroupAccess`** (`Commands.hs:5461`) so there's a single
verified path; factor out the shared `GroupProfile`-update + `XGrpInfo` broadcast so
both commands reuse it. Verify is **TLD-dependent**: resolve+compare for `TLDSimplex`,
a different/no check for `TLDWeb` (web domains don't resolve through the namespace).
- The own name has **no stored verification status** — the verify step checks it at
add time; the 3-state status is only for peers' names. No RNAME wiring (out of scope).
## 5. Removal checklist (from the current branch)
- `Contact.simplexName`, `GroupInfo.simplexName`, `Connection.simplexName`.
- `*VerifiedAt` timestamps (→ `Maybe Bool` status fields).
- `connections.simplex_name` column + the `createConnection_` carrier param + the
`XInfo` carrier consumption in `Subscriber.hs`.
- `contacts.simplex_name`, `groups.simplex_name` columns.
- The four partial UNIQUE indexes + `clearConflictingContactProfileSimplexName_`
/ `clearConflictingGroupProfileSimplexName_` + their call sites.
- `getContactBySimplexName` / `getGroupIdBySimplexName` against entity columns
(re-point or remove per 6.b).
## 6. Resolved decisions
a. **Wire-string encoding (§4.1):** `SimplexNameInfo` keeps object JSON; wire
fields are wrapped in `StrJSON` (string), `LocalProfile` stays unwrapped
(object). Channel name reaches the UI as a **string** (via `GroupProfile`) and
that is fine — no decoded-object field on `GroupInfo` (no reason to re-connect
to a channel you're in; channel names are only shown verified). The object form
matters for **contacts** (connect-from-groups, sharing), which `LocalProfile`
provides. Residual chore: teach the bot-API binding generator to emit `string`
for `StrJSON` fields and pick the `StrJSON` `name` Symbol.
b. **Lookup (§4.4):** re-point `getContactBySimplexName` (its one caller is
connect-by-name, `Commands.hs:4241`) to the **verified** `contact_profiles.
contact_domain`; miss/unverified ⇒ resolve-and-connect. Keeps the no-network
shortcut for already-known contacts. (`getGroupIdBySimplexName` has no external
caller — drop it.)
c. **Proof (§4.8):** a flat **`Profile.contactDomainProof :: Maybe ClaimProof`**, a wire
profile field like `Profile.badge`. Not stored on the own profile; **generated fresh and
added to the outgoing profile at send/save** (peer `XInfo` or save-to-link) by the
**same function** as the badge (`presentUserBadge`, which already runs at
peer-sends *and* link-data writes). Signed by the signer's **owner-identity key** — a
channel's **owner key** (`groups.member_priv_key`, `linkOwnerId = Just oid`)
or a contact address's **root key** (sole owner, `linkOwnerId = Nothing`) — over
`name <> presHeader`; `linkOwnerId` selects the verification key (`Nothing` = root,
allowed at validation). `PHSimplexLink` for address/invite saves, `PHTest` over
established connections (the gap). Verify checks the signature **and `presHeader`'s link
== the link actually used** (`connLinkToConnect`). Channels use presence (owner-signed
link data). **Gap:** the contact-address key isn't stored chat-side today
(`user_contact_links` has no key column) — add one to sign chat-side. Receiver status
on `LocalProfile.contactDomainVerification`.
d. **`redactedMemberProfile` contactLink exposure (§4.5):** intended — a member's
contact address becomes group-visible when links + DMs are allowed.
e. **Verify command + status (§4.6):** keep `apiVerifySimplexName` — required to
verify a claimed name for entities connected **not** by name (address / 1-time
link). Status is **3-state** because a failed name (or badge) verification must
**not** block the connection; connect-by-name is the only created-as-verified path
(and there a failure to resolve or to claim the name means no connection, not a failed state).
**Auto-verify on connect** (vs. on-demand only) is left open; it doesn't change
the 3-state requirement.
## 7. Rollout & scope
The **claim check** (§4.6) — a name resolves only if the **resolved link's own
data claims it** — is the anti-stray-names protection and **must ship regardless**:
a name someone registers against a real address must not "work" without that
address owner's agreement. It needs no signature (the address-case presence suffices),
so it lands with the core change.
The signed proof (§4.8) can ship in the **same** release (add + verify together) or
be **staged** — implemented at the core level with the user-facing name-addition
hidden in the UI until ready. Either way it stays a *core* change.
The `ProofPresHeader` rename + `PHSimplexLink` + letting badges opt into the link
context ripples through the badge code — accepted, and bounded:
- `Badges.hs`: `BadgePresHeader``ProofPresHeader`, `BadgePresHeaderTag`
`ProofPresHeaderTag`, `badgePresHeaderAccepted``proofPresHeaderAccepted`
(`:200, :212, :216, :226`); add the `PHSimplexLink AConnShortLink`
tag/constructor/`StrEncoding`/accepted-case; `badgeProof` (`:314`) takes the
renamed type.
- `presentUserBadge` (`Internal.hs:2037`) + its ~15 call sites — the **shared point that adds
proofs to the outgoing profile** (already runs at peer-sends *and* link-data writes): generalize it
to set **both** the badge proof and the name `ClaimProof` onto the outgoing profile,
using `PHSimplexLink link` where the destination is a link, `PHTest` otherwise.
The pure rename can land as a **standalone prep commit** ahead of the proof; the
`PHSimplexLink` wiring + name proof land with the proof work.
@@ -0,0 +1,243 @@
# SimpleX name UI: display + verify + set (iOS + Android/desktop)
Branch: `sh/namespace-ui` (rebased onto core `fc0582cf0` — the finalized verify API). This pass adds
displaying a contact's / channel's SimpleX name with verification state, verifying names (manual + auto),
and two screens for setting the user's own name and a channel's name.
**Upstream sync (2026-06-27):** core `sh/namespace` is at `5008b4e62` ("refactor setting user name" +
test/comment cleanup, on top of the verify-API split `fc0582cf0`). UI branch rebased onto it (backup tag
`backup-namespace-ui-pre-rebase5`); pure-frontend, rebase clean. All dep-touching changes across these
syncs were **wire-neutral**: cosmetic StrJSON label on `Profile.contactDomain`; `APISetUserName` handler
refactor (constructor `{userId, simplexName}`, `/_set_name` parser, `CRUserProfileUpdated`/`NoChange`
response unchanged); `Internal.hs` record-field reordering in bot profiles; store/view/test cleanup.
`Types.hs` model fields/JSON, verify/set commands, responses, `NameVerifyOutcome`, `NameClaimProof`, and
`SimplexNameInfo` are unchanged. **No UI code change required.**
## Scope
Ships (both iOS and Android/desktop):
- Models: decode name / proof / verification fields, add `NameClaimProof` + `SimplexNameInfo` helpers.
- Name display + 3-state verification indicator on contact info and channel info.
- Verify API calls + new response handling.
- "Verify SimpleX names" privacy toggle (default ON).
- Two "Set SimpleX name" screens (own name; channel name).
Out: no change to the core verify algorithm — the UI only triggers it and renders the result.
## Decisions
Single source of truth. The UX walkthrough shows the visuals; the implementation sections give file:line.
### Product / UX (confirmed)
- **A. Title** — rename to "SimpleX address and name".
- **B. Screen body copy** — placeholders for now (final copy TBD):
- Own: *"Set a SimpleX name so people can connect to you using @yourname instead of a link. The name
must already be registered to your address."*
- Channel: *"Set a SimpleX name so people can find this channel as #name. The name must be registered
to this channel's address."*
- **C. Own name read-only** — No; the current own name appears only inside the set screen.
- **D. Rename scope** — rename the shared string everywhere (settings-menu row + screen title).
- **E. Auto-verify trigger** — with the toggle ON, auto-verify on open only if stored state is `null`;
verified/failed shows the stored result, and tapping the indicator re-verifies.
- **F. Failure reason** — shown on tap of the red cross (alert); an inconclusive result shows a brief
alert on completion. Not shown inline.
### Technical approach
- **T1. Show the name only when a proof exists** (`contactDomainProof` / `groupDomainProof` != nil).
- **T2. One formatter + one parser.** `SimplexNameInfo.shortName` (display, mirrors `shortNameInfoStr`),
`editDomain` (prefix-less, prefills set fields), `SimplexNameInfo(parsing:)` (decode the encoded
`groupDomain` string). Contact display uses the decoded object; channel display parses `groupDomain`.
- **T3. Set screens send a name string; UI fixes only the type prefix.** `strP` reads `@`->contact /
`#`->group first, so the UI prepends `@` (own) / `#` (channel); the backend canonicalises the domain.
Empty input clears.
- **T4. Channel uses a raw `APIUpdateGroupProfile`** (documented at the call site): core has
`APISetUserName` but no `APISetGroupName`, so the channel name is set by re-sending the cloned
`GroupProfile` with `publicGroup.publicGroupAccess.groupDomain` updated.
- **T5. Gating.** Channel "Set SimpleX name" only when `useRelays && isOwner && publicGroup?.publicGroupAccess != nil`;
own "Set SimpleX name" lives in the existing-address branch (inherently requires an address).
Minor defaults (flag if wrong): iOS toggle in the first "Chats" Section (PrivacySettings.swift:85);
spinner delay ~300ms; channel name cleared via empty input; set-name fields rely on backend rejection
for validation (+ helper text).
## UX walkthrough
Same on both platforms. Nothing existing moves.
### Name + verification indicator (contact info / channel info)
A new line under the name, above the description, shown per T1.
```
Contact info Channel info
+------------------+ +------------------+
| [ photo ] | | [ photo ] |
| Alice | | My Team |
| @alice.simplex v| <- new | #myteam Verify | <- new
| "description..."| | "description..."|
+------------------+ +------------------+
(v = check) (Verify = action)
```
Name on the left, indicator on the right; the indicator depends on state:
| State (`*Verification`) | Name style | Indicator |
|---|---|---|
| Verified (`true`) | accent color | check mark in regular color |
| Failed (`false`) | code style | cross mark in red (tap -> failure reason, per F) |
| Not verified (`null`), toggle OFF | code style | "Verify name" action (accent) |
| Not verified (`null`), toggle ON | code style | auto-verify on open (per E) -> spinner -> result |
| Verifying (in-flight) | code style | delayed spinner (~300ms, so a fast result doesn't flash) |
### Set screens (2 new buttons -> 2 new screens)
1. **Own name** — on the "SimpleX address and name" screen, a new "Set SimpleX name" button just above
the "Or to share privately" section:
```
[ QR code ]
Share address
Business address (toggle)
Address settings
------------------
Set SimpleX name -> <- new
------------------
Or to share privately
Create 1-time link
```
2. **Channel name** — on channel info, in the existing "Advanced options" section (owners only):
```
Advanced options
Web access
Set SimpleX name -> <- new
```
Each opens the same view (parameterised by prefix / body text / save action): explanation + a text field
with a fixed prefix adornment (`@` own -> user types `alice.simplex`; `#` channel -> user types `myteam`)
+ Save. Empty + Save clears the name.
### Settings toggle
"Verify SimpleX names" toggle, default ON, in the privacy settings "Chats" section (iOS:
PrivacySettings.swift:85, no `MorePrivacyView`; Kotlin: `MorePrivacyView` Chats section, PrivacySettings.kt:118).
## Backend reference (core @ fc0582cf0)
Single source for wire/JSON facts.
- Display: `shortNameInfoStr` (simplexmq Protocol.hs:1594) — public group on default `.simplex` TLD with
empty subdomain -> `#myteam`; else prefix (`@`/`#`) + full domain (`@alice.simplex`, `#myteam.testing`).
- Encoded form: `strEncode SimplexNameInfo = "simplex:/name" <> ("@"|"#") <> fullDomain` (Protocol.hs:1565).
- JSON shapes: `LocalProfile.contactDomain :: Maybe SimplexNameInfo` -> JSON **object**;
`PublicGroupAccess.groupDomain :: Maybe (StrJSON SimplexNameInfo)` -> JSON **string** (encoded form).
- Verification: `LocalProfile.contactDomainVerification` / `GroupInfo.groupDomainVerification :: Maybe Bool`
-> JSON `true`/`false`/absent (decodes as Swift `Bool?` / Kotlin `Boolean?`); null=not attempted,
false=failed, true=verified.
- Proof: `LocalProfile.contactDomainProof` / `PublicGroupAccess.groupDomainProof :: Maybe NameClaimProof`
-> JSON object `{linkOwnerId?: string, presHeader: string, signature: string}` (all strings).
- Verify commands (manual; network/resolver errors are retryable `ChatErrorAgent`):
- `APIVerifyContactName {contactId}` -> `/_verify name @<contactId>`.
- `APIVerifyPublicGroupName {groupId}` -> `/_verify name #<groupId>`.
- Outcome `NVOVerified | NVOFailed Text | NVOInconclusive Text` -> persists `Just True` / `Just False` /
leaves unchanged. Responses `CRContactNameVerified {user, contact, verificationResult :: Maybe Text}` /
`CRGroupNameVerified {user, groupInfo, verificationResult :: Maybe Text}` return the **updated** entity
plus `Nothing`=verified / `Just reason`=failure-or-inconclusive text.
- Set own name: `APISetUserName userId (Maybe SimplexNameInfo)` -> `/_set_name <userId> [<name>]` (parsed by
`strP`, NOT json; Commands.hs:5438). Rejects `name is not registered to your address`. Response
`CRUserProfileUpdated` / `CRUserProfileNoChange`.
- Set channel name: `APIUpdateGroupProfile` (`/_group_profile #<id> <json>`, Commands.hs:5571) with
`groupDomain` set. Rejects `name is not registered to this channel`. Response `CRGroupUpdated`.
## Gotchas
1. **iOS Codable (compile-blocking).** `SimplexNameInfo`/`SimplexNameDomain`/`SimplexTLD`/`SimplexNameType`
are `Decodable`-only (ChatTypes.swift:5261-5281); adding `contactDomain` to the `Codable` `LocalProfile`
breaks `Encodable` synthesis -> make all four `Codable`. (Kotlin already `@Serializable`.)
2. **`groupDomain` string carries the `simplex:/name` prefix** -> strip it in `parsing`.
3. **`NameClaimProof` decoded for presence only.** UI just checks `!= nil`; if any field's JSON shape is
uncertain at implementation, decode permissively.
4. **Delayed spinner — no existing helper.** iOS: `@State var verifying` set true after `Task.sleep(~300ms)`,
guarded by an `inFlight` flag. Android: a small `LaunchedEffect(inFlight){ delay(300); show=true }` composable.
5. **Enum JSON parity** — backend `enumJSON (dropPrefix "TLD"/"NT")` lowercases -> matches the Swift/Kotlin
enum raw values.
6. **Navigation.** iOS `NavigationLink`; Android `ModalManager.start.showModalCloseable { ... }`
(template: PrivacySettings.kt:162).
---
## iOS implementation
### Models — `apps/ios/SimpleXChat/ChatTypes.swift`
- Make `SimplexNameInfo`/`SimplexNameDomain`/`SimplexTLD`/`SimplexNameType` (5261-5281) `Codable`.
- Add `NameClaimProof: Codable, Hashable { presHeader: String; signature: String; linkOwnerId: String? }`.
- `SimplexNameInfo` (5261): add `init?(parsing: String)`, `var shortName: String`, `var editDomain: String`.
- `LocalProfile` (153): add `contactDomain: SimplexNameInfo?`, `contactDomainProof: NameClaimProof?`,
`contactDomainVerification: Bool?`.
- `GroupInfo` (2506): add `groupDomainVerification: Bool?`.
- `PublicGroupAccess` (2616): keep `groupDomain: String?`; add `groupDomainProof: NameClaimProof?`.
### API — `apps/ios/Shared/Model/{AppAPITypes,SimpleXAPI}.swift`
- `ChatCommand`: `apiSetUserName(userId: Int64, name: String?)` -> `"/_set_name \(userId)" + (name.map{" "+$0} ?? "")`;
`apiVerifyContactName(contactId: Int64)` -> `"/_verify name @\(contactId)"`;
`apiVerifyPublicGroupName(groupId: Int64)` -> `"/_verify name #\(groupId)"`.
- `ChatResponse1`: add `contactNameVerified(user:contact:verificationResult:)` /
`groupNameVerified(user:groupInfo:verificationResult:)` cases (+ `responseType`/`details` entries).
Templates: `contactUpdated` AppAPITypes.swift:1114 / responseType:1193 / details:1267; `groupUpdated`:1140.
- Wrappers: `apiSetUserName(_:)`; `apiVerifyContactName(_:)` (updated `Contact` + reason);
`apiVerifyPublicGroupName(_:)` (updated `GroupInfo` + reason). Channel set reuses `apiUpdateGroup(_:_:)`.
### Views
- Reusable `SimplexNameView` subview rendering the 5-row state table (incl. delayed spinner + verify action).
- Contact: `ChatInfoView.swift` `contactInfoHeader()` — before the `shortDescr` block (~395), per T1 render
`SimplexNameView` from `contactDomain`/`contactDomainProof`/`contactDomainVerification`; onAppear auto-verify (E).
- Channel: `GroupChatInfoView.swift` `groupInfoHeader()` — before webPage (~334), same from parsed
`groupDomain` + `groupDomainProof` + `groupDomainVerification`; "Set SimpleX name" button in Advanced options (~248).
- Address view: `UserAddressView.swift` — "Set SimpleX name" Section above "Or to share privately" (194).
- New `SetSimplexNameView.swift` (own + channel modes).
- Title rename (A/D): the address screen's title is set by the presenter, not `UserAddressView`
(literal also at UserAddressLearnMore.swift:71) — locate the title source and rename everywhere.
- Toggle: `PrivacySettings.swift` main "Chats" Section (~85) `Toggle("Verify SimpleX names", isOn:)`
bound to `@AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES)`; declare the constant + default `true` in
the `appDefaults` dict (SettingsView.swift:88-105).
## Android/desktop implementation (multiplatform Kotlin)
### Models — `model/ChatModel.kt`
- Add `@Serializable data class NameClaimProof(presHeader: String, signature: String, linkOwnerId: String? = null)`.
- `SimplexNameInfo` (4875): add `companion fun parse(encoded): SimplexNameInfo?`, `val shortName`, `val editDomain`.
- `LocalProfile` (2061): add `contactDomain: SimplexNameInfo? = null`, `contactDomainProof: NameClaimProof? = null`,
`contactDomainVerification: Boolean? = null`.
- `GroupInfo` (2181): add `groupDomainVerification: Boolean? = null`.
- `PublicGroupAccess` (2323): keep `groupDomain: String?`; add `groupDomainProof: NameClaimProof? = null`.
### API — `model/SimpleXAPI.kt`
- `CC`: `ApiSetUserName(userId, name: String?)`, `ApiVerifyContactName(contactId)` -> `"/_verify name @$contactId"`,
`ApiVerifyPublicGroupName(groupId)` -> `"/_verify name #$groupId"` (+ cmdString entries).
- `CR`: `@SerialName("contactNameVerified") ContactNameVerified(user, contact, verificationResult: String?)`,
`GroupNameVerified(user, groupInfo, verificationResult: String?)` (+ `responseType`). Templates:
`ContactUpdated` SimpleXAPI.kt:6454 / responseType:6646; `GroupUpdated`:6500.
- Wrappers `apiSetUserName`, `apiVerifyContactName`, `apiVerifyPublicGroupName`; channel set reuses `apiUpdateGroupProfile`.
- Pref: `val privacyVerifySimplexNames = mkBoolPreference(SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES, true)` (~123).
### Views
- Reusable `SimplexNameView` composable (the 5-row state table).
- Contact: `views/chat/ChatInfoView.kt` — after `ChatInfoDescription(...)` (~759), per T1; onAppear auto-verify (E).
- Channel: `views/chat/group/GroupChatInfoView.kt` — after `ChatInfoDescription(...)` (~947), per T1;
"Set SimpleX name" button in Advanced options `SectionView` (~805).
- New `views/usersettings/SetSimplexNameView.kt` (own + channel modes).
- `UserAddressView.kt`: "Set SimpleX name" button above the one-time-link section (~347).
- Toggle: `PrivacySettings.kt` `MorePrivacyView` "Chats" Section (~118) `SettingsPreferenceItem(..., appPrefs.privacyVerifySimplexNames)`.
- `strings.xml`: `set_simplex_name`, `verify_simplex_name`, `verify_simplex_names`, screen titles/body;
rename `simplex_address` -> "SimpleX address and name" (D).
## Build / verify
- iOS: Xcode build of SimpleXChat + app (or swift build of the package targets).
- Android/desktop: `./gradlew` compile of `:common` (desktop target fastest).
- Manual: a peer with a verified name shows accent + check; tampered/failed shows red cross + reason on tap;
toggle OFF shows "Verify name"; toggle ON auto-verifies on open with a delayed spinner. Set own/channel
name; clearing works; core rejection surfaces as an alert.
## Commit plan (conventional)
1. `feat(names): decode name/proof/verification; add SimplexNameInfo helpers + NameClaimProof` (models, both)
2. `feat(names): verify API (contact/group) + response handling` (CC/CR, wrappers)
3. `feat(names): show name + verification state on contact and channel info` (SimplexNameView + headers)
4. `feat(names): "Verify SimpleX names" privacy toggle + auto-verify on open`
5. `feat(names): set user and channel SimpleX name screens`
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."98391fd677f547f610a17c48cc96d8c4b2d5e96e" = "1ajgidba08dq8yj7xyr9i47rfrlkzbr75j5nyyjcv0zf778sgv0m";
"https://github.com/simplex-chat/simplexmq.git"."836254a4c6bd5e17b40acbcaf77a83802d44919e" = "1g4385sv7i1h63yk3ch2kw8hprak1dzvr34v9mn6xz0fy10g8xlg";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+7
View File
@@ -40,6 +40,7 @@ library
Simplex.Chat.AppSettings
Simplex.Chat.Badges
Simplex.Chat.Badges.CLI
Simplex.Chat.Names
Simplex.Chat.Call
Simplex.Chat.Controller
Simplex.Chat.Delivery
@@ -144,6 +145,7 @@ library
Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at
Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain
Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster
Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name
else
exposed-modules:
Simplex.Chat.Archive
@@ -306,6 +308,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at
Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain
Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster
Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name
other-modules:
Paths_simplex_chat
hs-source-dirs:
@@ -579,12 +582,14 @@ test-suite simplex-chat-test
ChatTests.Forward
ChatTests.Groups
ChatTests.Local
ChatTests.Names
ChatTests.Profiles
ChatTests.Utils
JSONFixtures
JSONTests
MarkdownTests
MemberRelationsTests
NameResolver
MessageBatching
OperatorTests
ProtocolTests
@@ -665,6 +670,8 @@ test-suite simplex-chat-test
, unicode-transforms ==0.4.*
, unliftio ==0.2.*
, uri-bytestring >=0.3.3.1 && <0.4
, wai ==3.2.*
, warp ==3.3.*
default-language: Haskell2010
if flag(client_postgres)
other-modules:
+13 -10
View File
@@ -1,6 +1,7 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
@@ -27,8 +28,8 @@ module Simplex.Chat.Badges
maxXFTPFileSize,
maxFileSizeSupporter,
maxFileSizeLegend,
BadgePresHeaderTag (..),
BadgePresHeader (..),
ProofPresHeaderTag (..),
ProofPresHeader (..),
BadgePurchase (..),
BadgeMasterKey (..),
BadgeRequest (..),
@@ -197,9 +198,9 @@ maxXFTPFileSize = \case
-- presentation, not bound to any context; the 'T' tag marks it so master rejects it.
-- PHUnknown is the forward-compat catch-all for tags this version does not interpret.
data BadgePresHeaderTag = PHTestTag | PHUnknownTag Char
data ProofPresHeaderTag = PHTestTag | PHUnknownTag Char
instance StrEncoding BadgePresHeaderTag where
instance StrEncoding ProofPresHeaderTag where
strEncode = B.singleton . \case
PHTestTag -> 'T'
PHUnknownTag c -> c
@@ -209,11 +210,13 @@ instance StrEncoding BadgePresHeaderTag where
'T' -> PHTestTag
c -> PHUnknownTag c
data BadgePresHeader
data ProofPresHeader
= PHTest ByteString
| PHUnknown Char ByteString
deriving (Eq, Show)
deriving (ToJSON, FromJSON) via (StrJSON "ProofPresHeader" ProofPresHeader)
instance StrEncoding BadgePresHeader where
instance StrEncoding ProofPresHeader where
strEncode = \case
PHTest nonce -> strEncode PHTestTag <> nonce
PHUnknown c b -> strEncode (PHUnknownTag c) <> b
@@ -223,8 +226,8 @@ instance StrEncoding BadgePresHeader where
PHUnknownTag c -> PHUnknown c <$> A.takeByteString
-- v6.5.x accepts both; v7 will reject PHTest/PHUnknown
badgePresHeaderAccepted :: BadgePresHeader -> Bool
badgePresHeaderAccepted = \case
proofPresHeaderAccepted :: ProofPresHeader -> Bool
proofPresHeaderAccepted = \case
PHTest _ -> True
PHUnknown _ _ -> True
@@ -311,7 +314,7 @@ generateBadgeProof pk (BadgeCredential keyIdx masterKey signature badgeInfo) ph
fmap (\p -> BadgeProof keyIdx ph p badgeInfo) <$> bbsProofGen pk signature bbsBadgeHeader ph bbsBadgeDisclosedIndexes (badgeMessages masterKey badgeInfo)
-- application-level proof generation with a semantic presentation header
badgeProof :: BBSPublicKey -> BadgeCredential -> BadgePresHeader -> IO (Either String BadgeProof)
badgeProof :: BBSPublicKey -> BadgeCredential -> ProofPresHeader -> IO (Either String BadgeProof)
badgeProof pk cred ph = generateBadgeProof pk cred (BBSPresHeader $ strEncode ph)
-- Recipient-side: verify a badge proof with the configured key its index points to.
@@ -324,7 +327,7 @@ verifyBadge keys b@(BadgeProof keyIdx _ _ _) = case M.lookup keyIdx keys of
verifyBadgeWith :: BBSPublicKey -> BadgeProof -> IO Bool
verifyBadgeWith pk (BadgeProof _ ph@(BBSPresHeader phBytes) proof badgeInfo)
| either (const False) badgePresHeaderAccepted (strDecode phBytes) =
| either (const False) proofPresHeaderAccepted (strDecode phBytes) =
bbsProofVerify pk proof bbsBadgeHeader ph bbsBadgeDisclosedIndexes bbsBadgeMessageCount (badgeInfoMessages badgeInfo)
| otherwise = pure False
+21 -6
View File
@@ -417,6 +417,7 @@ data ChatCommand
| APIGetCallInvitations
| APICallStatus ContactId WebRTCCallStatus
| APIUpdateProfile {userId :: UserId, profile :: Profile}
| APISetUserDomain {userId :: UserId, simplexDomain :: Maybe SimplexDomain}
| APISetContactPrefs {contactId :: ContactId, preferences :: Preferences}
| APISetContactAlias {contactId :: ContactId, localAlias :: LocalAlias}
| APISetGroupAlias {groupId :: GroupId, localAlias :: LocalAlias}
@@ -442,6 +443,7 @@ data ChatCommand
| APILeaveGroup {groupId :: GroupId}
| APIListMembers {groupId :: GroupId}
| APIUpdateGroupProfile {groupId :: GroupId, groupProfile :: GroupProfile}
| APISetPublicGroupAccess GroupId PublicGroupAccess
| APICreateGroupLink {groupId :: GroupId, memberRole :: GroupMemberRole}
| APIGroupLinkMemberRole {groupId :: GroupId, memberRole :: GroupMemberRole}
| APIDeleteGroupLink {groupId :: GroupId}
@@ -527,15 +529,17 @@ data ChatCommand
| AddContact IncognitoEnabled
| APISetConnectionIncognito Int64 IncognitoEnabled
| APIChangeConnectionUser Int64 UserId -- new user id to switch connection to
| APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectionLink is used to report link parsing failure as special error
| APIPrepareContact UserId ACreatedConnLink ContactShortLinkData
| APIPrepareGroup UserId CreatedLinkContact DirectLink GroupShortLinkData
| APIConnectPlan {userId :: UserId, connectTarget :: Maybe AConnectTarget, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectTarget is used to report parsing failure as special error
| APIPrepareContact UserId ACreatedConnLink (Maybe SimplexDomain) ContactShortLinkData
| APIPrepareGroup UserId CreatedLinkContact DirectLink (Maybe SimplexDomain) GroupShortLinkData
| APIChangePreparedContactUser ContactId UserId
| APIChangePreparedGroupUser GroupId UserId
| APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent}
| APIConnectPreparedGroup {groupId :: GroupId, incognito :: IncognitoEnabled, ownerContact :: Maybe GroupOwnerContact, msgContent_ :: Maybe MsgContent}
| APIConnect {userId :: UserId, incognito :: IncognitoEnabled, preparedLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error
| Connect {incognito :: IncognitoEnabled, connLink_ :: Maybe AConnectionLink}
| Connect {incognito :: IncognitoEnabled, connTarget_ :: Maybe AConnectTarget}
| APIVerifyContactDomain {contactId :: ContactId}
| APIVerifyGroupDomain {groupId :: GroupId}
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
| DeleteContact ContactName ChatDeleteMode
@@ -774,6 +778,8 @@ data ChatResponse
| CRContactCode {user :: User, contact :: Contact, connectionCode :: Text}
| CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
| CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text}
| CRContactDomainVerified {user :: User, contact :: Contact, verificationFailure :: Maybe Text}
| CRGroupDomainVerified {user :: User, groupInfo :: GroupInfo, verificationFailure :: Maybe Text}
| CRTagsUpdated {user :: User, userTags :: [ChatTag], chatTags :: [ChatTagId]}
| CRNewChatItems {user :: User, chatItems :: [AChatItem]}
| CRChatItemUpdated {user :: User, chatItem :: AChatItem}
@@ -1090,7 +1096,7 @@ data InvitationLinkPlan
deriving (Show)
data ContactAddressPlan
= CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification}
= CAPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification, verifiedDomain :: Maybe SimplexDomain}
| CAPOwnLink
| CAPConnectingConfirmReconnect
| CAPConnectingProhibit {contact :: Contact}
@@ -1099,7 +1105,7 @@ data ContactAddressPlan
deriving (Show)
data GroupLinkPlan
= GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification}
= GLPOk {groupSLinkInfo_ :: Maybe GroupShortLinkInfo, groupSLinkData_ :: Maybe GroupShortLinkData, ownerVerification :: Maybe OwnerVerification, verifiedDomain :: Maybe SimplexDomain}
| GLPOwnLink {groupInfo :: GroupInfo}
| GLPConnectingConfirmReconnect
| GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo}
@@ -1399,6 +1405,12 @@ data ChatError
| ChatErrorRemoteHost {rhKey :: RHKey, remoteHostError :: RemoteHostError}
deriving (Show, Exception)
-- why a resolved SimpleX name could not be used (the name itself resolved; an unregistered name is the agent's NAME NOT_FOUND)
data SimplexDomainError
= SDENoValidLink -- the name's record has no usable contact/channel link
| SDEUnknownDomain -- the resolved link's profile has no name, or a different name
deriving (Eq, Show)
data ChatErrorType
= CENoActiveUser
| CENoConnectionUser {agentConnId :: AgentConnId}
@@ -1420,6 +1432,7 @@ data ChatErrorType
| CEChatNotStopped
| CEChatStoreChanged
| CEInvalidConnReq
| CESimplexDomainNotReady {simplexDomain :: SimplexDomain, simplexDomainError :: SimplexDomainError}
| CEUnsupportedConnReq
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
| CEConnReqMessageProhibited
@@ -1751,6 +1764,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SDE") ''SimplexDomainError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHE") ''RemoteHostError)
+1 -1
View File
@@ -140,7 +140,7 @@ createActiveUser cc CoreChatOpts {chatRelay} = \case
displayName <- T.pack <$> withPrompt "display name" getLine
createUser loop False $ mkProfile displayName
where
mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing}
mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing}
createUser onError clientService p =
execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, userChatRelay = BoolDef chatRelay, clientService = BoolDef clientService}) 0 `runReaderT` cc >>= \case
Right (CRActiveUser user) -> pure user
+234 -85
View File
@@ -13,6 +13,7 @@
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module Simplex.Chat.Library.Commands where
@@ -56,6 +57,7 @@ import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as V4
import Simplex.Chat.Library.Subscriber
import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), maxXFTPFileSize, mkBadgeStatus, verifyCredential)
import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim)
import Simplex.Chat.Call
import Simplex.Chat.Controller
import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..))
@@ -91,7 +93,7 @@ import Simplex.Chat.Types.Shared
import Simplex.Chat.Util (liftIOEither, zipWith3')
import qualified Simplex.Chat.Util as U
import Simplex.Chat.Web (webPreviewWorker)
import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSize, maxFileSizeHard)
import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSizeHard)
import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles)
import Simplex.Messaging.Agent.Protocol
@@ -105,11 +107,11 @@ import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.ShortLink as SL
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn)
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQSupportOff, pattern PQSupportOn)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
@@ -129,7 +131,7 @@ import qualified UnliftIO.Exception as E
import UnliftIO.IO (hClose)
import UnliftIO.STM
#if defined(dbPostgres)
import Data.Bifunctor (bimap, second)
import Data.Bifunctor (bimap, first, second)
import Simplex.Messaging.Agent.Client (SubInfo (..), getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, temporaryOrHostError)
#else
import Data.Bifunctor (bimap, first, second)
@@ -381,14 +383,14 @@ processChatCommand cxt nm = \case
user <- withFastStore $ \db -> do
user <- createUserRecordAt db (AgentUserId auId) (isTrue userChatRelay) service p True ts
mapM_ (setUserServers db user ts) uss
createPresetContactCards db cxt user `catchAllErrors` \_ -> pure ()
createPresetContactCards db user `catchAllErrors` \_ -> pure ()
createNoteFolder db user
pure user
atomically . writeTVar u $ Just user
pure $ CRActiveUser user
where
createPresetContactCards :: DB.Connection -> StoreCxt -> User -> ExceptT StoreError IO ()
createPresetContactCards db cxt user = do
createPresetContactCards :: DB.Connection -> User -> ExceptT StoreError IO ()
createPresetContactCards db user = do
createContact db cxt user simplexStatusContactProfile
createContact db cxt user simplexTeamContactProfile
chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)))
@@ -1491,6 +1493,24 @@ processChatCommand cxt nm = \case
withCurrentCall contactId $ \user ct call ->
updateCallItemStatus user ct call receivedStatus Nothing $> Just call
APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile)
APISetUserDomain userId domain_ -> withUserId userId $ \user@User {profile = p@LocalProfile {contactLink, contactDomain}} ->
if (claimDomain <$> contactDomain) == domain_
then pure $ CRUserProfileNoChange user
else do
cl' <- case domain_ of
Nothing -> pure contactLink
Just domain -> do
UserContactLink {shortLinkDataSet, connLinkContact = CCLink _ sl_} <- withFastStore (`getUserAddress` user)
case sl_ of
Just sl | shortLinkDataSet -> do
NameRecord {nrSimplexContact} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) domain
unless (nameResolvesTo sl nrSimplexContact) $ throwChatError $ CESimplexDomainNotReady domain SDENoValidLink
pure $ Just (CLShort sl)
_ -> throwCmdError "create the address short link and add it to name"
let p' = (fromLocalProfile p :: Profile) {contactDomain = mkDomainClaim <$> domain_, contactLink = cl'}
updateProfile_ user p' True $ withFastStore $ \db -> do
user' <- updateUserProfile db user p'
liftIO $ setUserSimplexDomain db user' domain_
APISetContactPrefs contactId prefs' -> withUser $ \user -> do
ct <- withFastStore $ \db -> getContact db cxt user contactId
updateContactPrefs user ct prefs'
@@ -1829,7 +1849,7 @@ processChatCommand cxt nm = \case
APIGroupInfo gId -> withUser $ \user ->
CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db cxt user gId)
APIGetUpdatedGroupLinkData groupId -> withUser $ \user -> do
gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
gInfo@GroupInfo {groupProfile = p} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
case p of
GroupProfile {publicGroup = Just PublicGroupProfile {groupLink = sLnk}} | useRelays' gInfo -> do
(_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- getShortLinkConnReq' nm user sLnk
@@ -1983,8 +2003,9 @@ processChatCommand cxt nm = \case
-- [incognito] generate profile for connection
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode
-- TODO [badges] bind link and badge to handshake context
linkProfile <- presentUserBadge user incognitoProfile $ userProfileDirect user incognitoProfile Nothing True
let userData = contactShortLinkData linkProfile Nothing
let userData = contactShortLinkData linkProfile {contactDomain = Nothing} Nothing
userLinkData = UserInvLinkData userData
(connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userLinkData) Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
@@ -2037,10 +2058,10 @@ processChatCommand cxt nm = \case
createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn
deleteAgentConnectionAsync (aConnId' conn)
pure conn'
APIConnectPlan userId (Just cLink) resolveKnown linkOwnerSig_ -> withUserId userId $ \user ->
uncurry (CRConnectionPlan user) <$> connectPlan user cLink resolveKnown linkOwnerSig_
APIConnectPlan userId (Just ct) resolveKnown linkOwnerSig_ -> withUserId userId $ \user ->
uncurry (CRConnectionPlan user) <$> connectPlan user ct resolveKnown linkOwnerSig_
APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq
APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do
APIPrepareContact userId accLink verifiedDomain contactSLinkData -> withUserId userId $ \user -> do
let ContactShortLinkData {profile, message, business} = contactSLinkData
welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId
case accLink of
@@ -2050,7 +2071,7 @@ processChatCommand cxt nm = \case
groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences
groupProfile = businessGroupProfile profile groupPreferences
gVar <- asks random
(gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing
(gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing Nothing
hostMember <- maybe (throwCmdError "no host member") pure hostMember_
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = CDGroupRcv gInfo Nothing hostMember
@@ -2063,7 +2084,7 @@ processChatCommand cxt nm = \case
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
ACCL _ (CCLink cReq _) -> do
ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId
ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId (True <$ verifiedDomain)
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = CDDirectRcv ct
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing Nothing
@@ -2075,14 +2096,10 @@ processChatCommand cxt nm = \case
Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci}
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTDirect chat
APIPrepareGroup userId ccLink direct groupSLinkData -> withUserId userId $ \user -> do
let GroupShortLinkData {groupProfile = gp@GroupProfile {description}, publicGroupData = publicGroupData_} = groupSLinkData
publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_
APIPrepareGroup userId ccLink direct verifiedDomain groupSLinkData -> withUserId userId $ \user -> do
let GroupShortLinkData {groupProfile = GroupProfile {description}} = groupSLinkData
welcomeSharedMsgId <- forM description $ \_ -> getSharedMsgId
let useRelays = not direct
subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember
gVar <- asks random
(gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_
(gInfo, hostMember_) <- preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId (True <$ verifiedDomain)
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = maybe (CDChannelRcv gInfo Nothing) (CDGroupRcv gInfo Nothing) hostMember_
cInfo = GroupChat gInfo Nothing
@@ -2265,10 +2282,28 @@ processChatCommand cxt nm = \case
CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct
CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do
(ccLink, plan) <- connectPlan user cLink False Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e
Connect incognito (Just ct) -> withUser $ \user -> do
let con m cReq = pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing))
(ccLink, plan) <- connectPlan user ct False Nothing `catchAllErrors` \e -> case ct of
ACTarget m (CTFullContact cReq) -> con m cReq
ACTarget m (CTInv (CLFull cReq)) -> con m cReq
_ -> throwError e
connectWithPlan user incognito ccLink plan
Connect _ Nothing -> throwChatError CEInvalidConnReq
APIVerifyContactDomain contactId -> withUser $ \user -> do
ct@Contact {profile = LocalProfile {contactDomain}, preparedContact} <- withFastStore $ \db -> getContact db cxt user contactId
let connLink_ = preparedContact >>= \PreparedContact {connLinkToConnect = ACCL m (CCLink _ sLnk_)} -> ACSL m <$> sLnk_
domain <- maybe (throwCmdError "contact has no name to verify") pure contactDomain
(verified, reason) <- verifyEntityDomain user nm NTContact domain connLink_
ct' <- maybe (pure ct) (\v -> withFastStore' $ \db -> setContactDomainVerified db user ct v) verified
pure $ CRContactDomainVerified user ct' reason
APIVerifyGroupDomain groupId -> withUser $ \user -> do
g@GroupInfo {groupProfile = GroupProfile {publicGroup}, preparedGroup} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
let connLink_ = preparedGroup >>= \PreparedGroup {connLinkToConnect = CCLink _ sLnk_} -> ACSL SCMContact <$> sLnk_
domain <- maybe (throwCmdError "group has no name to verify") pure $ publicGroup >>= publicGroupAccess >>= groupDomainClaim
(verified, reason) <- verifyEntityDomain user nm NTPublicGroup domain connLink_
g' <- maybe (pure g) (\v -> withFastStore' $ \db -> setGroupDomainVerified db user g v) verified
pure $ CRGroupDomainVerified user g' reason
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db cxt user contactId
ccLink <- case contactLink of
@@ -2284,7 +2319,7 @@ processChatCommand cxt nm = \case
toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct')
throwError e
ConnectSimplex incognito -> withUser $ \user -> do
plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing))
plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing Nothing))
connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan
DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm
ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing
@@ -2298,16 +2333,20 @@ processChatCommand cxt nm = \case
Left e -> throwError $ ChatErrorStore e
Right _ -> throwError $ ChatErrorStore SEDuplicateContactLink
subMode <- chatReadVar subscriptionMode
gVar <- asks random
rootKey@(rootPubKey, rootPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar
let entityId = C.sha256Hash $ C.pubKeyBytes rootPubKey
(ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True Nothing
ccLink' <- shortenCreatedLink ccLink
-- TODO [relays] relay: add identity, key to link data?
userData <-
if isTrue userChatRelay
then pure $ relayShortLinkData (userProfileDirect user Nothing Nothing True)
else (`contactShortLinkData` Nothing) <$> presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True)
let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
(connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData IKPQOn subMode
let ccLink'' = if isTrue userChatRelay then setShortLinkType CCTRelay ccLink' else ccLink'
withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode
withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode rootPrivKey
pure $ CRUserContactLinkCreated user ccLink''
CreateMyAddress -> withUser $ \User {userId} ->
processChatCommand cxt nm $ APICreateMyAddress userId
@@ -2538,7 +2577,7 @@ processChatCommand cxt nm = \case
then throwError e
else do
let relayResults = map toRelayResult results
toRelayResult (r, Left e) = AddRelayResult r (Just e)
toRelayResult (r, Left e') = AddRelayResult r (Just e')
toRelayResult (r, Right _) = AddRelayResult r Nothing
pure $ CRPublicGroupCreationFailed user relayResults
where
@@ -3094,11 +3133,15 @@ processChatCommand cxt nm = \case
updateGroupProfileByName gName $ \p -> p {description}
ShowGroupDescription gName -> withUser $ \user ->
CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db cxt user gName)
SetPublicGroupAccess gName access -> withUser $ \user -> do
gInfo@GroupInfo {groupProfile = p@GroupProfile {publicGroup}} <- withStore $ \db ->
getGroupIdByName db user gName >>= getGroupInfo db cxt user
APISetPublicGroupAccess gId access@PublicGroupAccess {groupDomainClaim = newClaim} -> withUser $ \user -> do
gInfo@GroupInfo {groupProfile = p@GroupProfile {publicGroup}} <- withStore $ \db -> getGroupInfo db cxt user gId
case publicGroup of
Just pg -> runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}}
Just pg@PublicGroupProfile {groupLink, publicGroupAccess = existingAccess} -> do
forM_ (claimDomain <$> newClaim) $ \newDomain ->
when (Just newDomain /= (claimDomain <$> (existingAccess >>= groupDomainClaim))) $ do
NameRecord {nrSimplexChannel} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) newDomain
unless (nameResolvesTo groupLink nrSimplexChannel) $ throwChatError $ CESimplexDomainNotReady newDomain SDENoValidLink
runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}}
Nothing -> throwChatError $ CECommandError "not a public group"
APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do
gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
@@ -3213,6 +3256,9 @@ processChatCommand cxt nm = \case
sqSecured <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode
let newStatus = if sqSecured then ConnSndReady else ConnJoined
void $ withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus
SetPublicGroupAccess gName access -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand cxt nm $ APISetPublicGroupAccess groupId access
CreateGroupLink gName mRole -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand cxt nm $ APICreateGroupLink groupId mRole
@@ -3741,9 +3787,7 @@ processChatCommand cxt nm = \case
-- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile
profileToSend <-
presentUserBadge user incognitoProfile $ case gInfo_ of
Just gInfo_' ->
let allowSimplexLinks = maybe True groupUserAllowSimplexLinks gInfo_'
in userProfileInGroup' user allowSimplexLinks incognitoProfile
Just gInfo_' -> userProfileInGroup' user gInfo_' incognitoProfile
Nothing -> userProfileDirect user incognitoProfile Nothing True
dm <- case gInfo_ of
Just (Just gInfo) | useRelays' gInfo -> case relayMemberId_ of
@@ -3819,16 +3863,16 @@ processChatCommand cxt nm = \case
-- non-incognito (filtered above), so the user's badge is presented; a profile update keeps the badge instead of clearing it
ctSndEvent :: ChangedProfileContact -> CM (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json)
ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = do
p <- presentUserBadge user' Nothing mergedProfile'
pure (ConnectionId connId, Nothing, XInfo p)
p'' <- presentUserBadge user' Nothing mergedProfile'
pure (ConnectionId connId, Nothing, XInfo p'')
ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq
ctMsgReq ChangedProfileContact {conn} =
fmap $ \SndMessage {msgId, msgBody} ->
(conn, MsgFlags {notification = hasNotification XInfo_}, (vrValue msgBody, [msgId]))
setMyAddressData :: User -> UserContactLink -> CM UserContactLink
setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _sLnk_, addressSettings} = do
setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _, addressSettings} = do
conn <- withFastStore $ \db -> getUserAddressConnection db cxt user
shortLinkProfile <- presentUserBadge user Nothing $ userProfileDirect user Nothing Nothing True
shortLinkProfile <- presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True)
-- TODO [short links] do not save address to server if data did not change, spinners, error handling
let userData
| isTrue userChatRelay = relayShortLinkData shortLinkProfile
@@ -4051,9 +4095,8 @@ processChatCommand cxt nm = \case
conn <- createRelayConnection db cxt user (groupMemberId' relayMember) connId ConnPrepared chatV subMode
pure (relayMember, conn, groupRelay)
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
allowSimplexLinks = groupUserAllowSimplexLinks gInfo
GroupMember {memberId = relayMemberId} = relayMember
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership
let relayInv = GroupRelayInvitation {
fromMember = MemberIdRole userMemberId userRole,
fromMemberProfile = membershipProfile,
@@ -4129,8 +4172,8 @@ processChatCommand cxt nm = \case
pure (gId, chatSettings)
_ -> throwCmdError "not supported"
processChatCommand cxt nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings
connectPlan :: User -> AConnectionLink -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan)
connectPlan user (ACL SCMInvitation cLink) _ sig_ = case cLink of
connectPlan :: User -> AConnectTarget -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan)
connectPlan user (ACTarget SCMInvitation (CTInv cLink)) _ sig_ = case cLink of
CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing Nothing
CLShort l -> do
let l' = serverShortLink l
@@ -4151,51 +4194,80 @@ processChatCommand cxt nm = \case
invitationReqAndPlan cReq sLnk_ cld ov = do
plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError)
pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan)
connectPlan user (ACL SCMContact cLink) resolveKnown sig_ = case cLink of
CLFull cReq -> do
connectPlan user (ACTarget SCMContact ct) resolveKnown sig_ = case ct of
CTFullContact cReq -> do
plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError)
pure (ACCL SCMContact $ CCLink cReq Nothing, plan)
CLShort l@(CSLContact _ ct _ _) ->
case ct of
CTShortContact nl ->
case ctType of
CCTContact ->
knownLinkPlans >>= \case
Just r -> pure r
Nothing -> do
l' <- resolveSLink
(FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l'
contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData)
let linkProfile_ = (\ContactShortLinkData {profile} -> profile) <$> contactSLinkData_
linkDomain_ = linkProfile_ >>= \Profile {contactDomain} -> claimDomain <$> contactDomain
verifiedDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing
refreshContact ct' = case (verifiedDomain, linkProfile_) of
(Just _, Just p) -> updateContactFromLinkData user ct' p
_ -> pure ct'
forM_ verifiedDomain $ \nameDomain ->
unless (linkDomain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain
withFastStore' (\db -> getContactWithoutConnViaShortAddress db cxt user l') >>= \case
Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct'))
Just ct' | not (contactDeleted ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct''))
_ -> do
contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData)
let ContactLinkData _ UserContactData {owners} = cData
ov = verifyLinkOwner rootKey owners l' sig_
plan <- contactRequestPlan user cReq contactSLinkData_ ov
pure (con cReq, plan)
case plan of
CPContactAddress cap@(CAPOk {}) -> pure (con l' cReq, CPContactAddress cap {verifiedDomain})
CPContactAddress (CAPKnown ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPKnown ct''))
CPContactAddress (CAPContactViaAddress ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct''))
_ -> pure (con l' cReq, plan)
where
knownLinkPlans = withFastStore $ \db ->
liftIO (getUserContactLinkViaShortLink db user l') >>= \case
Just UserContactLink {connLinkContact = CCLink cReq _} -> pure $ Just (con cReq, CPContactAddress CAPOwnLink)
liftIO (getUserContactLinkViaTarget db user nl') >>= \case
Just UserContactLink {connLinkContact} -> pure $ Just (ACCL SCMContact connLinkContact, CPContactAddress CAPOwnLink)
Nothing ->
getContactViaShortLinkToConnect db cxt user l' >>= \case
Just (cReq, ct') -> pure $ if contactDeleted ct' then Nothing else Just (con cReq, CPContactAddress (CAPKnown ct'))
Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db cxt user l'
getContactToConnect db cxt user nl' >>= \case
Just (ccl, ct') -> pure $ if contactDeleted ct' then Nothing else Just (ACCL SCMContact ccl, CPContactAddress (CAPKnown ct'))
Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl'
CCTGroup -> groupShortLinkPlan
CCTChannel -> groupShortLinkPlan
CCTRelay -> throwCmdError "chat relay links are not supported in this version"
where
l' = serverShortLink l
con cReq = ACCL SCMContact $ CCLink cReq (Just l')
gPlan (cReq, g) = if memberRemoved (membership g) then Nothing else Just (con cReq, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef [])))
nl' = case nl of
CTLink sl -> CTLink (serverShortLink sl)
CTName _ -> nl
ctType = case nl of
CTLink (CSLContact _ t _ _) -> t
CTName SimplexNameInfo {nameType = NTContact} -> CCTContact
CTName SimplexNameInfo {nameType = NTPublicGroup} -> CCTChannel
resolveSLink = case nl' of
CTLink l' -> pure l'
CTName n -> serverShortLink <$> resolveNameLink user n
con l' cReq = ACCL SCMContact $ CCLink cReq (Just l')
gPlan (ccl, g) = if memberRemoved (membership g) then Nothing else Just (ACCL SCMContact ccl, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef [])))
groupShortLinkPlan =
knownLinkPlans >>= \case
Just (_, CPGroupLink (GLPKnown g _ _ _))
| resolveKnown -> resolveKnownGroup g
Just r -> pure r
Nothing -> do
l' <- resolveSLink
(fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l'
groupSLinkData_ <- liftIO $ decodeLinkUserData cData
if
| not direct && unsupportedGroupType groupSLinkData_ -> pure (con (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_))
| not direct && null relays -> pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_))
| not direct && unsupportedGroupType groupSLinkData_ -> pure (con l' (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_))
| not direct && null relays -> pure (con l' (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_))
| otherwise -> do
let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd
linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId}
@@ -4206,17 +4278,29 @@ processChatCommand cxt nm = \case
(Nothing, Nothing) -> pure ()
_ -> throwChatError CEInvalidConnReq
let ov = verifyLinkOwner rootKey owners l' sig_
verifiedDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing
plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov
pure (con cReq, plan)
forM_ verifiedDomain $ \nameDomain ->
let domain_ = (\GroupProfile {publicGroup} -> claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) =<< case plan of
CPGroupLink (GLPOk _ (Just GroupShortLinkData {groupProfile}) _ _) -> Just groupProfile
CPGroupLink (GLPKnown GroupInfo {groupProfile} _ _ _) -> Just groupProfile
CPGroupLink (GLPOwnLink GroupInfo {groupProfile}) -> Just groupProfile
CPGroupLink (GLPConnectingProhibit (Just GroupInfo {groupProfile})) -> Just groupProfile
_ -> (\GroupShortLinkData {groupProfile} -> groupProfile) <$> groupSLinkData_
in unless (domain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain
pure $ case plan of
CPGroupLink glp@(GLPOk {}) -> (con l' cReq, CPGroupLink glp {verifiedDomain})
_ -> (con l' cReq, plan)
where
unsupportedGroupType = \case
Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel
_ -> False
knownLinkPlans = withFastStore $ \db ->
liftIO (getGroupInfoViaUserShortLink db cxt user l') >>= \case
Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g))
Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db cxt user l'
liftIO (getGroupInfoViaUserTarget db cxt user nl') >>= \case
Just (ccl, g) -> pure $ Just (ACCL SCMContact ccl, CPGroupLink (GLPOwnLink g))
Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl'
resolveKnownGroup g = do
l' <- resolveSLink
(fd@FixedLinkData {rootKey = rk}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' nm user l'
groupSLinkData_ <- liftIO $ decodeLinkUserData cData
let ov = verifyLinkOwner rk owners l' sig_
@@ -4224,7 +4308,16 @@ processChatCommand cxt nm = \case
(g', updated) <- case groupSLinkData_ of
Just sLinkData -> updateGroupFromLinkData user g sLinkData
_ -> pure (g, False)
pure (con (linkConnReq fd), CPGroupLink (GLPKnown g' (BoolDef updated) ov (ListDef glOwners)))
pure (con l' (linkConnReq fd), CPGroupLink (GLPKnown g' (BoolDef updated) ov (ListDef glOwners)))
-- resolve a name to its first contact/channel short link
resolveNameLink :: User -> SimplexNameInfo -> CM (ConnShortLink 'CMContact)
resolveNameLink user SimplexNameInfo {nameType, nameDomain} = do
NameRecord {nrSimplexContact, nrSimplexChannel} <-
withAgent $ \a -> resolveSimplexName a nm (aUserId user) nameDomain
let (candidates, ctType) = case nameType of
NTContact -> (nrSimplexContact, CCTContact)
NTPublicGroup -> (nrSimplexChannel, CCTChannel)
maybe (throwChatError $ CESimplexDomainNotReady nameDomain SDENoValidLink) pure $ firstNameLink ctType candidates
connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse
connectWithPlan user@User {userId} incognito ccLink plan
| connectionPlanProceed plan = do
@@ -4232,13 +4325,14 @@ processChatCommand cxt nm = \case
case plan of
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
processChatCommand cxt nm $ APIConnectContactViaAddress userId incognito contactId
CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _)
| ACCL SCMContact ccl <- ccLink -> joinChannelViaRelays ccl gld
CPContactAddress (CAPOk (Just sld) _ vName@(Just _)) -> connectContactViaName sld vName
CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _ vName)
| ACCL SCMContact ccl <- ccLink -> joinChannelViaRelays ccl gld vName
_ -> processChatCommand cxt nm $ APIConnect userId incognito $ Just ccLink
| otherwise = pure $ CRConnectionPlan user ccLink plan
where
joinChannelViaRelays :: CreatedLinkContact -> GroupShortLinkData -> CM ChatResponse
joinChannelViaRelays ccl gld = do
joinChannelViaRelays :: CreatedLinkContact -> GroupShortLinkData -> Maybe SimplexDomain -> CM ChatResponse
joinChannelViaRelays ccl gld vName = do
GroupInfo {groupId} <- prepareChannelGroup
processChatCommand cxt nm APIConnectPreparedGroup {groupId, incognito, ownerContact = Nothing, msgContent_ = Nothing}
`catchAllErrors` \e -> do
@@ -4246,13 +4340,19 @@ processChatCommand cxt nm = \case
throwError e
where
prepareChannelGroup =
processChatCommand cxt nm (APIPrepareGroup userId ccl False gld) >>= \case
processChatCommand cxt nm (APIPrepareGroup userId ccl False vName gld) >>= \case
CRNewPreparedChat _ (AChat SCTGroup (Chat (GroupChat gInfo _) _ _)) -> pure gInfo
_ -> throwChatError $ CEException "joinChannelViaRelays: unexpected response from APIPrepareGroup"
deletePreparedChannel groupId = do
gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId
deleteGroupConnections user gInfo False
withFastStore' $ \db -> deleteGroup db user gInfo
connectContactViaName :: ContactShortLinkData -> Maybe SimplexDomain -> CM ChatResponse
connectContactViaName sld vName =
processChatCommand cxt nm (APIPrepareContact userId ccLink vName sld) >>= \case
CRNewPreparedChat _ (AChat SCTDirect (Chat (DirectChat Contact {contactId}) _ _)) ->
processChatCommand cxt nm (APIConnectPreparedContact contactId incognito Nothing)
_ -> throwChatError $ CEException "connectContactViaName: unexpected response from APIPrepareContact"
invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan
invitationRequestPlan user cReq cld ov = do
maybe (CPInvitationLink (ILPOk cld ov)) (invitationEntityPlan cld ov)
@@ -4292,13 +4392,13 @@ processChatCommand cxt nm = \case
Nothing ->
withFastStore' (\db -> getContactWithoutConnViaAddress db cxt user cReqSchemas) >>= \case
Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct)
_ -> pure $ CPContactAddress (CAPOk cld ov)
_ -> pure $ CPContactAddress (CAPOk cld ov Nothing)
Just (RcvDirectMsgConnection Connection {connStatus} Nothing)
| connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk cld ov)
| connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk cld ov Nothing)
| otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect
Just (RcvDirectMsgConnection _ (Just ct))
| not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct)
| contactDeleted ct -> pure $ CPContactAddress (CAPOk cld ov)
| contactDeleted ct -> pure $ CPContactAddress (CAPOk cld ov Nothing)
| otherwise -> pure $ CPContactAddress (CAPKnown ct)
-- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?)
Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing
@@ -4313,12 +4413,12 @@ processChatCommand cxt nm = \case
connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db cxt user cReqHashes
gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db cxt user cReqHashes
case (gInfo_, connEnt_) of
(Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov)
(Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing)
-- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?)
(Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect
(Nothing, Just (RcvDirectMsgConnection _ (Just ct)))
| not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_)
| otherwise -> pure $ CPGroupLink (GLPOk linkInfo gld ov)
| otherwise -> pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing)
(Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection"
(Just gInfo, _) -> groupPlan gInfo linkInfo gld ov
groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan
@@ -4327,7 +4427,7 @@ processChatCommand cxt nm = \case
| not (memberActive membership) && not (memberRemoved membership) =
pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo)
| memberActive membership = pure $ CPGroupLink (GLPKnown gInfo (BoolDef False) ov (ListDef []))
| otherwise = pure $ CPGroupLink (GLPOk linkInfo gld ov)
| otherwise = pure $ CPGroupLink (GLPOk linkInfo gld ov Nothing)
contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact)
contactCReqSchemas crData =
( CRContactUri crData {crScheme = SSSimplex},
@@ -4665,11 +4765,56 @@ processChatCommand cxt nm = \case
gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId
a $ SRGroup gId scope (sendAsGroup' gInfo scope)
_ -> throwCmdError "not supported"
preparedGroupFromLink :: User -> CreatedLinkContact -> DirectLink -> GroupShortLinkData -> Maybe SharedMsgId -> Maybe Bool -> CM (GroupInfo, Maybe GroupMember)
preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId nameVerified = do
let GroupShortLinkData {groupProfile = gp, publicGroupData = publicGroupData_} = groupSLinkData
publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_
useRelays = not direct
subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember
gVar <- asks random
withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ nameVerified
getSharedMsgId :: CM SharedMsgId
getSharedMsgId = do
gVar <- asks random
liftIO $ SharedMsgId <$> encodedRandomBytes gVar 12
firstNameLink :: ContactConnType -> [Text] -> Maybe (ConnShortLink 'CMContact)
firstNameLink ctType = foldr (\t r -> nameLink t <|> r) Nothing
where
nameLink t = case strDecode @(ConnShortLink 'CMContact) (encodeUtf8 t) of
Right sl@(CSLContact _ ct _ _) | ct == ctType -> Just sl
_ -> Nothing
nameResolvesTo :: ConnShortLink 'CMContact -> [Text] -> Bool
nameResolvesTo sLnk = any (either (const False) (sameShortLinkContact sLnk) . strDecode . encodeUtf8)
verifyEntityDomain :: User -> NetworkRequestMode -> SimplexNameType -> SimplexDomainClaim -> Maybe AConnShortLink -> CM (Maybe Bool, Maybe Text)
verifyEntityDomain user nm nameType SimplexDomainClaim {domain = StrJSON domain, proof = proof_} connLink_ = case (proof_, connLink_) of
(Nothing, _) -> pure (Nothing, Just "no name proof to verify")
(_, Nothing) -> pure (Nothing, Just "no connection link to check the name against")
(Just proof, Just (ACSL SCMContact profileSLnk)) -> do
NameRecord {nrSimplexContact, nrSimplexChannel} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) domain
let resolvedLinks = case nameType of
NTContact -> nrSimplexContact
NTPublicGroup -> nrSimplexChannel
if not (nameResolvesTo profileSLnk resolvedLinks)
then pure (Just False, Just "the name does not resolve to this address")
else do
ok <- verifyDomainProof proof profileSLnk
pure (Just ok, if ok then Nothing else Just "the name proof was not signed by this address's owner")
(Just _, Just _) -> pure (Nothing, Just "unexpected connection link type for name verification")
where
verifyDomainProof :: SimplexDomainProof -> ShortLinkContact -> CM Bool
verifyDomainProof SimplexDomainProof {linkOwnerId, presHeader, signature} sLnk@(CSLContact _ ct srv key) = do
(FixedLinkData {rootKey}, ContactLinkData _ UserContactData {owners}) <- getShortLinkConnReq nm user sLnk
let ownerKey_ = case linkOwnerId of
Nothing -> Just rootKey
Just (StrJSON oid) -> ownerKey <$> find (\OwnerAuth {ownerId} -> ownerId == oid) owners
pure $ maybe False (\k -> C.verify' k signature proofPayload) ownerKey_
where
proofPayload = strEncode (Str "simplex_domain_v1", presHeader, domain, ct, srv, key)
data ConnectViaContactResult
= CVRConnectedContact Contact
| CVRSentInvitation Connection (Maybe Profile)
@@ -5146,6 +5291,7 @@ chatCommandP =
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
"/_call get" $> APIGetCallInvitations,
"/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP),
"/_set domain " *> (APISetUserDomain <$> A.decimal <*> optional (A.space *> strP)),
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias #" *> (APISetGroupAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
@@ -5286,6 +5432,7 @@ chatCommandP =
"/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <* A.space <*> (Just <$> msgTextP)),
"/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing),
"/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP),
"/_public group access #" *> (APISetPublicGroupAccess <$> A.decimal <* A.space <*> jsonP),
"/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)),
"/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole),
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
@@ -5303,8 +5450,8 @@ chatCommandP =
"/_contacts " *> (APIListContacts <$> A.decimal),
"/contacts" $> ListContacts,
"/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> ((" resolve=" *> onOffP) <|> pure False) <*> optional (" sig=" *> jsonP)),
"/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP),
"/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <* A.space <*> jsonP),
"/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <*> optional (" domain=" *> strP) <* A.space <*> jsonP),
"/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <*> optional (" domain=" *> strP) <* A.space <*> jsonP),
"/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal),
"/_set group user #" *> (APIChangePreparedGroupUser <$> A.decimal <* A.space <*> A.decimal),
"/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)),
@@ -5315,6 +5462,8 @@ chatCommandP =
"/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal),
("/connect" <|> "/c") *> (AddContact <$> incognitoP),
("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)),
"/_verify domain @" *> (APIVerifyContactDomain <$> A.decimal),
"/_verify domain #" *> (APIVerifyGroupDomain <$> A.decimal),
ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP,
@@ -5490,10 +5639,10 @@ chatCommandP =
onOffP = ("on" $> True) <|> ("off" $> False)
publicGroupAccessP = do
groupWebPage <- optional (" web=" *> (safeDecodeUtf8 <$> A.takeTill A.isSpace))
groupDomain <- optional (" domain=" *> (safeDecodeUtf8 <$> A.takeTill A.isSpace))
groupDomain <- optional (" domain=" *> strP)
domainWebPage <- (" domain_page=" *> onOffP) <|> pure False
allowEmbedding <- (" embed=" *> onOffP) <|> pure False
pure PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding}
pure PublicGroupAccess {groupWebPage, groupDomainClaim = mkDomainClaim <$> groupDomain, domainWebPage, allowEmbedding}
profileNameDescr = (,) <$> displayNameP <*> shortDescrP
-- 'Help with bot':'link <ID>','Menu of commands':[...]
botCommandsP :: Parser [ChatBotCommand]
@@ -5514,7 +5663,7 @@ chatCommandP =
newUserP relay = do
(cName, shortDescr) <- profileNameDescr
service <- (" service=" *> onOffP) <|> pure False
let profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing}
let profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing}
pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef relay, clientService = BoolDef service}
newBotUserP = do
files_ <- optional $ "files=" *> onOffP <* A.space
@@ -5523,7 +5672,7 @@ chatCommandP =
let preferences = case files_ of
Just True -> Nothing
_ -> Just (emptyChatPrefs :: Preferences) {files = Just FilesPreference {allow = FANo}}
profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing}
profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing, contactDomain = Nothing}
pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef False, clientService = BoolDef service}
jsonP :: J.FromJSON a => Parser a
jsonP = J.eitherDecodeStrict' <$?> A.takeByteString
@@ -5635,9 +5784,9 @@ chatCommandP =
srvRolesP = srvRoles <$?> A.takeTill (\c -> c == ':' || c == ',')
where
srvRoles = \case
"off" -> Right $ ServerRoles False False
"proxy" -> Right ServerRoles {storage = False, proxy = True}
"storage" -> Right ServerRoles {storage = True, proxy = False}
"off" -> Right $ ServerRoles False False False
"proxy" -> Right ServerRoles {storage = False, proxy = True, names = False}
"storage" -> Right ServerRoles {storage = True, proxy = False, names = False}
"on" -> Right allRoles
_ -> Left "bad ServerRoles"
netCfgP = do
+34 -15
View File
@@ -53,7 +53,8 @@ import Data.Text.Encoding (encodeUtf8)
import Data.Time (addUTCTime)
import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds, secondsToDiffTime)
import Simplex.Chat.Badges (BadgeCredential (..), BadgePresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge)
import Simplex.Chat.Badges (BadgeCredential (..), ProofPresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge)
import Simplex.Chat.Names (SimplexDomainClaim (..), claimDomain)
import Simplex.Chat.Call
import Simplex.Chat.Controller
import Simplex.Chat.Files
@@ -1223,13 +1224,14 @@ introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn
sendGroupMemberMessages user gInfo conn evts
userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile
userProfileInGroup user = userProfileInGroup' user . groupUserAllowSimplexLinks
userProfileInGroup user g = userProfileInGroup' user (Just g)
{-# INLINE userProfileInGroup #-}
userProfileInGroup' :: User -> Bool -> Maybe Profile -> Profile
userProfileInGroup' User {profile = p} allowSimplexLinks incognitoProfile =
-- Nothing group ⇒ no redaction (e.g. joining via a link with no group profile yet).
userProfileInGroup' :: User -> Maybe GroupInfo -> Maybe Profile -> Profile
userProfileInGroup' User {profile = p} mg incognitoProfile =
let p' = fromMaybe (fromLocalProfile p) incognitoProfile
in redactedMemberProfile allowSimplexLinks p'
in maybe p' (\g -> redactedMemberProfile g (membership g) p') mg
memberInfo :: GroupInfo -> GroupMember -> MemberInfo
memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, activeConn} =
@@ -1237,16 +1239,18 @@ memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, a
{ memberId,
memberRole,
v = ChatVersionRange . peerChatVRange <$> activeConn,
profile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile memberProfile,
profile = redactedMemberProfile g m $ fromLocalProfile memberProfile,
memberKey = MemberKey <$> memberPubKey
}
where
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && groupFeatureMemberAllowed SGFDirectMessages m g
redactedMemberProfile :: Bool -> Profile -> Profile
redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDescr, image, peerType, badge} =
Profile {displayName, fullName, shortDescr = removeSimplexLink =<< shortDescr, image, contactLink = Nothing, preferences = Nothing, peerType, badge}
redactedMemberProfile :: GroupInfo -> GroupMember -> Profile -> Profile
redactedMemberProfile g m Profile {displayName, fullName, shortDescr, image, contactLink = lnk, peerType, badge, contactDomain} =
Profile {displayName, fullName, shortDescr = removeSimplexLink =<< shortDescr, image, contactLink, preferences = Nothing, peerType, badge, contactDomain = redactedDomain}
where
contactLink = if allowSimplexLinks then lnk else Nothing
redactedDomain = if allowDirect then (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain else Nothing
allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect
removeSimplexLink s
| allowSimplexLinks = Just s
| hasObfuscatedSimplexLink s = Nothing
@@ -1463,6 +1467,19 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = G
Just PublicGroupData {publicMemberCount} -> Just publicMemberCount /= localCount
_ -> False
updateContactFromLinkData :: User -> Contact -> Profile -> CM Contact
updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {contactDomain = prevClaim, contactDomainVerified}} linkProfile@Profile {contactDomain = newClaim}
| profileChanged || verifyChanged = do
cxt <- chatStoreCxt
withFastStore $ \db -> do
ct' <- updateContactProfile db cxt user ct linkProfile
if verifyChanged then liftIO $ setContactDomainVerified db user ct' True else pure ct'
| otherwise = pure ct
where
profileChanged = fromLocalProfile profile /= linkProfile
claimChanged = (claimDomain <$> prevClaim) /= (claimDomain <$> newClaim)
verifyChanged = contactDomainVerified /= Just True || claimChanged
-- TODO [relays] owner: set owners on updating link data (multi-owner)
groupLinkData :: GroupInfo -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData)
groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}, membership = GroupMember {memberId}, groupKeys} GroupLink {groupLinkId} groupRelays =
@@ -2050,6 +2067,7 @@ presentUserBadge User {profile = LocalProfile {localBadge}} incognitoProfile p =
Left e -> p <$ logError ("presentUserBadge: proof generation failed: " <> T.pack e)
_ -> pure p
-- receiving side of contact/invitation link data: verify the badge proof from the link profile
-- and set the crypto-free display badge for the UI (the raw proof stays in profile for APIPrepareContact)
linkDataBadge :: ContactShortLinkData -> CM ContactShortLinkData
@@ -2349,9 +2367,8 @@ sendGroupMessages user gInfo scope asGroup members events = do
_ -> False
sendProfileUpdate = do
let members' = filter (`supportsVersion` memberProfileUpdateVersion) members
allowSimplexLinks = groupUserAllowSimplexLinks gInfo
-- shouldSendProfileUpdate excludes incognito membership, so the badge is presented
profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile p
profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile gInfo (membership gInfo) $ fromLocalProfile p
void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate
currentTs <- liftIO getCurrentTime
withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs
@@ -3091,7 +3108,8 @@ simplexTeamContactProfile =
contactLink = Just $ CLFull adminContactReq,
peerType = Nothing,
preferences = Nothing,
badge = Nothing
badge = Nothing,
contactDomain = Nothing
}
simplexStatusContactProfile :: Profile
@@ -3104,7 +3122,8 @@ simplexStatusContactProfile =
contactLink = Just (either error CLFull $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"),
peerType = Just CPTBot,
preferences = Nothing,
badge = Nothing
badge = Nothing,
contactDomain = Nothing
}
timeItToView :: String -> CM' a -> CM' a
+16 -21
View File
@@ -381,8 +381,6 @@ processAgentMessageConn :: StoreCxt -> User -> ACorrId -> ConnId -> AEvent 'AECo
processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = do
-- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert,
-- as in this case no need to ACK message - we can't process messages for this connection anyway.
-- SEDBException will be re-trown as CRITICAL as it is likely to indicate a temporary database condition
-- that will be resolved with app restart.
entity <- critical agentConnId $ withStore (\db -> getConnectionEntity db cxt user $ AgentConnId agentConnId) >>= updateConnStatus
case agentMessage of
END -> case entity of
@@ -838,8 +836,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
XGrpMemInfo memId _memProfile
| sameMemberId memId m -> do
let GroupMember {memberId = membershipMemId} = membership
allowSimplexLinks = groupUserAllowSimplexLinks gInfo
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership
-- TODO update member profile
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
allowAgentConnectionAsync user conn' confId $ XGrpMemInfo membershipMemId membershipProfile
@@ -1400,13 +1397,13 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
CFSetShortLink ->
case (ucGroupId_, auData) of
(Just groupId, UserContactLinkData UserContactData {relays = relayLinks}) -> do
(gInfo, gLink, relays, relaysChanged, newlyActiveLinks, newlyActiveGMIds) <- withStore $ \db -> do
(gInfo, gLink, relays, relaysChanged, newlyActiveLinks) <- withStore $ \db -> do
gInfo <- getGroupInfo db cxt user groupId
gLink <- getGroupLink db user gInfo
relays <- liftIO $ getGroupRelays db gInfo
(relays', changed, newlyActiveLinks, newlyActiveGMIds) <- liftIO $ foldrM (updateRelay db) ([], False, [], []) relays
(relays', changed, newlyActiveLinks) <- liftIO $ foldrM (updateRelay db) ([], False, []) relays
liftIO $ setGroupInProgressDone db gInfo
pure (gInfo, gLink, relays', changed, newlyActiveLinks, newlyActiveGMIds)
pure (gInfo, gLink, relays', changed, newlyActiveLinks)
toView $ CEvtGroupLinkDataUpdated user gInfo gLink relays relaysChanged
let GroupSummary {publicMemberCount} = groupSummary gInfo
-- Owner is counted in publicMemberCount; > 1 means at least one subscriber.
@@ -1424,16 +1421,16 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
unless (null recipients) $
void $ sendGroupMessages user gInfo Nothing False recipients events
where
updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact], [GroupMemberId]) -> IO ([GroupRelay], Bool, [ShortLinkContact], [GroupMemberId])
updateRelay db relay@GroupRelay {groupMemberId, relayLink, relayStatus} (acc, changed, newlyActiveLinks, newlyActiveGMIds) =
updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact]) -> IO ([GroupRelay], Bool, [ShortLinkContact])
updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed, newlyActiveLinks) =
case relayLink of
Just rLink
-- version is gated upstream at publish (getPublishableGroupRelays): an RSAccepted relay
-- whose link is in the published data is necessarily pre-roster, so activate it too
| rLink `elem` relayLinks && (relayStatus == RSAcknowledgedRoster || relayStatus == RSAccepted) -> do
relay' <- updateRelayStatus db relay RSActive
pure (relay' : acc, True, rLink : newlyActiveLinks, groupMemberId : newlyActiveGMIds)
| rLink `elem` relayLinks -> pure (relay : acc, changed, newlyActiveLinks, newlyActiveGMIds)
pure (relay' : acc, True, rLink : newlyActiveLinks)
| rLink `elem` relayLinks -> pure (relay : acc, changed, newlyActiveLinks)
| relayStatus == RSActive -> do
-- Relay link absent from link data — deactivate.
-- RSAccepted relays are not deactivated: their own link data update
@@ -1442,8 +1439,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
-- TODO the SMP server, but this owner won't receive a LINK callback for it
-- TODO (LINK only fires in response to own setConnShortLink calls).
relay' <- updateRelayStatus db relay RSInactive
pure (relay' : acc, True, newlyActiveLinks, newlyActiveGMIds)
_ -> pure (relay : acc, changed, newlyActiveLinks, newlyActiveGMIds)
pure (relay' : acc, True, newlyActiveLinks)
_ -> pure (relay : acc, changed, newlyActiveLinks)
_ -> throwChatError $ CECommandError "LINK event expected for a group link only"
_ -> throwChatError $ CECommandError "unexpected cmdFunction"
MERR _ err -> do
@@ -2812,8 +2809,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
| otherwise =
pure m
where
contentChanged = not (sameProfileContent (redactedMemberProfile allowSimplexLinks (fromLocalProfile p)) (redactedMemberProfile allowSimplexLinks p'))
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo && groupFeatureMemberAllowed SGFDirectMessages m gInfo
contentChanged = not (sameProfileContent (redactedMemberProfile gInfo m (fromLocalProfile p)) (redactedMemberProfile gInfo m p'))
updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of
Just bc | isMainBusinessMember bc m -> do
g' <- withStore $ \db -> updateGroupProfileFromMember db user g p'
@@ -3228,8 +3224,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
pure toMember
subMode <- chatReadVar subscriptionMode
-- [incognito] send membership incognito profile, create direct connection as incognito
let allowSimplexLinks = groupUserAllowSimplexLinks gInfo
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership
dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile
-- [async agent commands] no continuation needed, but commands should be asynchronous for stability
groupConnIds <- joinAgentConnectionAsync user Nothing (chatHasNtfs chatSettings) groupConnReq dm subMode
@@ -3340,7 +3335,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
-- transfer record + its scratch file in one transaction (file owned by the transfer, keyed per source)
rft@RcvFileTransfer {fileId} <- withStore $ \db -> do
transferId <- liftIO $ createRosterTransfer db gInfo (groupMemberId' fromMember) newVer fileDigest (groupMemberId' author) brokerTs relayHdr
createRosterRcvFile db userId gInfo fromMember transferId sharedMsgId rosterFInv (Just IFMSent) (fromIntegral chSize)
createRosterRcvFile db userId gInfo fromMember transferId sharedMsgId rosterFInv (Just IFMSent) chSize
-- accept the chat-item-free file before chunk 1 (FIFO before it) so chunk 1 isn't rejected on RFSNew
-- transient scratch file (consumed into roster_blob, then deleted): temp folder, not the user's files folder / Downloads
tmpDir <- lift getChatTempDirectory
@@ -3366,10 +3361,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
Just RcvRosterTransfer {rosterTransferId = transferId, rosterTransferVersion = pendingVer, rosterTransferDigest = pendingDigest, rosterTransferOwnerGMId = ownerGMId, rosterTransferBrokerTs = rosterBrokerTs, rosterTransferHeader = header_} -> do
owner_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberById db cxt user ownerGMId)
blob <- readAssembledRoster
let isRelay = isUserGrpFwdRelay gInfo
let isRelay' = isUserGrpFwdRelay gInfo
ackErr err = do
cleanupRosterTransferById transferId
when isRelay $ forM_ owner_ $ \owner -> sendRosterAck gInfo owner pendingVer (Just err)
when isRelay' $ forM_ owner_ $ \owner -> sendRosterAck gInfo owner pendingVer (Just err)
if FD.FileDigest (LC.sha512Hash (LB.fromStrict blob)) /= pendingDigest
then ackErr "relay could not verify the roster blob"
else case parseAll rosterBlobP blob of
@@ -3393,7 +3388,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
forM_ results_ $ \results -> do
emitRosterResults gInfo author rosterBrokerTs results
-- ack while setting up (own status accepted/acknowledged); a serving (active) relay must not ack broadcasts.
when (isRelay && (relayOwnStatus gInfo == Just RSAccepted || relayOwnStatus gInfo == Just RSAcknowledgedRoster)) $ do
when (isRelay' && (relayOwnStatus gInfo == Just RSAccepted || relayOwnStatus gInfo == Just RSAcknowledgedRoster)) $ do
sendRosterAck gInfo author pendingVer Nothing
withStore' $ \db -> void $ updateRelayOwnStatusFromTo db gInfo RSAccepted RSAcknowledgedRoster
where
+61
View File
@@ -0,0 +1,61 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Chat.Names
( SimplexDomainClaim (..),
SimplexDomainProof (..),
mkDomainClaim,
claimDomain,
)
where
import qualified Data.Aeson.TH as JQ
import Simplex.Chat.Badges (ProofPresHeader)
import Simplex.Messaging.Agent.Protocol (OwnerId, SimplexDomain)
import Simplex.Messaging.Agent.Store.DB (fromTextField_)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
#if defined(dbPostgres)
import Database.PostgreSQL.Simple.FromField (FromField (..))
import Database.PostgreSQL.Simple.ToField (ToField (..))
#else
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
#endif
-- A name claim proof: signed by the address owner's key over proof payload - see verifyDomainProof.
data SimplexDomainProof = SimplexDomainProof
{ linkOwnerId :: Maybe (StrJSON "OwnerId" OwnerId),
presHeader :: ProofPresHeader,
signature :: C.Signature 'C.Ed25519
}
deriving (Eq, Show)
$(JQ.deriveJSON defaultJSON ''SimplexDomainProof)
instance ToField SimplexDomainProof where toField = toField . encodeJSON
instance FromField SimplexDomainProof where fromField = fromTextField_ decodeJSON
data SimplexDomainClaim = SimplexDomainClaim
{ domain :: StrJSON "SimplexDomain" SimplexDomain,
proof :: Maybe SimplexDomainProof
}
deriving (Eq, Show)
mkDomainClaim :: SimplexDomain -> SimplexDomainClaim
mkDomainClaim = (`SimplexDomainClaim` Nothing) . StrJSON
claimDomain :: SimplexDomainClaim -> SimplexDomain
claimDomain (SimplexDomainClaim n _) = unStrJSON n
$(JQ.deriveJSON defaultJSON ''SimplexDomainClaim)
+12 -6
View File
@@ -511,7 +511,9 @@ data UserServersError
| USEDuplicateChatRelayAddress {duplicateChatRelay :: Text, duplicateAddress :: ShortLinkContact}
deriving (Show)
data UserServersWarning = USWNoChatRelays {user :: Maybe User}
data UserServersWarning
= USWNoChatRelays {user :: Maybe User}
| USWNoNamesServers {user :: Maybe User}
deriving (Show)
validateUserServers :: UserServersClass u' => [u'] -> [(User, [UserOperatorServers])] -> ([UserServersError], [UserServersWarning])
@@ -552,15 +554,19 @@ validateUserServers curr others = (currUserErrs <> concatMap otherUserErrs other
addAddress (xs, dups) x
| any (sameShortLinkContact x) xs = (xs, x : dups)
| otherwise = (x : xs, dups)
currUserWarns = noChatRelaysWarns Nothing curr
otherUserWarns (user, uss) = noChatRelaysWarns (Just user) uss
currUserWarns = noChatRelaysWarns Nothing curr <> noNamesServersWarns Nothing curr
otherUserWarns (user, uss) = noChatRelaysWarns (Just user) uss <> noNamesServersWarns (Just user) uss
noChatRelaysWarns :: UserServersClass u => Maybe User -> [u] -> [UserServersWarning]
noChatRelaysWarns user uss
| noChatRelays opEnabled = [USWNoChatRelays user]
| otherwise = []
noChatRelaysWarns user uss = [USWNoChatRelays user | noChatRelays opEnabled]
where
noChatRelays cond = not $ any relayEnabled $ userChatRelays $ filter cond uss
relayEnabled (AUCR _ UserChatRelay {deleted, enabled}) = enabled && not deleted
noNamesServersWarns :: UserServersClass u => Maybe User -> [u] -> [UserServersWarning]
noNamesServersWarns user uss = [USWNoNamesServers user | noNamesServers]
where
noNamesServers = not $ any srvEnabled $ userServers SPSMP $ filter namesEnabled uss
srvEnabled (AUS _ UserServer {deleted, enabled}) = enabled && not deleted
namesEnabled = maybe True (\op@ServerOperator {enabled} -> enabled && names (operatorRoles SPSMP op)) . operator'
userChatRelays :: UserServersClass u => [u] -> [AUserChatRelay]
userChatRelays = map aUserChatRelay' . concatMap chatRelays'
opEnabled :: UserServersClass u => u -> Bool
+2 -2
View File
@@ -39,8 +39,8 @@ operatorFlux =
serverDomains = ["simplexonflux.com"],
conditionsAcceptance = CARequired Nothing,
enabled = True,
smpRoles = ServerRoles {storage = False, proxy = True},
xftpRoles = ServerRoles {storage = False, proxy = True}
smpRoles = ServerRoles {storage = False, proxy = True, names = True},
xftpRoles = allRoles
}
-- Please note: if any servers are removed from the lists below, they MUST be added here.
+1 -1
View File
@@ -10,7 +10,7 @@ generateRandomProfile :: IO Profile
generateRandomProfile = do
adjective <- pick adjectives
noun <- pickNoun adjective 2
pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing}
pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing}
where
pick :: [a] -> IO a
pick xs = (xs !!) <$> randomRIO (0, length xs - 1)
+8 -7
View File
@@ -116,15 +116,16 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id,
c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection,
c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.contact_status = ? AND c.deleted = 0
|]
(userId, contactId, CSActive)
toContact' :: UTCTime -> Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact
toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow) =
let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias}
toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) =
let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
activeConn = Just conn
@@ -143,26 +144,26 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.use_relays, g.relay_own_status,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified,
g.root_priv_key, g.root_pub_key, g.member_priv_key,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
-- GroupInfo {membership = GroupMember {memberProfile}}
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link,
-- from GroupMember
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link
FROM group_members m
+2 -2
View File
@@ -116,7 +116,7 @@ createOrUpdateContactRequest
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
@@ -152,7 +152,7 @@ createOrUpdateContactRequest
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p USING (contact_profile_id)
WHERE cr.user_id = ?
+66 -20
View File
@@ -46,10 +46,12 @@ module Simplex.Chat.Store.Direct
deleteContactWithoutGroups,
getDeletedContacts,
getContactByName,
getContactToConnect,
getContact,
getContactViaShortLinkToConnect,
getContactIdByName,
updateContactProfile,
setContactDomainVerified,
updateContactUserPreferences,
updateContactAlias,
updateContactConnectionAlias,
@@ -98,6 +100,7 @@ where
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
import Data.Bifunctor (first)
import Data.Either (rights)
import Data.Functor (($>))
import Data.Int (Int64)
@@ -108,16 +111,18 @@ import Data.Type.Equality
import Simplex.Chat.Badges (badgeToRow)
import Simplex.Chat.Messages
import Simplex.Chat.Store.Shared
import Simplex.Chat.Names (SimplexDomainClaim (..))
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionModeI (..), ConnectionRequestUri, CreatedConnLink (..), UserId)
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionModeI (..), ConnectionRequestUri, CreatedConnLink (..), SConnectionMode (..), SimplexNameInfo (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Crypto.Ratchet (PQSupport, pattern PQSupportOff)
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util ((<$$>))
#if defined(dbPostgres)
import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..))
import Database.PostgreSQL.Simple.SqlQQ (sql)
@@ -321,6 +326,7 @@ getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
@@ -397,13 +403,13 @@ createIncognitoProfile db User {userId} p = do
createdAt <- getCurrentTime
createIncognitoProfile_ db userId createdAt p
createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> ExceptT StoreError IO Contact
createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId = do
createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe Bool -> ExceptT StoreError IO Contact
createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId verified_ = do
currentTs <- liftIO getCurrentTime
let prepared = Just (connLinkToConnect, welcomeSharedMsgId)
ctUserPreferences = newContactUserPrefs user p
contactId <- createContact_ db cxt user p ctUserPreferences prepared "" currentTs
getContact db cxt user contactId
ct <- getContact db cxt user =<< createContact_ db cxt user p ctUserPreferences prepared "" currentTs
liftIO $ maybe (pure ct) (setContactDomainVerified db user ct) verified_
updatePreparedContactUser :: DB.Connection -> StoreCxt -> User -> Contact -> User -> ExceptT StoreError IO Contact
updatePreparedContactUser
@@ -559,22 +565,41 @@ updateContactProfile :: DB.Connection -> StoreCxt -> User -> Contact -> Profile
updateContactProfile db cxt user@User {userId} c p' = do
currentTs <- liftIO getCurrentTime
badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) lp p'
let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified
let nameVerified = if claimChanged then Nothing else prevVerification
profile = toLocalProfile profileId p'' localAlias currentTs badgeVerified nameVerified
updateContactProfile' currentTs badgeVerified profile
where
Contact {contactId, localDisplayName, profile = lp@LocalProfile {profileId, displayName, localAlias}, userPreferences} = c
Profile {displayName = newName, preferences} = p'
Contact {contactId, localDisplayName, profile = lp@LocalProfile {profileId, displayName, localAlias, contactDomain = prevClaim, contactDomainVerified = prevVerification}, userPreferences} = c
Profile {displayName = newName, contactDomain, preferences} = p'
mergedPreferences = contactUserPreferences user userPreferences preferences $ contactConnIncognito c
claimChanged = (domain <$> prevClaim) /= (domain <$> contactDomain)
p'' = (p' :: Profile) {contactDomain = (\d -> d {proof = if claimChanged then Nothing else proof =<< prevClaim}) <$> contactDomain}
clearVerificationIfClaimChanged =
when claimChanged $
DB.execute db "UPDATE contact_profiles SET contact_domain_verified = NULL WHERE user_id = ? AND contact_profile_id = ?" (userId, profileId)
updateContactProfile' currentTs badgeVerified profile
| displayName == newName = do
liftIO $ updateContactProfile_' db userId profileId p' badgeVerified currentTs
liftIO $ updateContactProfile_' db userId profileId p'' badgeVerified currentTs
liftIO clearVerificationIfClaimChanged
pure c {profile, mergedPreferences}
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
updateContactProfile_' db userId profileId p' badgeVerified currentTs
updateContactProfile_' db userId profileId p'' badgeVerified currentTs
updateContactLDN_ db user contactId localDisplayName ldn currentTs
clearVerificationIfClaimChanged
pure $ Right c {localDisplayName = ldn, profile, mergedPreferences}
setContactDomainVerified :: DB.Connection -> User -> Contact -> Bool -> IO Contact
setContactDomainVerified db User {userId} ct@Contact {contactId, profile = p} verified = do
DB.execute
db
[sql|
UPDATE contact_profiles SET contact_domain_verified = ?
WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?)
|]
(BI verified, userId, contactId)
pure (ct {profile = p {contactDomainVerified = Just verified}} :: Contact)
updateContactUserPreferences :: DB.Connection -> User -> Contact -> Preferences -> IO Contact
updateContactUserPreferences db user@User {userId} c@Contact {contactId} userPreferences = do
updatedAt <- getCurrentTime
@@ -706,16 +731,17 @@ updateContactProfile_ db userId profileId profile badgeVerified = do
updateContactProfile_' db userId profileId profile badgeVerified currentTs
updateContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO ()
updateContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge} badgeVerified updatedAt =
updateContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain, preferences, peerType, badge} badgeVerified updatedAt =
DB.execute
db
[sql|
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?,
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?,
contact_domain = ?, contact_domain_proof = ?
WHERE user_id = ? AND contact_profile_id = ?
|]
((displayName, fullName, shortDescr, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId))
((displayName, fullName, shortDescr, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId))
-- update only member profile fields (when member doesn't have associated contact - we can reset contactLink and prefs)
updateMemberContactProfileReset_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO ()
@@ -724,16 +750,17 @@ updateMemberContactProfileReset_ db userId profileId profile badgeVerified = do
updateMemberContactProfileReset_' db userId profileId profile badgeVerified currentTs
updateMemberContactProfileReset_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO ()
updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, image, badge} badgeVerified updatedAt =
updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactDomain, badge} badgeVerified updatedAt =
DB.execute
db
[sql|
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?,
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?,
contact_domain = ?, contact_domain_proof = ?
WHERE user_id = ? AND contact_profile_id = ?
|]
((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId))
((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId))
-- update only member profile fields (when member has associated contact - we keep contactLink and prefs)
updateMemberContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO ()
@@ -742,16 +769,17 @@ updateMemberContactProfile_ db userId profileId profile badgeVerified = do
updateMemberContactProfile_' db userId profileId profile badgeVerified currentTs
updateMemberContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO ()
updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, badge} badgeVerified updatedAt =
updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactDomain, badge} badgeVerified updatedAt =
DB.execute
db
[sql|
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?,
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?,
contact_domain = ?, contact_domain_proof = ?
WHERE user_id = ? AND contact_profile_id = ?
|]
((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId))
((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId))
updateContactLDN_ :: DB.Connection -> User -> Int64 -> ContactName -> ContactName -> UTCTime -> IO ()
updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt = do
@@ -770,6 +798,22 @@ getContactByName db cxt user localDisplayName = do
cId <- getContactIdByName db user localDisplayName
getContact db cxt user cId
getContactToConnect :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> ExceptT StoreError IO (Maybe (CreatedLinkContact, Contact))
getContactToConnect db cxt user@User {userId} = \case
CTLink sl -> first (`CCLink` Just sl) <$$> getContactViaShortLinkToConnect db cxt user sl
CTName ni ->
liftIO (maybeFirstRow id $ DB.query db byNameQuery (userId, nameDomain ni)) >>= \case
Just (ctId :: Int64, Just (ACR cMode cReq), Just (sLnk :: ShortLinkContact)) | Just Refl <- testEquality cMode SCMContact ->
Just . (CCLink cReq (Just sLnk),) <$> getContact db cxt user ctId
_ -> pure Nothing
where
byNameQuery =
[sql|
SELECT ct.contact_id, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect FROM contacts ct
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
WHERE ct.user_id = ? AND cp.contact_domain = ? AND cp.contact_domain_verified = 1 AND ct.deleted = 0
|]
getUserContacts :: DB.Connection -> StoreCxt -> User -> IO [Contact]
getUserContacts db cxt user@User {userId} = do
contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId)
@@ -809,7 +853,8 @@ contactRequestQuery =
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p USING (contact_profile_id)
|]
@@ -930,6 +975,7 @@ getContact_ db cxt user@User {userId} contactId deleted = do
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
+87 -38
View File
@@ -42,16 +42,18 @@ module Simplex.Chat.Store.Groups
setGroupInvitationChatItemId,
getGroup,
getGroupInfoByUserContactLinkConnReq,
getGroupInfoViaUserShortLink,
getGroupInfoViaUserTarget,
getGroupViaShortLinkToConnect,
getGroupInfoByGroupLinkHash,
updateGroupProfile,
setGroupDomainVerified,
updateGroupPreferences,
updateGroupProfileFromMember,
getGroupIdByName,
getGroupMemberIdByName,
getActiveMembersByName,
getGroupInfoByName,
getGroupToConnect,
getGroupMember,
getHostMember,
getMentionedGroupMember,
@@ -205,7 +207,7 @@ import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
import Crypto.Random (ChaChaDRG)
import Data.Bifunctor (second)
import Data.Bifunctor (first, second)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Char (toLower)
@@ -220,6 +222,7 @@ import qualified Data.Text as T
import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, getCurrentTime)
import Data.Text.Encoding (encodeUtf8)
import Simplex.Chat.Badges (BadgeRow, badgeToRow, verifyBadge_)
import Simplex.Chat.Names (SimplexDomainClaim (..))
import Simplex.Chat.Messages
import Simplex.Chat.Operators
import Simplex.Chat.Protocol hiding (Binary)
@@ -230,7 +233,7 @@ import Simplex.Chat.Types.MemberRelations (IntroductionDirection (..), MemberRel
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), UserId)
import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), SimplexNameInfo (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow)
import qualified Simplex.FileTransfer.Description as FD
import Simplex.Messaging.Encoding (smpDecode, smpEncode)
@@ -251,11 +254,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..))
import Database.SQLite.Simple.QQ (sql)
#endif
type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact)
type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact)
toMaybeGroupMember :: UTCTime -> Int64 -> MaybeGroupMemberRow -> Maybe GroupMember
toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) =
Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink))
toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) =
Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink))
toMaybeGroupMember _ _ _ = Nothing
createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink
@@ -395,9 +398,9 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays
INSERT INTO group_profiles
(display_name, full_name, short_descr, description, image,
group_type, group_link, public_group_id,
group_web_page, group_domain, domain_web_page, allow_embedding,
group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof,
user_id, preferences, member_admission, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup
:. (userId, groupPreferences, memberAdmission, currentTs, currentTs))
@@ -443,7 +446,8 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays
customData = Nothing,
membersRequireAttention = 0,
viaGroupLinkUri = Nothing,
groupKeys
groupKeys,
groupDomainVerified = Nothing
}
-- | creates a new group record for the group the current user was invited to, or returns an existing one
@@ -521,7 +525,8 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti
customData = Nothing,
membersRequireAttention = 0,
viaGroupLinkUri = Nothing,
groupKeys = Nothing
groupKeys = Nothing,
groupDomainVerified = Nothing
},
groupMemberId
)
@@ -637,8 +642,8 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile {
DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId)
DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId)
createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> GroupMemberRole -> Maybe Int64 -> ExceptT StoreError IO (GroupInfo, Maybe GroupMember)
createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays userMemberRole publicMemberCount_ = do
createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> GroupMemberRole -> Maybe Int64 -> Maybe Bool -> ExceptT StoreError IO (GroupInfo, Maybe GroupMember)
createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays userMemberRole publicMemberCount_ verified_ = do
currentTs <- liftIO getCurrentTime
let prepared = Just (connLinkToConnect, welcomeSharedMsgId)
(groupId, groupLDN) <- createGroup_ db userId groupProfile prepared Nothing useRelays Nothing publicMemberCount_ currentTs
@@ -657,7 +662,8 @@ createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile b
forM_ hostMember_ $ \hostMember ->
when business $ liftIO $ setGroupBusinessChatInfo groupId membership hostMember
g <- getGroupInfo db cxt user groupId
pure (g, hostMember_)
g' <- liftIO $ maybe (pure g) (setGroupDomainVerified db user g) verified_
pure (g', hostMember_)
where
insertHost_ currentTs groupId groupLDN = do
randHostId <- liftIO $ encodedRandomBytes gVar 12
@@ -899,9 +905,9 @@ createGroup_ db userId groupProfile prepared business useRelays relayOwnStatus p
INSERT INTO group_profiles
(display_name, full_name, short_descr, description, image,
group_type, group_link, public_group_id,
group_web_page, group_domain, domain_web_page, allow_embedding,
group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof,
user_id, preferences, member_admission, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup
:. (userId, groupPreferences, memberAdmission, currentTs, currentTs))
@@ -1068,6 +1074,21 @@ getGroupInfoByName db cxt user gName = do
gId <- getGroupIdByName db user gName
getGroupInfo db cxt user gId
getGroupToConnect :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> ExceptT StoreError IO (Maybe (CreatedLinkContact, GroupInfo))
getGroupToConnect db cxt user@User {userId} = \case
CTLink sl -> first (`CCLink` Just sl) <$$> getGroupViaShortLinkToConnect db cxt user sl
CTName ni ->
liftIO (maybeFirstRow id $ DB.query db byNameQuery (userId, nameDomain ni)) >>= \case
Just (gId :: Int64, Just cReq, Just (sLnk :: ShortLinkContact)) -> Just . (CCLink cReq (Just sLnk),) <$> getGroupInfo db cxt user gId
_ -> pure Nothing
where
byNameQuery =
[sql|
SELECT g.group_id, g.conn_full_link_to_connect, g.conn_short_link_to_connect FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
WHERE g.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified = 1
|]
getGroupMember :: DB.Connection -> StoreCxt -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember
getGroupMember db cxt user@User {userId} groupId groupMemberId = do
currentTs <- liftIO getCurrentTime
@@ -1929,7 +1950,7 @@ getRelayPublishableGroups db User {userId, userContactId} =
db
[sql|
SELECT g.group_id, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
JOIN group_members mu ON mu.group_id = g.group_id AND mu.contact_id = ?
@@ -2432,7 +2453,7 @@ createNewMember_
invitedBy,
invitedByGroupMemberId = memInvitedByGroupMemberId,
localDisplayName,
memberProfile = toLocalProfile memberContactProfileId memberProfile "" createdAt badgeVerified,
memberProfile = toLocalProfile memberContactProfileId memberProfile "" createdAt badgeVerified Nothing,
memberContactId,
memberContactProfileId,
activeConn,
@@ -2619,19 +2640,27 @@ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange
createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission}
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, publicGroup = oldPublicGroup}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission}
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
updateGroupProfile_ currentTs
pure (g :: GroupInfo) {groupProfile = p', fullGroupPreferences}
clearVerificationIfClaimChanged
pure $ (g' :: GroupInfo) {groupProfile = p', fullGroupPreferences}
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
updateGroupProfile_ currentTs
updateGroup_ ldn currentTs
pure $ Right (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}
clearVerificationIfClaimChanged
pure $ Right $ (g' :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}
where
fullGroupPreferences = mergeGroupPreferences groupPreferences
groupClaim pg = domain <$> (pg >>= publicGroupAccess >>= groupDomainClaim)
claimChanged = groupClaim oldPublicGroup /= groupClaim publicGroup
g' = if claimChanged then (g :: GroupInfo) {groupDomainVerified = Nothing} else g
clearVerificationIfClaimChanged =
when claimChanged $
DB.execute db "UPDATE groups SET group_domain_verified = NULL WHERE user_id = ? AND group_id = ?" (userId, groupId)
(groupType_, groupLink_) = case publicGroup of
Just PublicGroupProfile {groupType, groupLink} -> (Just groupType, Just groupLink)
Nothing -> (Nothing, Nothing)
@@ -2642,7 +2671,7 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName,
UPDATE group_profiles
SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?,
group_type = ?, group_link = ?,
group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?,
group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?,
preferences = ?, member_admission = ?, updated_at = ?
WHERE group_profile_id IN (
SELECT group_profile_id
@@ -2658,6 +2687,14 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName,
(ldn, currentTs, userId, groupId)
safeDeleteLDN db user localDisplayName
setGroupDomainVerified :: DB.Connection -> User -> GroupInfo -> Bool -> IO GroupInfo
setGroupDomainVerified db User {userId} g@GroupInfo {groupId} verified = do
DB.execute
db
"UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ?"
(BI verified, userId, groupId)
pure g {groupDomainVerified = Just verified}
updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo
updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do
currentTs <- getCurrentTime
@@ -2690,7 +2727,7 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName
[sql|
SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image,
gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
gp.preferences, gp.member_admission
FROM group_profiles gp
JOIN groups g ON gp.group_profile_id = g.group_profile_id
@@ -2716,24 +2753,36 @@ getGroupInfoByUserContactLinkConnReq db cxt user@User {userId} (cReqSchema1, cRe
(userId, cReqSchema1, cReqSchema2)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db cxt user) groupId_
getGroupInfoViaUserShortLink :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> IO (Maybe (ConnReqContact, GroupInfo))
getGroupInfoViaUserShortLink db cxt user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do
(cReq, groupId) <- ExceptT getConnReqGroup
(cReq,) <$> getGroupInfo db cxt user groupId
getGroupInfoViaUserTarget :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> IO (Maybe (CreatedLinkContact, GroupInfo))
getGroupInfoViaUserTarget db cxt user@User {userId} target = fmap eitherToMaybe $ runExceptT $ do
(cReq, sLnk, groupId) <- ExceptT getConnReqGroup
(CCLink cReq (Just sLnk),) <$> getGroupInfo db cxt user groupId
where
getConnReqGroup =
firstRow' toConnReqGroupId (SEInternalError "group link not found") $
DB.query
db
[sql|
SELECT conn_req_contact, group_id
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
|]
(userId, shortLink)
firstRow' toConnReqGroupId (SEInternalError "group link not found") $ case target of
CTLink shortLink ->
DB.query
db
[sql|
SELECT conn_req_contact, short_link_contact, group_id
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
|]
(userId, shortLink)
CTName ni ->
DB.query
db
[sql|
SELECT ucl.conn_req_contact, ucl.short_link_contact, ucl.group_id
FROM user_contact_links ucl
JOIN groups g ON g.group_id = ucl.group_id
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
WHERE ucl.user_id = ? AND gp.group_domain = ?
|]
(userId, nameDomain ni)
toConnReqGroupId = \case
-- cReq is "not null", group_id is nullable
(cReq, Just groupId) -> Right (cReq, groupId)
(cReq, Just (sLnk :: ShortLinkContact), Just groupId) -> Right (cReq, sLnk, groupId)
_ -> Left $ SEInternalError "no conn req or group ID"
getGroupViaShortLinkToConnect :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> ExceptT StoreError IO (Maybe (ConnReqContact, GroupInfo))
@@ -3286,7 +3335,7 @@ updateMemberProfile :: DB.Connection -> StoreCxt -> User -> GroupMember -> Profi
updateMemberProfile db cxt user@User {userId} m p' = do
currentTs <- liftIO getCurrentTime
badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) (memberProfile m) p'
let memberProfile = toLocalProfile profileId p' localAlias currentTs badgeVerified
let memberProfile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing
updateMemberProfile' currentTs badgeVerified memberProfile
where
GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
@@ -3309,7 +3358,7 @@ updateContactMemberProfile :: DB.Connection -> StoreCxt -> User -> GroupMember -
updateContactMemberProfile db cxt user@User {userId} m ct@Contact {contactId} p' = do
currentTs <- liftIO getCurrentTime
badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) (memberProfile m) p'
let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified
let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing
updateContactMemberProfile' currentTs badgeVerified profile
where
GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
+5 -5
View File
@@ -715,7 +715,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category,
m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link
FROM group_members m
@@ -1139,7 +1139,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = do
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
@@ -3070,7 +3070,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category,
m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
-- quoted ChatItem
@@ -3079,14 +3079,14 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category,
rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences,
rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx,
rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified,
rm.created_at, rm.updated_at,
rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link,
-- deleted by GroupMember
dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category,
dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences,
dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx,
dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified,
dbm.created_at, dbm.updated_at,
dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link
FROM chat_items i
@@ -38,6 +38,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services
import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at
import Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain
import Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster
import Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -75,7 +76,8 @@ schemaMigrations =
("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services),
("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at),
("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain),
("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster)
("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster),
("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,38 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260603_simplex_name :: Text
m20260603_simplex_name =
[r|
ALTER TABLE contact_profiles ADD COLUMN contact_domain TEXT;
ALTER TABLE contact_profiles ADD COLUMN contact_domain_proof TEXT;
ALTER TABLE contact_profiles ADD COLUMN contact_domain_verified SMALLINT;
ALTER TABLE group_profiles ADD COLUMN group_domain_proof TEXT;
ALTER TABLE groups ADD COLUMN group_domain_verified SMALLINT;
ALTER TABLE user_contact_links ADD COLUMN link_priv_sig_key BYTEA;
ALTER TABLE server_operators ADD COLUMN smp_role_names SMALLINT NOT NULL DEFAULT 0;
UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex' OR server_operator_tag = 'flux';
|]
down_m20260603_simplex_name :: Text
down_m20260603_simplex_name =
[r|
ALTER TABLE contact_profiles DROP COLUMN contact_domain;
ALTER TABLE contact_profiles DROP COLUMN contact_domain_proof;
ALTER TABLE contact_profiles DROP COLUMN contact_domain_verified;
ALTER TABLE group_profiles DROP COLUMN group_domain_proof;
ALTER TABLE groups DROP COLUMN group_domain_verified;
ALTER TABLE user_contact_links DROP COLUMN link_priv_sig_key;
ALTER TABLE server_operators DROP COLUMN smp_role_names;
|]
@@ -540,7 +540,10 @@ CREATE TABLE test_chat_schema.contact_profiles (
badge_extra text,
badge_master_key bytea,
badge_signature bytea,
badge_key_idx bigint
badge_key_idx bigint,
contact_domain text,
contact_domain_proof text,
contact_domain_verified smallint
);
@@ -866,7 +869,8 @@ CREATE TABLE test_chat_schema.group_profiles (
group_web_page text,
group_domain text,
domain_web_page bigint,
allow_embedding bigint
allow_embedding bigint,
group_domain_proof text
);
@@ -980,7 +984,7 @@ CREATE TABLE test_chat_schema.groups (
public_member_count bigint,
relay_request_retries bigint DEFAULT 0 NOT NULL,
relay_request_delay bigint DEFAULT 0 NOT NULL,
relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 04:00:00+04'::timestamp with time zone NOT NULL,
relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 01:00:00+01'::timestamp with time zone NOT NULL,
relay_inactive_at timestamp with time zone,
relay_sent_web_domain text,
roster_version bigint,
@@ -989,7 +993,8 @@ CREATE TABLE test_chat_schema.groups (
roster_msg_signatures bytea,
roster_sending_owner_gm_id bigint,
roster_broker_ts timestamp with time zone,
roster_blob bytea
roster_blob bytea,
group_domain_verified smallint
);
@@ -1377,7 +1382,8 @@ CREATE TABLE test_chat_schema.server_operators (
xftp_role_storage smallint DEFAULT 1 NOT NULL,
xftp_role_proxy smallint DEFAULT 1 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
updated_at timestamp with time zone DEFAULT now() NOT NULL,
smp_role_names smallint DEFAULT 0 NOT NULL
);
@@ -1454,7 +1460,8 @@ CREATE TABLE test_chat_schema.user_contact_links (
business_address smallint DEFAULT 0,
short_link_contact bytea,
short_link_data_set smallint DEFAULT 0 NOT NULL,
short_link_large_data_set smallint DEFAULT 0 NOT NULL
short_link_large_data_set smallint DEFAULT 0 NOT NULL,
link_priv_sig_key bytea
);
+46 -28
View File
@@ -50,10 +50,11 @@ module Simplex.Chat.Store.Profiles
getUserAddressConnection,
deleteUserAddress,
getUserAddress,
setUserSimplexDomain,
getUserContactLinkById,
getGroupLinkInfo,
getUserContactLinkByConnReq,
getUserContactLinkViaShortLink,
getUserContactLinkViaTarget,
setUserContactLinkShortLink,
getContactWithoutConnViaAddress,
getContactWithoutConnViaShortAddress,
@@ -105,12 +106,13 @@ import Simplex.Chat.Operators
import Simplex.Chat.Protocol
import Simplex.Chat.Store.Direct
import Simplex.Chat.Store.Shared
import Simplex.Chat.Names (claimDomain, mkDomainClaim)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..))
import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), UserId)
import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), SimplexDomain, SimplexNameInfo (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB
@@ -161,7 +163,7 @@ createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {di
(profileId, displayName, userId, BI True, currentTs, currentTs, currentTs)
contactId <- insertedRowId db
DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId)
pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing
pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing :. (Nothing, Nothing, Nothing)
-- TODO [mentions]
getUsersInfo :: DB.Connection -> IO [UserInfo]
@@ -332,7 +334,7 @@ updateUserProfile db user p'
currentTs <- getCurrentTime
updateUserProfileFields_' db userId profileId p' currentTs
userMemberProfileUpdatedAt' <- updateUserMemberProfileUpdatedAt_ currentTs
pure user {profile = (toLocalProfile profileId p' localAlias currentTs (Just False)) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'}
pure user {profile = (toLocalProfile profileId p' localAlias currentTs (Just False) Nothing) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'}
| otherwise =
checkConstraint SEDuplicateName . liftIO $ do
currentTs <- getCurrentTime
@@ -344,7 +346,7 @@ updateUserProfile db user p'
(newName, newName, userId, currentTs, currentTs)
updateUserProfileFields_' db userId profileId p' currentTs
updateContactLDN_ db user userContactId localDisplayName newName currentTs
pure user {localDisplayName = newName, profile = (toLocalProfile profileId p' localAlias currentTs (Just False)) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'}
pure user {localDisplayName = newName, profile = (toLocalProfile profileId p' localAlias currentTs (Just False) Nothing) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'}
where
updateUserMemberProfileUpdatedAt_ currentTs
| userMemberProfileChanged = do
@@ -384,6 +386,15 @@ setUserBadge db user@User {userId, profile = p@LocalProfile {profileId}} localBa
DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (ts, userId)
pure (user :: User) {profile = p {localBadge}, userMemberProfileUpdatedAt = Just ts}
setUserSimplexDomain :: DB.Connection -> User -> Maybe SimplexDomain -> IO User
setUserSimplexDomain db user@User {userId, profile = p@LocalProfile {profileId}} domain_ = do
ts <- getCurrentTime
DB.execute
db
"UPDATE contact_profiles SET contact_domain = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ?"
(domain_, ts, userId, profileId)
pure (user :: User) {profile = p {contactDomain = mkDomainClaim <$> domain_}}
setUserProfileContactLink :: DB.Connection -> User -> Maybe UserContactLink -> IO User
setUserProfileContactLink db user@User {userId, profile = p@LocalProfile {profileId}} ucl_ = do
ts <- getCurrentTime
@@ -406,24 +417,24 @@ getUserContactProfiles db User {userId} =
<$> DB.query
db
[sql|
SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, preferences
SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, contact_domain, preferences
FROM contact_profiles
WHERE user_id = ?
|]
(Only userId)
where
toContactProfile :: (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) -> Profile
toContactProfile (displayName, fullName, shortDescr, image, contactLink, peerType, preferences) = Profile {displayName, fullName, shortDescr, image, contactLink, peerType, preferences, badge = Nothing}
toContactProfile :: (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe SimplexDomain, Maybe Preferences) -> Profile
toContactProfile (displayName, fullName, shortDescr, image, contactLink, peerType, domain_, preferences) = Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain = mkDomainClaim <$> domain_, peerType, preferences, badge = Nothing}
createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> ExceptT StoreError IO ()
createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode =
createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> C.PrivateKeyEd25519 -> ExceptT StoreError IO ()
createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode linkPrivSigKey =
checkConstraint SEDuplicateContactLink . liftIO $ do
currentTs <- getCurrentTime
let slDataSet = BI (isJust shortLink)
DB.execute
db
"INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(userId, cReq, shortLink, slDataSet, slDataSet, currentTs, currentTs)
"INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, link_priv_sig_key, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(userId, cReq, shortLink, slDataSet, slDataSet, linkPrivSigKey, currentTs, currentTs)
userContactLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
@@ -546,10 +557,16 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) =
maybeFirstRow toUserContactLink $
DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND conn_req_contact IN (?,?)") (userId, cReqSchema1, cReqSchema2)
getUserContactLinkViaShortLink :: DB.Connection -> User -> ShortLinkContact -> IO (Maybe UserContactLink)
getUserContactLinkViaShortLink db User {userId} shortLink =
maybeFirstRow toUserContactLink $
DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink)
getUserContactLinkViaTarget :: DB.Connection -> User -> ContactNameOrLink -> IO (Maybe UserContactLink)
getUserContactLinkViaTarget db User {userId, profile = LocalProfile {contactDomain}} = \case
CTLink shortLink ->
maybeFirstRow toUserContactLink $
DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink)
CTName ni
| (claimDomain <$> contactDomain) == Just (nameDomain ni) ->
maybeFirstRow toUserContactLink $
DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND group_id IS NULL AND short_link_contact IS NOT NULL") (Only userId)
| otherwise -> pure Nothing
userContactLinkQuery :: Query
userContactLinkQuery =
@@ -752,10 +769,10 @@ updateServerOperator db currentTs ServerOperator {operatorId, enabled, smpRoles,
db
[sql|
UPDATE server_operators
SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ?
SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ?
WHERE server_operator_id = ?
|]
(BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId)
(BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId)
getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [(Maybe PresetOperator, Maybe ServerOperator)]
getUpdateServerOperators db presetOps newUser = do
@@ -790,20 +807,20 @@ getUpdateServerOperators db presetOps newUser = do
db
[sql|
UPDATE server_operators
SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?
SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?
WHERE server_operator_id = ?
|]
(tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId)
(tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId)
insertOperator :: NewServerOperator -> IO ServerOperator
insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = do
DB.execute
db
[sql|
INSERT INTO server_operators
(server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy)
VALUES (?,?,?,?,?,?,?,?,?)
(server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy)
VALUES (?,?,?,?,?,?,?,?,?,?)
|]
(operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles))
(operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles))
opId <- insertedRowId db
pure op {operatorId = DBEntityId opId}
autoAcceptConditions op UsageConditions {conditionsCommit} now =
@@ -814,14 +831,14 @@ serverOperatorQuery :: Query
serverOperatorQuery =
[sql|
SELECT server_operator_id, server_operator_tag, trade_name, legal_name,
server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy
server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy
FROM server_operators
|]
getServerOperators_ :: DB.Connection -> IO [ServerOperator]
getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery
toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator
toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator
toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI enabled) :. smpRoles' :. xftpRoles') =
ServerOperator
{ operatorId,
@@ -831,11 +848,12 @@ toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI en
serverDomains = T.splitOn "," domains,
conditionsAcceptance = CARequired Nothing,
enabled,
smpRoles = serverRoles smpRoles',
xftpRoles = serverRoles xftpRoles'
smpRoles = serverRolesSMP smpRoles',
xftpRoles = serverRolesXFTP xftpRoles'
}
where
serverRoles (BI storage, BI proxy) = ServerRoles {storage, proxy}
serverRolesSMP (BI storage, BI proxy, BI names) = ServerRoles {storage, proxy, names}
serverRolesXFTP (BI storage, BI proxy) = ServerRoles {storage, proxy, names = False}
getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance
getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do
+3 -1
View File
@@ -161,6 +161,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services
import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at
import Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain
import Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster
import Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -321,7 +322,8 @@ schemaMigrations =
("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services),
("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at),
("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain),
("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster)
("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster),
("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,37 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260603_simplex_name :: Query
m20260603_simplex_name =
[sql|
ALTER TABLE contact_profiles ADD COLUMN contact_domain TEXT;
ALTER TABLE contact_profiles ADD COLUMN contact_domain_proof TEXT;
ALTER TABLE contact_profiles ADD COLUMN contact_domain_verified INTEGER;
ALTER TABLE group_profiles ADD COLUMN group_domain_proof TEXT;
ALTER TABLE groups ADD COLUMN group_domain_verified INTEGER;
ALTER TABLE user_contact_links ADD COLUMN link_priv_sig_key BLOB;
ALTER TABLE server_operators ADD COLUMN smp_role_names INTEGER NOT NULL DEFAULT 0;
UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex' OR server_operator_tag = 'flux';
|]
down_m20260603_simplex_name :: Query
down_m20260603_simplex_name =
[sql|
ALTER TABLE contact_profiles DROP COLUMN contact_domain;
ALTER TABLE contact_profiles DROP COLUMN contact_domain_proof;
ALTER TABLE contact_profiles DROP COLUMN contact_domain_verified;
ALTER TABLE group_profiles DROP COLUMN group_domain_proof;
ALTER TABLE groups DROP COLUMN group_domain_verified;
ALTER TABLE user_contact_links DROP COLUMN link_priv_sig_key;
ALTER TABLE server_operators DROP COLUMN smp_role_names;
|]
@@ -125,7 +125,7 @@ Query:
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
@@ -144,26 +144,26 @@ Query:
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.use_relays, g.relay_own_status,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified,
g.root_priv_key, g.root_pub_key, g.member_priv_key,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
-- GroupInfo {membership = GroupMember {memberProfile}}
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link,
-- from GroupMember
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link
FROM group_members m
@@ -401,7 +401,7 @@ Query:
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p USING (contact_profile_id)
WHERE cr.user_id = ?
@@ -443,6 +443,14 @@ Query:
Plan:
SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_server_operator_id (server_operator_id=?)
Query:
SELECT conn_req_contact, short_link_contact, group_id
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query:
SELECT timed_ttl
FROM chat_items
@@ -451,6 +459,18 @@ Query:
Plan:
SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT ucl.conn_req_contact, ucl.short_link_contact, ucl.group_id
FROM user_contact_links ucl
JOIN groups g ON g.group_id = ucl.group_id
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
WHERE ucl.user_id = ? AND gp.group_domain = ?
Plan:
SEARCH ucl USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
SEARCH g USING INTEGER PRIMARY KEY (rowid=?)
SEARCH gp USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE chat_items
SET user_id = ?, updated_at = ?
@@ -687,7 +707,8 @@ Query:
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id,
c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection,
c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.contact_status = ? AND c.deleted = 0
@@ -907,14 +928,6 @@ Plan:
SEARCH chat_items USING INDEX idx_chat_items_groups_item_viewed (user_id=?)
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT conn_req_contact, group_id
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query:
SELECT conn_req_contact, group_id
FROM user_contact_links
@@ -994,7 +1007,7 @@ SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_next (group_id=? A
Query:
SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image,
gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
gp.preferences, gp.member_admission
FROM group_profiles gp
JOIN groups g ON gp.group_profile_id = g.group_profile_id
@@ -1038,7 +1051,7 @@ Query:
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category,
m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link
FROM group_members m
@@ -1269,9 +1282,9 @@ Query:
INSERT INTO group_profiles
(display_name, full_name, short_descr, description, image,
group_type, group_link, public_group_id,
group_web_page, group_domain, domain_web_page, allow_embedding,
group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof,
user_id, preferences, member_admission, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
@@ -1303,8 +1316,8 @@ Plan:
Query:
INSERT INTO server_operators
(server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy)
VALUES (?,?,?,?,?,?,?,?,?)
(server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy)
VALUES (?,?,?,?,?,?,?,?,?,?)
Plan:
@@ -1347,7 +1360,7 @@ Query:
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category,
m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id,
p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
-- quoted ChatItem
@@ -1356,14 +1369,14 @@ Query:
rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category,
rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id,
rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences,
rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx,
rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified,
rm.created_at, rm.updated_at,
rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link,
-- deleted by GroupMember
dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category,
dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences,
dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx,
dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified,
dbm.created_at, dbm.updated_at,
dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link
FROM chat_items i
@@ -1417,6 +1430,7 @@ Query:
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
@@ -1810,7 +1824,7 @@ Query:
UPDATE group_profiles
SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?,
group_type = ?, group_link = ?,
group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?,
group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?,
preferences = ?, member_admission = ?, updated_at = ?
WHERE group_profile_id IN (
SELECT group_profile_id
@@ -1997,6 +2011,7 @@ Query:
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection,
ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx,
cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
@@ -2082,7 +2097,7 @@ Query:
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
@@ -2112,7 +2127,7 @@ Query:
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
@@ -2142,7 +2157,7 @@ Query:
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id
JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id
@@ -3672,8 +3687,8 @@ Plan:
SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx
SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified
FROM contact_profiles cp
WHERE cp.user_id = ? AND cp.contact_profile_id = ?
@@ -3693,6 +3708,15 @@ Plan:
SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?)
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT ct.contact_id, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect FROM contacts ct
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
WHERE ct.user_id = ? AND cp.contact_domain = ? AND cp.contact_domain_verified = 1 AND ct.deleted = 0
Plan:
SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?)
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT d.file_descr_id, d.file_descr_text, d.file_descr_part_no, d.file_descr_complete
FROM xftp_file_descriptions d
@@ -3716,7 +3740,7 @@ SEARCH f USING PRIMARY KEY (file_id=?)
SEARCH d USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, preferences
SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, contact_domain, preferences
FROM contact_profiles
WHERE user_id = ?
@@ -3768,9 +3792,18 @@ Query:
Plan:
SEARCH files USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT g.group_id, g.conn_full_link_to_connect, g.conn_short_link_to_connect FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
WHERE g.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified = 1
Plan:
SEARCH g USING INDEX sqlite_autoindex_groups_2 (user_id=?)
SEARCH gp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT g.group_id, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof
FROM groups g
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
JOIN group_members mu ON mu.group_id = g.group_id AND mu.contact_id = ?
@@ -5110,7 +5143,8 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?,
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?,
contact_domain = ?, contact_domain_proof = ?
WHERE user_id = ? AND contact_profile_id = ?
Plan:
@@ -5119,7 +5153,8 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?,
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?,
contact_domain = ?, contact_domain_proof = ?
WHERE user_id = ? AND contact_profile_id = ?
Plan:
@@ -5128,7 +5163,8 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?,
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?
badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?,
contact_domain = ?, contact_domain_proof = ?
WHERE user_id = ? AND contact_profile_id = ?
Plan:
@@ -5142,6 +5178,15 @@ Query:
Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE contact_profiles SET contact_domain_verified = ?
WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?)
Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
LIST SUBQUERY 1
SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE contacts
SET contact_group_member_id = NULL, contact_grp_inv_sent = 0, updated_at = ?
@@ -5385,7 +5430,7 @@ SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE server_operators
SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ?
SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ?
WHERE server_operator_id = ?
Plan:
@@ -5468,19 +5513,19 @@ Query:
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.use_relays, g.relay_own_status,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified,
g.root_priv_key, g.root_pub_key, g.member_priv_key,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link
@@ -5506,19 +5551,19 @@ Query:
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.use_relays, g.relay_own_status,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified,
g.root_priv_key, g.root_pub_key, g.member_priv_key,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link
@@ -5537,19 +5582,19 @@ Query:
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.use_relays, g.relay_own_status,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified,
g.root_priv_key, g.root_pub_key, g.member_priv_key,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link
@@ -5572,7 +5617,8 @@ Query:
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p USING (contact_profile_id)
WHERE cr.business_group_id = ?
@@ -5588,7 +5634,8 @@ Query:
cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences,
cr.created_at, cr.updated_at,
cr.peer_chat_min_version, cr.peer_chat_max_version,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.contact_domain, p.contact_domain_proof, p.contact_domain_verified
FROM contact_requests cr
JOIN contact_profiles p USING (contact_profile_id)
WHERE cr.user_id = ? AND cr.contact_request_id = ?
@@ -5600,7 +5647,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5628,7 +5675,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5649,7 +5696,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5669,7 +5716,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5689,7 +5736,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5709,7 +5756,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5729,7 +5776,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5749,7 +5796,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5769,7 +5816,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5789,7 +5836,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5809,7 +5856,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5829,7 +5876,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5849,7 +5896,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5869,7 +5916,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -5889,7 +5936,7 @@ Query:
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -6073,7 +6120,7 @@ SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT server_operator_id, server_operator_tag, trade_name, legal_name,
server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy
server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy
FROM server_operators
Plan:
@@ -6081,7 +6128,7 @@ SCAN server_operators
Query:
SELECT server_operator_id, server_operator_tag, trade_name, legal_name,
server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy
server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy
FROM server_operators
WHERE server_operator_id = ?
Plan:
@@ -6090,7 +6137,7 @@ SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6103,7 +6150,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6117,7 +6164,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6131,7 +6178,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6146,7 +6193,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6160,7 +6207,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6174,7 +6221,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6188,7 +6235,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6202,7 +6249,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6215,7 +6262,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
@@ -6848,7 +6895,7 @@ Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image
Plan:
SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?)
Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Query: INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?)
@@ -6938,7 +6985,7 @@ Query: INSERT INTO snd_files (file_id, file_status, file_descr_id, group_member_
Plan:
SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, created_at, updated_at) VALUES (?,?,?,?,?,?,?)
Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, link_priv_sig_key, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)
Plan:
Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
@@ -7426,6 +7473,10 @@ Query: UPDATE connections_sync SET should_sync = 1 WHERE connections_sync_id = 1
Plan:
SEARCH connections_sync USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE contact_profiles SET contact_domain = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ?
Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE contact_profiles SET image = ? WHERE display_name = ?
Plan:
SEARCH contact_profiles USING INDEX contact_profiles_index (display_name=?)
@@ -7630,6 +7681,14 @@ Query: UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE use
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE groups SET group_domain_verified = NULL WHERE user_id = ? AND group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE groups SET local_alias = ?, updated_at = ? WHERE user_id = ? AND group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
@@ -28,7 +28,10 @@ CREATE TABLE contact_profiles(
badge_extra TEXT,
badge_master_key BLOB,
badge_signature BLOB,
badge_key_idx INTEGER
badge_key_idx INTEGER,
contact_domain TEXT,
contact_domain_proof TEXT,
contact_domain_verified INTEGER
) STRICT;
CREATE TABLE users(
user_id INTEGER PRIMARY KEY,
@@ -139,7 +142,8 @@ CREATE TABLE group_profiles(
group_web_page TEXT,
group_domain TEXT,
domain_web_page INTEGER,
allow_embedding INTEGER
allow_embedding INTEGER,
group_domain_proof TEXT
) STRICT;
CREATE TABLE groups(
group_id INTEGER PRIMARY KEY, -- local group ID
@@ -199,7 +203,8 @@ CREATE TABLE groups(
roster_msg_signatures BLOB,
roster_sending_owner_gm_id INTEGER,
roster_broker_ts TEXT,
roster_blob BLOB, -- received
roster_blob BLOB,
group_domain_verified INTEGER, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -393,6 +398,7 @@ CREATE TABLE user_contact_links(
short_link_contact BLOB,
short_link_data_set INTEGER NOT NULL DEFAULT 0,
short_link_large_data_set INTEGER NOT NULL DEFAULT 0,
link_priv_sig_key BLOB,
UNIQUE(user_id, local_display_name)
) STRICT;
CREATE TABLE contact_requests(
@@ -700,6 +706,8 @@ CREATE TABLE server_operators(
xftp_role_proxy INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
smp_role_names INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE usage_conditions(
usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
+46 -33
View File
@@ -33,13 +33,14 @@ import qualified Data.Text as T
import Data.Time.Clock (UTCTime (..), getCurrentTime)
import Data.Type.Equality
import Simplex.Chat.Badges (BadgeRow, badgeToRow, rowToBadge, verifyBadge_)
import Simplex.Chat.Names (SimplexDomainProof, SimplexDomainClaim (..), claimDomain)
import Simplex.Chat.Messages
import Simplex.Chat.Remote.Types
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), UserId, connMode)
import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), SimplexDomain, UserId, connMode)
import Simplex.Messaging.Agent.Store (AnyStoreError (..))
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.Common (withSavepoint)
@@ -48,6 +49,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..))
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding.String (StrJSON (..))
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util (AnyError (..))
@@ -413,13 +415,13 @@ createContact db cxt user profile = do
void $ createContact_ db cxt user profile emptyChatPrefs Nothing "" currentTs
createContact_ :: DB.Connection -> StoreCxt -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> ExceptT StoreError IO ContactId
createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, peerType, badge, preferences} ctUserPreferences prepared localAlias currentTs =
createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, contactDomain, peerType, badge, preferences} ctUserPreferences prepared localAlias currentTs =
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
badgeVerified <- verifyBadge_ (badgeKeys cxt) badge
DB.execute
db
"INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
((displayName, fullName, shortDescr, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified)
"INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
((displayName, fullName, shortDescr, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain)
profileId <- insertedRowId db
DB.execute
db
@@ -486,13 +488,15 @@ type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Ma
type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt)
type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow
type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow
type ContactRow = Only ContactId :. ContactRow'
type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe BoolInt)
toContact :: UTCTime -> StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact
toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias}
toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias}
activeConn = toMaybeConnection cxt connRow
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
incognito = maybe False connIncognito activeConn
@@ -501,6 +505,15 @@ toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName
groupDirectInv = toGroupDirectInvitation groupDirectInvRow
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
rowToContactDomain :: ContactDomainRow -> Maybe SimplexDomainClaim
rowToContactDomain (domain_, domainProof_, _) = (`SimplexDomainClaim` domainProof_) . StrJSON <$> domain_
rowToDomainVerified :: ContactDomainRow -> Maybe Bool
rowToDomainVerified (_, _, domainVerification_) = unBI <$> domainVerification_
contactDomainToRow :: Maybe SimplexDomainClaim -> (Maybe SimplexDomain, Maybe SimplexDomainProof)
contactDomainToRow d = (claimDomain <$> d, proof =<< d)
toPreparedContact :: PreparedContactRow -> Maybe PreparedContact
toPreparedContact (connFullLink, connShortLink, welcomeSharedMsgId, requestSharedMsgId) =
(\cl@(ACCL m _) -> PreparedContact {connLinkToConnect = cl, uiConnLinkType = connMode m, welcomeSharedMsgId, requestSharedMsgId})
@@ -524,18 +537,18 @@ getProfileById db userId profileId = do
DB.query
db
[sql|
SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences,
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx
SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences
cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified
FROM contact_profiles cp
WHERE cp.user_id = ? AND cp.contact_profile_id = ?
|]
(userId, profileId)
type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow
type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow
toContactRequest :: UTCTime -> ContactRequestRow -> UserContactRequest
toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow) = do
let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias}
toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do
let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias}
cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer
in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt}
@@ -544,17 +557,17 @@ userQuery =
[sql|
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.is_user_chat_relay, u.client_service, u.ui_themes,
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx
ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
|]
toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow -> User
toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow) =
toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow :. ContactDomainRow -> User
toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow :. domainRow) =
User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, userChatRelay = BoolDef userChatRelay, clientService = BoolDef clientService, uiThemes}
where
profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""}
profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""}
fullPreferences = fullPreferences' userPreferences
viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_
@@ -670,16 +683,16 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member
type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519)
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow
type PublicGroupAccessRow = (Maybe Text, Maybe Text, Maybe BoolInt, Maybe BoolInt)
type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof)
type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact)
type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow
type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow
toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) =
toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) =
let membership = (toGroupMember now userContactId userMemberRow) {memberChatVRange = vr cxt}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
@@ -689,7 +702,7 @@ toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayN
businessChat = toBusinessChatInfo businessRow
preparedGroup = toPreparedGroup preparedGroupRow
groupSummary = GroupSummary {currentMembers, publicMemberCount}
in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupKeys}
in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupKeys, groupDomainVerified = unBI <$> groupDomainVerified}
toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup
toPreparedGroup = \case
@@ -704,14 +717,14 @@ toPublicGroupProfile _ _ _ _ = Nothing
publicGroupAccessRow :: Maybe PublicGroupProfile -> PublicGroupAccessRow
publicGroupAccessRow pgp = case pgp >>= publicGroupAccess of
Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding} ->
(groupWebPage, groupDomain, Just (BI domainWebPage), Just (BI allowEmbedding))
Nothing -> (Nothing, Nothing, Nothing, Nothing)
Just PublicGroupAccess {groupWebPage, groupDomainClaim, domainWebPage, allowEmbedding} ->
(groupWebPage, claimDomain <$> groupDomainClaim, Just (BI domainWebPage), Just (BI allowEmbedding), proof =<< groupDomainClaim)
Nothing -> (Nothing, Nothing, Nothing, Nothing, Nothing)
toPublicGroupAccess :: PublicGroupAccessRow -> Maybe PublicGroupAccess
toPublicGroupAccess (groupWebPage, groupDomain, domainWebPage_, allowEmbedding_)
| isJust groupWebPage || isJust groupDomain || domainWebPage || allowEmbedding =
Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding}
toPublicGroupAccess (groupWebPage, groupDomain_, domainWebPage_, allowEmbedding_, groupDomainProof_)
| isJust groupWebPage || isJust groupDomain_ || domainWebPage || allowEmbedding =
Just PublicGroupAccess {groupWebPage, groupDomainClaim = (`SimplexDomainClaim` groupDomainProof_) . StrJSON <$> groupDomain_, domainWebPage, allowEmbedding}
| otherwise = Nothing
where
domainWebPage = maybe False unBI domainWebPage_
@@ -750,7 +763,7 @@ groupMemberQuery =
SELECT
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx,
p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified,
m.created_at, m.updated_at,
m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id,
@@ -767,8 +780,8 @@ toContactMember now cxt User {userContactId} (memberRow :. connRow) =
(toGroupMember now userContactId memberRow) {activeConn = toMaybeConnection cxt connRow}
rowToLocalProfile :: UTCTime -> ProfileRow -> LocalProfile
rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, preferences) :. badgeRow) =
LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences}
rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, preferences) :. badgeRow :. domainRow) =
LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences}
toBusinessChatInfo :: BusinessChatInfoRow -> Maybe BusinessChatInfo
toBusinessChatInfo (Just chatType, Just businessId, Just customerId) = Just BusinessChatInfo {chatType, businessId, customerId}
@@ -783,19 +796,19 @@ groupInfoQueryFields =
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding,
gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof,
g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at,
g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id,
g.business_chat, g.business_member_id, g.customer_member_id,
g.use_relays, g.relay_own_status,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri,
g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified,
g.root_priv_key, g.root_pub_key, g.member_priv_key,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link
|]
+70 -26
View File
@@ -4,6 +4,7 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
@@ -19,6 +20,7 @@
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilyDependencies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
@@ -34,6 +36,7 @@ import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
import qualified Data.Aeson.TH as JQ
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Attoparsec.Combinator (lookAhead)
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString, pack, unpack)
import qualified Data.ByteString.Char8 as B
@@ -46,16 +49,18 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Time.Clock (UTCTime)
import Data.Type.Equality (testEquality, (:~:) (Refl))
import Data.Typeable (Typeable)
import Data.Word (Word16)
import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), localBadgeInfo, localBadgeStatus, mkBadgeStatus, verifyBadge)
import Simplex.Chat.Names (SimplexDomainClaim (..))
import Simplex.Messaging.Crypto.BBS (BBSPublicKey)
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.FileTransfer.Description (FileDigest)
import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink, ConnectionMode (..), ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), UserId)
import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AConnectionLink (..), AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink (..), ConnectionMode (..), ConnectionModeI, ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), SConnectionMode (..), SimplexNameInfo, UserId)
import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
@@ -493,7 +498,8 @@ data GroupInfo = GroupInfo
rosterVersion :: Maybe VersionRoster,
membersRequireAttention :: Int,
viaGroupLinkUri :: Maybe ConnReqContact,
groupKeys :: Maybe GroupKeys
groupKeys :: Maybe GroupKeys,
groupDomainVerified :: Maybe Bool
}
deriving (Eq, Show)
@@ -641,12 +647,6 @@ groupFeatureUserAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupInfo -
groupFeatureUserAllowed feature GroupInfo {membership = GroupMember {memberRole}, fullGroupPreferences} =
groupFeatureMemberAllowed' feature memberRole fullGroupPreferences
-- A connection link in a profile description enables a direct connection, so a description
-- keeps its links only when both SimpleX links and direct messages are allowed.
groupUserAllowSimplexLinks :: GroupInfo -> Bool
groupUserAllowSimplexLinks g =
groupFeatureUserAllowed SGFSimplexLinks g && groupFeatureUserAllowed SGFDirectMessages g
mergeUserChatPrefs :: User -> Contact -> FullPreferences
mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct)
@@ -698,7 +698,8 @@ data Profile = Profile
contactLink :: Maybe ConnLinkContact,
preferences :: Maybe Preferences,
peerType :: Maybe ChatPeerType,
badge :: Maybe BadgeProof
badge :: Maybe BadgeProof,
contactDomain :: Maybe SimplexDomainClaim
-- fields that should not be read into this data type to prevent sending them as part of profile to contacts:
-- - contact_profile_id
-- - incognito
@@ -731,21 +732,22 @@ instance TextEncoding ChatPeerType where
profileFromName :: ContactName -> Profile
profileFromName displayName =
Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, preferences = Nothing, peerType = Nothing, badge = Nothing}
Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, preferences = Nothing, peerType = Nothing, badge = Nothing, contactDomain = Nothing}
-- check if profiles match ignoring preferences
profilesMatch :: LocalProfile -> LocalProfile -> Bool
profilesMatch
LocalProfile {displayName = n1, fullName = fn1, image = i1}
LocalProfile {displayName = n2, fullName = fn2, image = i2} =
n1 == n2 && fn1 == fn2 && i1 == i2
LocalProfile {displayName = n1, fullName = fn1, image = i1, shortDescr = d1}
LocalProfile {displayName = n2, fullName = fn2, image = i2, shortDescr = d2} =
n1 == n2 && fn1 == fn2 && i1 == i2 && d1 == d2
-- equal for profile-update detection: badge proofs are re-generated for every presentation,
-- so compare badges by disclosed info (not proof bytes) - a re-presentation of the same badge is a no-op
sameProfileContent :: Profile -> Profile -> Bool
sameProfileContent p@Profile {badge = b} p'@Profile {badge = b'} =
p {badge = Nothing} == p' {badge = Nothing} && (proofInfo <$> b) == (proofInfo <$> b')
clearProofs p == clearProofs p' && (proofInfo <$> b) == (proofInfo <$> b')
where
clearProofs pr@Profile {contactDomain} = pr {badge = Nothing, contactDomain = (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain}
proofInfo :: BadgeProof -> BadgeInfo
proofInfo (BadgeProof _ _ _ info) = info
@@ -781,29 +783,31 @@ data LocalProfile = LocalProfile
preferences :: Maybe Preferences,
peerType :: Maybe ChatPeerType,
localBadge :: Maybe LocalBadge,
localAlias :: LocalAlias
localAlias :: LocalAlias,
contactDomain :: Maybe SimplexDomainClaim,
contactDomainVerified :: Maybe Bool
}
deriving (Eq, Show)
localProfileId :: LocalProfile -> ProfileId
localProfileId LocalProfile {profileId} = profileId
toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> LocalProfile
toLocalProfile profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge} localAlias now verified =
LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, localAlias}
toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> Maybe Bool -> LocalProfile
toLocalProfile profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge, contactDomain} localAlias now badgeVerified contactDomainVerified =
LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, localAlias, contactDomain, contactDomainVerified}
where
localBadge = (\b@(BadgeProof _ _ _ info) -> PeerBadge b (mkBadgeStatus now verified info)) <$> badge
localBadge = (\b@(BadgeProof _ _ _ info) -> PeerBadge b (mkBadgeStatus now badgeVerified info)) <$> badge
fromLocalProfile :: LocalProfile -> Profile
fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge} =
Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge}
fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, contactDomain} =
-- the name proof is re-signed on each send
Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge, contactDomain = (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain}
where
-- any stored peer proof rides the wire (receivers verify independently); the own credential is presented fresh, and a display-only badge never sends
wireBadge :: LocalBadge -> Maybe BadgeProof
wireBadge = \case
PeerBadge b _ -> Just b
OwnBadge _ _ -> Nothing
ShownBadge _ _ -> Nothing
PeerBadge b _ -> Just b -- stored peer proof sent as is
OwnBadge _ _ -> Nothing -- the own credential is not sent, proof is generated on send
ShownBadge _ _ -> Nothing -- a display-only badge is not sent
profileBadgeVerified :: Map Int BBSPublicKey -> LocalProfile -> Profile -> IO (Maybe Bool)
profileBadgeVerified keys LocalProfile {localBadge} Profile {badge = newBadge} =
@@ -842,7 +846,7 @@ instance ToField GroupType where toField = toField . textEncode
data PublicGroupAccess = PublicGroupAccess
{ groupWebPage :: Maybe Text,
groupDomain :: Maybe Text,
groupDomainClaim :: Maybe SimplexDomainClaim,
domainWebPage :: Bool,
allowEmbedding :: Bool
}
@@ -1782,6 +1786,46 @@ type ConnReqInvitation = ConnectionRequestUri 'CMInvitation
type ConnReqContact = ConnectionRequestUri 'CMContact
data ConnectTarget (m :: ConnectionMode) where
CTFullContact :: ConnectionRequestUri 'CMContact -> ConnectTarget 'CMContact
CTShortContact :: ContactNameOrLink -> ConnectTarget 'CMContact
CTInv :: ConnectionLink 'CMInvitation -> ConnectTarget 'CMInvitation
data ContactNameOrLink = CTName SimplexNameInfo | CTLink (ConnShortLink 'CMContact)
deriving (Eq, Show)
deriving instance Eq (ConnectTarget m)
deriving instance Show (ConnectTarget m)
data AConnectTarget = forall m. ConnectionModeI m => ACTarget (SConnectionMode m) (ConnectTarget m)
deriving (ToJSON, FromJSON) via (StrJSON "AConnectTarget" AConnectTarget)
instance Eq AConnectTarget where
ACTarget m t == ACTarget m' t' = case testEquality m m' of
Just Refl -> t == t'
_ -> False
deriving instance Show AConnectTarget
instance StrEncoding AConnectTarget where
strEncode (ACTarget _ t) = case t of
CTFullContact cr -> strEncode cr
CTShortContact (CTName n) -> strEncode n
CTShortContact (CTLink sl) -> strEncode sl
CTInv l -> strEncode l
strP =
(ACTarget SCMContact . CTShortContact . CTName <$> (lookAhead nameStart *> strP))
<|> (aConnectTarget <$> strP)
where
nameStart = "@" <|> "#" <|> "simplex:/name"
aConnectTarget :: AConnectionLink -> AConnectTarget
aConnectTarget (ACL SCMInvitation cl) = ACTarget SCMInvitation (CTInv cl)
aConnectTarget (ACL SCMContact cl) = ACTarget SCMContact $ case cl of
CLFull cr -> CTFullContact cr
CLShort sl -> CTShortContact (CTLink sl)
type CreatedLinkInvitation = CreatedConnLink 'CMInvitation
type CreatedLinkContact = CreatedConnLink 'CMContact
+70 -26
View File
@@ -52,6 +52,7 @@ import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange)
import Simplex.Chat.Remote.Types
import Simplex.Chat.Store (AddressSettings (..), AutoAccept (..), StoreError (..), UserContactLink (..))
import Simplex.Chat.Styled
import Simplex.Chat.Names (SimplexDomainClaim (..), claimDomain)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
@@ -147,6 +148,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRContactRatchetSyncStarted {} -> ["connection synchronization started"]
CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"]
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
CRContactDomainVerified u (Contact {profile = LocalProfile {contactDomain}}) result -> ttyUser u $ viewDomainVerified NTContact (claimDomain <$> contactDomain) result
CRGroupDomainVerified u g result -> ttyUser u $ viewDomainVerified NTPublicGroup (groupSimplexDomain g) result
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView
@@ -1125,6 +1128,31 @@ simplexChatContact' = \case
CLFull (CRContactUri crData) -> CLFull $ CRContactUri crData {crScheme = simplexChat}
l@(CLShort _) -> l
groupSimplexDomain :: GroupInfo -> Maybe SimplexDomain
groupSimplexDomain GroupInfo {groupProfile = GroupProfile {publicGroup}} =
claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)
viewDomainVerified :: SimplexNameType -> Maybe SimplexDomain -> Maybe Text -> [StyledString]
viewDomainVerified nameType domain_ result =
let nameStr = maybe "name" (\d -> "SimpleX name " <> shortNameInfoStr (SimplexNameInfo nameType d)) domain_
in case result of
Nothing -> [plain nameStr <> " verified"]
Just reason -> [plain nameStr <> " not verified: " <> plain reason]
-- §4.7: show a peer's claimed name only with its verification context — "verified" / "verification
-- failed" when a status is recorded, "unverified" when there is a proof but no status yet, and nothing
-- at all when there is neither (an unproven, unverifiable claim is not shown).
simplexDomainLine :: SimplexNameType -> Maybe SimplexDomainClaim -> Maybe Bool -> [StyledString]
simplexDomainLine _ Nothing _ = []
simplexDomainLine nameType (Just SimplexDomainClaim {domain, proof}) status = case status of
Just True -> [line "verified"]
Just False -> [line "verification failed"]
Nothing
| isJust proof -> [line "unverified"]
| otherwise -> []
where
line s = plain $ "SimpleX name: " <> shortNameInfoStr (SimplexNameInfo nameType (unStrJSON domain)) <> " (" <> s <> ")"
-- TODO [short links] show all settings
viewAddressSettings :: AddressSettings -> [StyledString]
viewAddressSettings AddressSettings {businessAddress, autoAccept, autoReply} = case autoAccept of
@@ -1142,12 +1170,14 @@ groupLink_ :: StyledString -> GroupInfo -> GroupLink -> [StyledString]
groupLink_ intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMemberRole} =
[ intro,
"",
plain $ maybe cReqStr strEncode shortLink,
"",
"Anybody can connect to you and join group as " <> showRole acceptMemberRole <> " with: " <> highlight' "/c <group_link_above>",
"to show it again: " <> highlight ("/show link #" <> viewGroupName g),
"to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)"
plain $ maybe cReqStr strEncode shortLink
]
<> [plain ("SimpleX name: " <> shortNameInfoStr (SimplexNameInfo NTPublicGroup d)) | Just d <- [groupSimplexDomain g]]
<> [ "",
"Anybody can connect to you and join group as " <> showRole acceptMemberRole <> " with: " <> highlight' "/c <group_link_above>",
"to show it again: " <> highlight ("/show link #" <> viewGroupName g),
"to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)"
]
<> ["The group link for old clients: " <> plain cReqStr | isJust shortLink]
where
cReqStr = strEncode $ simplexChatContact cReq
@@ -1223,6 +1253,7 @@ viewGroupLinkRelaysUpdated g groupLink relays =
[ "group link:",
plain $ maybe cReqStr strEncode shortLink
]
<> [plain ("SimpleX name: " <> shortNameInfoStr (SimplexNameInfo NTPublicGroup d)) | Just d <- [groupSimplexDomain g]]
where
GroupLink {connLinkContact = CCLink cReq shortLink} = groupLink
cReqStr = strEncode $ simplexChatContact cReq
@@ -1778,11 +1809,12 @@ viewContactBadge = maybe [] $ \lb ->
in [plain (textEncode badgeType <> " badge - " <> st), plain expiry]
viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge}, activeConn, uiThemes, customData} stats incognitoProfile =
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge, contactDomain, contactDomainVerified}, activeConn, uiThemes, customData} stats incognitoProfile =
["contact ID: " <> sShow contactId]
<> viewContactBadge localBadge
<> maybe [] viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink
<> maybe [] (\l -> ["contact address: " <> plain (strEncode (simplexChatContact' l))]) contactLink
<> simplexDomainLine NTContact contactDomain contactDomainVerified
<> maybe
["you've shared main profile with this contact"]
(\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p])
@@ -2017,9 +2049,9 @@ viewGroupUpdated
access = pg >>= publicGroupAccess
access' = pg' >>= publicGroupAccess
viewAccess Nothing = " removed"
viewAccess (Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding}) =
viewAccess (Just PublicGroupAccess {groupWebPage, groupDomainClaim, domainWebPage, allowEmbedding}) =
maybe "" (\u -> " web=" <> plain u) groupWebPage
<> maybe "" (\d -> " domain=" <> plain d) groupDomain
<> maybe "" (\ni -> " domain=" <> plain (strEncode ni)) (claimDomain <$> groupDomainClaim)
<> (if domainWebPage then " domain_page=on" else "")
<> (if allowEmbedding then " embed=on" else "")
@@ -2120,12 +2152,12 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
ILPConnecting Nothing -> [invLink "connecting"]
ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)]
ILPKnown ct
| nextConnectPrepared ct -> [invLink ("known prepared contact " <> ttyContact' ct)]
| contactDeleted ct -> [invLink ("known deleted contact " <> ttyContact' ct)]
| nextConnectPrepared ct -> [invLink ("known prepared contact " <> ttyContact' ct)] <> contactDomainLine ct
| contactDeleted ct -> [invLink ("known deleted contact " <> ttyContact' ct)] <> contactDomainLine ct
| otherwise ->
[ invLink ("known contact " <> ttyContact' ct),
"use " <> ttyToContact' ct <> highlight' "<message>" <> " to send messages"
]
[invLink ("known contact " <> ttyContact' ct)]
<> contactDomainLine ct
<> ["use " <> ttyToContact' ct <> highlight' "<message>" <> " to send messages"]
where
invLink = ("invitation link: " <>)
invOrBiz = \case
@@ -2133,17 +2165,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
| business -> ("business address: " <>)
_ -> ("invitation link: " <>)
CPContactAddress cap -> case cap of
CAPOk contactSLinkData ov -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView]
CAPOk contactSLinkData ov _ -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView]
CAPOwnLink -> [ctAddr "own address"]
CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"]
CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)]
CAPKnown ct
| nextConnectPrepared ct -> [ctAddr ("known prepared contact " <> ttyContact' ct)]
| nextConnectPrepared ct -> [ctAddr ("known prepared contact " <> ttyContact' ct)] <> contactDomainLine ct
| otherwise ->
[ ctAddr ("known contact " <> ttyContact' ct),
"use " <> ttyToContact' ct <> highlight' "<message>" <> " to send messages"
]
CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)]
[ctAddr ("known contact " <> ttyContact' ct)]
<> contactDomainLine ct
<> ["use " <> ttyToContact' ct <> highlight' "<message>" <> " to send messages"]
CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)] <> contactDomainLine ct
where
ctAddr = ("contact address: " <>)
addrOrBiz = \case
@@ -2151,7 +2183,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
| business -> ("business address: " <>)
_ -> ("contact address: " <>)
CPGroupLink glp -> case glp of
GLPOk groupSLinkInfo_ groupSLinkData ov ->
GLPOk groupSLinkInfo_ groupSLinkData ov _ ->
let direct = maybe True (\(GroupShortLinkInfo {direct = d}) -> d) groupSLinkInfo_
in [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"]
<> viewSigVerification ov
@@ -2164,17 +2196,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
Just PreparedGroup {connLinkStartedConnection} -> case memberStatus m of
GSMemUnknown
| connLinkStartedConnection -> connecting g
| otherwise -> [knownGroup "prepared "]
| otherwise -> [knownGroup "prepared "] <> groupDomainLine g
GSMemAccepted -> connecting g
_
| memberRemoved m -> [knownGroup "deleted "] -- it should not get here, as this plan is returned as GLPOk
| memberRemoved m -> [knownGroup "deleted "] <> groupDomainLine g -- it should not get here, as this plan is returned as GLPOk
| otherwise -> knownActive
_ -> knownActive
where
knownActive =
[ knownGroup "",
"use " <> ttyToGroup g Nothing <> highlight' "<message>" <> " to send messages"
]
[knownGroup ""]
<> groupDomainLine g
<> ["use " <> ttyToGroup g Nothing <> highlight' "<message>" <> " to send messages"]
knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g
GLPNoRelays _ -> [grpLink "channel has no active relays, please try to join later"]
GLPUpdateRequired _ -> [grpLink "this group requires a newer version of the app, please upgrade"]
@@ -2192,6 +2224,13 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
nextConnectPrepared Contact {preparedContact, activeConn} = case preparedContact of
Just _ -> maybe True (\c -> connStatus c == ConnPrepared) activeConn
_ -> False
contactDomainLine :: Contact -> [StyledString]
contactDomainLine Contact {profile = LocalProfile {contactDomain, contactDomainVerified}} =
simplexDomainLine NTContact contactDomain contactDomainVerified
groupDomainLine :: GroupInfo -> [StyledString]
groupDomainLine GroupInfo {groupDomainVerified, groupProfile = GroupProfile {publicGroup}} = do
let domain = publicGroup >>= publicGroupAccess >>= groupDomainClaim
in simplexDomainLine NTPublicGroup domain groupDomainVerified
viewSigVerification = \case
Just OVVerified -> ["owner signature: verified"]
Just (OVFailed r) -> ["owner signature: FAILED (" <> plain r <> ")"]
@@ -2643,6 +2682,11 @@ viewChatError isCmd logLevel testView = \case
CEChatNotStopped -> ["error: chat not stopped"]
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
CEInvalidConnReq -> viewInvalidConnReq
CESimplexDomainNotReady domain domainErr ->
let reason = case domainErr of
SDENoValidLink -> "has no valid connection link"
SDEUnknownDomain -> "is not included in the connection link's profile"
in [plain $ "SimpleX name " <> strEncode domain <> " " <> reason]
CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr]
CEInvalidChatMessage Connection {connId} msgMeta_ msg e ->
[ plain $
+1 -1
View File
@@ -33,7 +33,7 @@ withBroadcastBot opts test =
bot = simplexChatCore testCfg (mkChatOpts opts) $ broadcastBot opts
broadcastBotProfile :: Profile
broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing}
broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing}
mkBotOpts :: TestParams -> [KnownContact] -> BroadcastBotOpts
mkBotOpts ps publishers =
+1 -1
View File
@@ -101,7 +101,7 @@ directoryServiceTests = do
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}
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 =
+10 -11
View File
@@ -57,9 +57,9 @@ import Simplex.Messaging.Client (ProtocolClientConfig (..))
import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig)
import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange)
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Protocol (sndAuthKeySMPClientVersion)
import Simplex.Messaging.Server (runSMPServerBlocking)
import Simplex.Messaging.Server.Env.STM (ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration)
import NameResolver (NameRegistry, resolverNamesConfig, withNameResolver)
import Simplex.Messaging.Server.MsgStore.STM (STMMsgStore)
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Server (ServerCredentials (..), mkTransportServerConfig)
@@ -202,13 +202,6 @@ testAgentCfg =
where
RetryInterval2 {riFast, riSlow} = messageRetryInterval aCfg
testAgentCfgNoShortLinks :: AgentConfig
testAgentCfgNoShortLinks =
testAgentCfg
{ smpClientVRange = mkVersionRange (Version 1) sndAuthKeySMPClientVersion, -- v3
smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange minClientSMPRelayVersion (Version 14)} -- before shortLinksSMPVersion
}
testCfg :: ChatConfig
testCfg =
defaultChatConfig
@@ -221,9 +214,6 @@ testCfg =
confirmMigrations = MCYesUp
}
testCfgNoShortLinks :: ChatConfig
testCfgNoShortLinks = testCfg {agentConfig = testAgentCfgNoShortLinks}
testAgentCfgVPrev :: AgentConfig
testAgentCfgVPrev =
testAgentCfg
@@ -586,6 +576,8 @@ smpServerCfg =
smpAgentCfg = defaultSMPClientAgentConfig,
allowSMPProxy = True,
serverClientConcurrency = 16,
serverResolverConcurrency = 1000,
namesConfig = Nothing,
information = Nothing,
startOptions = StartOptions {maintenance = False, compactLog = False, logLevel = LogError, skipWarnings = False, confirmMigrations = MCYesUp}
}
@@ -599,6 +591,13 @@ withSmpServer = withSmpServer' smpServerCfg
withSmpServer' :: ServerConfig STMMsgStore -> IO a -> IO a
withSmpServer' cfg = serverBracket (\started -> runSMPServerBlocking started cfg Nothing)
-- | SMP server with a local names resolver attached; the action gets the resolver
-- registry to map names to the addresses it creates.
withSmpServerAndNames :: (NameRegistry -> IO a) -> IO a
withSmpServerAndNames action =
withNameResolver $ \port reg ->
withSmpServer' smpServerCfg {namesConfig = Just (resolverNamesConfig port)} (action reg)
xftpTestPort :: ServiceName
xftpTestPort = "7002"
+3 -3
View File
@@ -1210,20 +1210,20 @@ testOperators =
alice <##. "Current conditions: 2."
alice ##> "/_operators"
alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required"
alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: required"
alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: required"
alice <##. "The new conditions will be accepted for SimpleX Chat Ltd, InFlux Technologies Limited at "
-- set conditions notified
alice ##> "/_conditions_notified 2"
alice <## "ok"
alice ##> "/_operators"
alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required"
alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: required"
alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: required"
alice ##> "/_conditions"
alice <##. "Current conditions: 2 (notified)."
-- accept conditions
alice ##> "/_accept_conditions 2 1,2"
alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted ("
alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: accepted ("
alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: accepted ("
-- update operators
alice ##> "/operators 2:on:smp=proxy:xftp=off"
alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted ("
+143
View File
@@ -0,0 +1,143 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}
module ChatTests.Names where
import ChatClient
import ChatTests.DBUtils
import ChatTests.Groups (prepareChannel1Relay)
import ChatTests.Utils
import Control.Concurrent.Async (concurrently_)
import qualified Data.Text as T
import NameResolver
import Simplex.Messaging.SimplexName (SimplexDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
import Test.Hspec hiding (it)
chatNamesTests :: SpecWith TestParams
chatNamesTests = do
it "connect by resolved name" testConnectByName
it "connect by name not claimed in link profile is rejected" testConnectByNameNotClaimed
it "connect by name to a known contact not claimed in profile is rejected" testConnectByNameKnownContactNotClaimed
it "connect by unregistered name fails to resolve" testConnectByNameNotFound
it "set name not resolving to own address is rejected" testSetNameNotOwnAddress
it "connect by channel name" testConnectByChannelName
testConnectByName :: HasCallStack => TestParams -> IO ()
testConnectByName ps = withSmpServerAndNames $ \reg ->
testChat2 aliceProfile bobProfile (test reg) ps
where
aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" [])
test reg alice bob = do
alice ##> "/ad"
(shortLink, _) <- getContactLinks alice True
registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink))
alice ##> "/_set domain 1 alice.simplex"
alice <## "new contact address set"
bob ##> "/c @alice.simplex"
bob <## "alice: connection started"
alice <## "bob (Bob) wants to connect to you!"
alice <## "to accept: /ac bob"
alice <## "to reject: /rc bob (the sender will NOT be notified)"
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
(bob <## "alice (Alice): contact is connected")
(alice <## "bob (Bob): contact is connected")
alice <##> bob
bob ##> "/i alice"
bob <## "contact ID: 2"
bob <## "receiving messages via: localhost"
bob <## "sending messages via: localhost"
_ <- getTermLine bob
bob <## "SimpleX name: @alice.simplex (verified)"
bob <## "you've shared main profile with this contact"
bob <## "connection not verified, use /code command to see security code"
bob <## "quantum resistant end-to-end encryption"
_ <- getTermLine bob
pure ()
testConnectByNameNotClaimed :: HasCallStack => TestParams -> IO ()
testConnectByNameNotClaimed ps = withSmpServerAndNames $ \reg ->
testChat2 aliceProfile bobProfile (test reg) ps
where
aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" [])
test reg alice bob = do
alice ##> "/ad"
(shortLink, _) <- getContactLinks alice True
registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink))
bob ##> "/c @alice.simplex"
bob <## "SimpleX name alice.simplex is not included in the connection link's profile"
testConnectByNameKnownContactNotClaimed :: HasCallStack => TestParams -> IO ()
testConnectByNameKnownContactNotClaimed ps = withSmpServerAndNames $ \reg ->
testChat2 aliceProfile bobProfile (test reg) ps
where
aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" [])
test reg alice bob = do
alice ##> "/ad"
(shortLink, _) <- getContactLinks alice True
bob ##> ("/c " <> shortLink)
bob <## "connection request sent!"
alice <## "bob (Bob) wants to connect to you!"
alice <## "to accept: /ac bob"
alice <## "to reject: /rc bob (the sender will NOT be notified)"
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
(bob <## "alice (Alice): contact is connected")
(alice <## "bob (Bob): contact is connected")
registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink))
bob ##> "/c @alice.simplex"
bob <## "SimpleX name alice.simplex is not included in the connection link's profile"
testConnectByNameNotFound :: HasCallStack => TestParams -> IO ()
testConnectByNameNotFound ps = withSmpServerAndNames $ \_reg ->
testChat2 aliceProfile bobProfile test ps
where
test _alice bob = do
bob ##> "/c @nobody.simplex"
bob .<## "smpErr = NAME {nameErr = NOT_FOUND}}"
testSetNameNotOwnAddress :: HasCallStack => TestParams -> IO ()
testSetNameNotOwnAddress ps = withSmpServerAndNames $ \reg ->
testChat2 aliceProfile bobProfile (test reg) ps
where
aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" [])
test reg alice bob = do
bob ##> "/ad"
(bobShortLink, _) <- getContactLinks bob True
registerName reg aliceName (contactNameRecord "alice" (T.pack bobShortLink))
alice ##> "/ad"
_ <- getContactLinks alice True
alice ##> "/_set domain 1 alice.simplex"
alice <## "SimpleX name alice.simplex has no valid connection link"
testConnectByChannelName :: HasCallStack => TestParams -> IO ()
testConnectByChannelName ps = withSmpServerAndNames $ \reg ->
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath ->
withNewTestChat ps "bob" bobProfile $ \bob -> do
(shortLink, _) <- prepareChannel1Relay "team" alice cath
registerName reg teamName (channelNameRecord "team" (T.pack shortLink))
alice ##> "/public group access #team domain=team.simplex"
alice <## "updated public group access: domain=team.simplex"
cath <## "alice updated group #team: (signed)"
cath <## "updated public group access: domain=team.simplex"
bob ##> "/c #team.simplex"
bob <## "#team: connection started"
concurrentlyN_
[ bob
<### [ "#team: joining the group (connecting to relay cath)...",
"#team: you joined the group (connected to relay cath)"
]
, do
cath <## "bob (Bob): accepting request to join group #team..."
cath <## "#team: bob joined the group"
, alice <### [EndsWith "introduced bob (Bob) in the channel"]
]
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "group link: known group #team"
bob <## "SimpleX name: #team (verified)"
bob <## "use #team <message> to send messages"
where
teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" [])
+7 -84
View File
@@ -65,12 +65,9 @@ chatProfileTests = do
it "reject contact and delete contact link" testRejectContactAndDeleteUserContact
it "keep connection requests when contact link deleted" testKeepConnectionRequests
it "connected contact works when contact link deleted" testContactLinkDeletedConnectedContactWorks
-- TODO [short links] test auto-reply with current version, with connecting client not preparing contact
it "auto-reply message" testAutoReplyMessage
it "auto-reply message in incognito" testAutoReplyMessageInIncognito
describe "business address" $ do
it "create and connect via business address" testBusinessAddress
-- TODO [short links] test business auto-reply with current version, with connecting client not preparing contact
it "update profiles with business address" testBusinessUpdateProfiles
describe "contact address connection plan" $ do
it "contact address ok to connect; known contact" testPlanAddressOkKnown
@@ -82,7 +79,6 @@ chatProfileTests = do
describe "incognito" $ do
it "connect incognito via invitation link" testConnectIncognitoInvitationLink
it "connect incognito via contact address" testConnectIncognitoContactAddress
it "accept contact request incognito" testAcceptContactRequestIncognito
it "set connection incognito" testSetConnectionIncognito
it "reset connection incognito" testResetConnectionIncognito
it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation
@@ -501,7 +497,7 @@ testMultiWordProfileNames =
aliceProfile' = baseProfile {displayName = "Alice Jones"}
bobProfile' = baseProfile {displayName = "Bob James"}
cathProfile' = baseProfile {displayName = "Cath Johnson"}
baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing}
baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing}
testUserContactLink :: HasCallStack => TestParams -> IO ()
testUserContactLink =
@@ -974,10 +970,10 @@ testContactLinkDeletedConnectedContactWorks = testChat2 aliceProfile bobProfile
bob @@@ [("@alice", "hey")]
testAutoReplyMessage :: HasCallStack => TestParams -> IO ()
testAutoReplyMessage = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile $
testAutoReplyMessage = testChat2 aliceProfile bobProfile $
\alice bob -> do
alice ##> "/ad"
cLink <- getContactLinkNoShortLink alice True
cLink <- getContactLink alice True
alice ##> "/auto_accept on incognito=off text hello!"
alice <## "auto_accept on"
alice <## "auto reply:"
@@ -995,31 +991,6 @@ testAutoReplyMessage = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile
alice <## "bob (Bob): contact is connected"
]
testAutoReplyMessageInIncognito :: HasCallStack => TestParams -> IO ()
testAutoReplyMessageInIncognito = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile $
\alice bob -> do
alice ##> "/ad"
cLink <- getContactLinkNoShortLink alice True
alice ##> "/auto_accept on incognito=on text hello!"
alice <## "auto_accept on, incognito"
alice <## "auto reply:"
alice <## "hello!"
bob ##> ("/c " <> cLink)
bob <## "connection request sent!"
alice <## "bob (Bob): accepting contact request..."
alice <## "bob (Bob): you can send messages to contact"
alice <# "i @bob hello!"
aliceIncognito <- getTermLine alice
concurrentlyN_
[ do
bob <# (aliceIncognito <> "> hello!")
bob <## (aliceIncognito <> ": contact is connected"),
do
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## "use /i bob to print out this incognito profile again"
]
testBusinessAddress :: HasCallStack => TestParams -> IO ()
testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile $
\biz alice bob -> do
@@ -1075,10 +1046,10 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice
(biz <# "#bob bob_1> hey there")
testBusinessUpdateProfiles :: HasCallStack => TestParams -> IO ()
testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile aliceProfile bobProfile cathProfile $
testBusinessUpdateProfiles = testChat4 businessProfile aliceProfile bobProfile cathProfile $
\biz alice bob cath -> do
biz ##> "/ad"
cLink <- getContactLinkNoShortLink biz True
cLink <- getContactLink biz True
biz ##> "/auto_accept on business text Welcome"
biz <## "auto_accept on, business"
biz <## "auto reply:"
@@ -1108,7 +1079,7 @@ testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile al
biz ##> "/mr alisa alisa_1 admin"
biz <## "#alisa: you changed the role of alisa_1 to admin"
alice <## "#biz: biz_1 changed your role from member to admin"
connectUsersNoShortLink alice bob
connectUsers alice bob
alice ##> "/a #biz bob"
alice <## "invitation to join the group #biz sent to bob"
bob <## "#biz (Biz Inc): alisa invites you to join the group as member"
@@ -1139,7 +1110,7 @@ testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile al
alice <# "#biz robert> hi there"
biz <# "#alisa robert> hi there"
-- add business team member
connectUsersNoShortLink biz cath
connectUsers biz cath
biz ##> "/a #alisa cath"
biz <## "invitation to join the group #alisa sent to cath"
cath <## "#alisa: biz invites you to join the group as member"
@@ -1628,54 +1599,6 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
(bob </)
bob `hasContactProfiles` ["bob"]
testAcceptContactRequestIncognito :: HasCallStack => TestParams -> IO ()
testAcceptContactRequestIncognito = testChatCfg3 testCfgNoShortLinks aliceProfile bobProfile cathProfile $
\alice bob cath -> do
alice ##> "/ad"
cLink <- getContactLinkNoShortLink alice True
-- GUI /_accept api
bob ##> ("/c " <> cLink)
alice <#? bob
alice ##> "/_accept incognito=on 1"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
aliceIncognitoBob <- getTermLine alice
concurrentlyN_
[ bob <## (aliceIncognitoBob <> ": contact is connected"),
do
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognitoBob)
alice <## "use /i bob to print out this incognito profile again"
]
-- conversation is incognito
alice ?#> "@bob my profile is totally inconspicuous"
bob <# (aliceIncognitoBob <> "> my profile is totally inconspicuous")
bob #> ("@" <> aliceIncognitoBob <> " I know!")
alice ?<# "bob> I know!"
-- list contacts
alice ##> "/contacts"
alice <## "i bob (Bob)"
alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognitoBob]
-- delete contact, incognito profile is deleted
alice ##> "/d bob"
alice <## "bob: contact is deleted"
bob <## (aliceIncognitoBob <> " deleted contact with you")
alice ##> "/contacts"
(alice </)
alice `hasContactProfiles` ["alice"]
-- terminal /accept api
cath ##> ("/c " <> cLink)
alice <#? cath
alice ##> "/accept incognito cath"
alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
aliceIncognitoCath <- getTermLine alice
concurrentlyN_
[ cath <## (aliceIncognitoCath <> ": contact is connected"),
do
alice <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> aliceIncognitoCath)
alice <## "use /i cath to print out this incognito profile again"
]
alice `hasContactProfiles` ["alice", "cath", T.pack aliceIncognitoCath]
cath `hasContactProfiles` ["cath", T.pack aliceIncognitoCath]
testSetConnectionIncognito :: HasCallStack => TestParams -> IO ()
testSetConnectionIncognito = testChat2 aliceProfile bobProfile $
\alice bob -> do
+1 -1
View File
@@ -88,7 +88,7 @@ serviceProfile :: Profile
serviceProfile = mkProfile "service_user" "Service user" Nothing
mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile
mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing}
mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing}
it :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation))
it name test =
+2 -2
View File
@@ -10,7 +10,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Simplex.Chat.Markdown
import Simplex.Messaging.Agent.Protocol (SimplexNameDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
import Simplex.Messaging.Agent.Protocol (SimplexDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$$>))
import System.Console.ANSI.Types
@@ -400,7 +400,7 @@ command' :: Text -> Text -> FormattedText
command' = FormattedText . Just . Command
sname :: SimplexNameType -> SimplexTLD -> Text -> [Text] -> Text -> Markdown
sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt (SimplexNameDomain ns dom sub)) (pfx <> txt)
sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt (SimplexDomain ns dom sub)) (pfx <> txt)
where
pfx = case nt of NTPublicGroup -> "#"; NTContact -> "@"
+83
View File
@@ -0,0 +1,83 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Local HTTP names resolver for chat tests, copied from simplexmq's
-- NamesResolverServer and made dynamic: it answers /resolve/<domain> from a
-- mutable name -> NameRecord registry, so a test can resolve a name to the
-- address it just created.
module NameResolver
( NameRegistry,
withNameResolver,
registerName,
contactNameRecord,
channelNameRecord,
resolverNamesConfig,
)
where
import Control.Concurrent.STM
import qualified Data.Aeson as J
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Text (Text)
import Network.HTTP.Types (hContentType, notFound404, ok200)
import Network.Wai (Application, pathInfo, responseLBS)
import qualified Network.Wai.Handler.Warp as Warp
import Simplex.Messaging.Names.Record (NameRecord (..))
import Simplex.Messaging.Server.Names (NamesConfig (..))
import Simplex.Messaging.SimplexName (SimplexNameInfo (..), fullDomainName)
type NameRegistry = TVar (Map Text NameRecord)
-- | Run an action with a local resolver on a free port and its registry (keyed
-- by full domain name, what the resolver looks the name up by).
withNameResolver :: (Int -> TVar (Map Text NameRecord) -> IO a) -> IO a
withNameResolver action = do
reg <- newTVarIO M.empty
Warp.withApplication (pure (app reg)) $ \port -> action port reg
where
app :: TVar (Map Text NameRecord) -> Application
app reg req send = do
(st, body) <- case pathInfo req of
["health"] -> pure (ok200, "{}")
["resolve", d] -> maybe (notFound404, "{}") (\r -> (ok200, J.encode r)) . M.lookup d <$> readTVarIO reg
_ -> pure (notFound404, "{}")
send $ responseLBS st [(hContentType, "application/json")] body
-- | Register a name's domain to resolve to the given record.
registerName :: TVar (Map Text NameRecord) -> SimplexNameInfo -> NameRecord -> IO ()
registerName reg SimplexNameInfo {nameDomain} r =
atomically $ modifyTVar' reg $ M.insert (fullDomainName nameDomain) r
contactNameRecord :: Text -> Text -> NameRecord
contactNameRecord name link = (emptyRecord name) {nrSimplexContact = [link]}
channelNameRecord :: Text -> Text -> NameRecord
channelNameRecord name link = (emptyRecord name) {nrSimplexChannel = [link]}
emptyRecord :: Text -> NameRecord
emptyRecord name =
NameRecord
{ nrName = name,
nrNickname = "",
nrWebsite = "",
nrLocation = "",
nrSimplexContact = [],
nrSimplexChannel = [],
nrEth = Nothing,
nrBtc = Nothing,
nrXmr = Nothing,
nrDot = Nothing,
nrOwner = "",
nrResolver = ""
}
-- | NamesConfig for a chat test SMP server pointing at this resolver.
resolverNamesConfig :: Int -> NamesConfig
resolverNamesConfig port =
NamesConfig
{ resolverEndpoint = "http://127.0.0.1:" <> show port,
resolverAuth = Nothing,
resolverTimeoutMs = 1000,
resolverMaxResponseBytes = 65536
}
+3 -3
View File
@@ -38,9 +38,9 @@ validateServersTest :: Spec
validateServersTest = describe "validate user servers" $ do
it "should pass valid user servers" $ validateUserServers [valid] [] `shouldBe` ([], [])
it "should fail without servers" $ do
validateUserServers [invalidNoServers] [] `shouldBe` ([USENoServers aSMP Nothing], [])
validateUserServers [invalidDisabled] [] `shouldBe` ([USENoServers aSMP Nothing], [])
validateUserServers [invalidDisabledOp] [] `shouldBe` ([USENoServers aSMP Nothing, USENoServers aXFTP Nothing], [USWNoChatRelays Nothing])
validateUserServers [invalidNoServers] [] `shouldBe` ([USENoServers aSMP Nothing], [USWNoNamesServers Nothing])
validateUserServers [invalidDisabled] [] `shouldBe` ([USENoServers aSMP Nothing], [USWNoNamesServers Nothing])
validateUserServers [invalidDisabledOp] [] `shouldBe` ([USENoServers aSMP Nothing, USENoServers aXFTP Nothing], [USWNoChatRelays Nothing, USWNoNamesServers Nothing])
it "should fail without servers with storage role" $ do
validateUserServers [invalidNoStorage] [] `shouldBe` ([USEStorageMissing aSMP Nothing], [])
it "should fail with duplicate host" $ do
+3 -1
View File
@@ -80,5 +80,7 @@ skipComparisonForDownMigrations =
-- group_member_intro_id field moves
"20251128_migrate_member_relations",
-- on down migration single_sender_group_member_id column is re-added at the end of the table
"20260529_delivery_job_senders"
"20260529_delivery_job_senders",
-- group_domain is removed
"20260603_simplex_name"
]
+3 -2
View File
@@ -3,6 +3,7 @@
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module ProtocolTests where
@@ -107,7 +108,7 @@ testGroupPreferences :: Maybe GroupPreferences
testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, commands = Nothing}
testProfile :: Profile
testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing}
testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing}
testGroupProfile :: GroupProfile
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing}
@@ -240,7 +241,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
#==# XInfo testProfile
it "x.info with empty full name" $
"{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing}
#==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing}
it "x.contact with xContactId" $
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
#==# XContact testProfile (Just $ XContactId "\1\2\3\4") Nothing Nothing
+3 -1
View File
@@ -145,7 +145,9 @@ skipComparisonForDownMigrations =
-- on down migration single_sender_group_member_id column and its index
-- are re-added at the end of the table / file (ALTER TABLE ADD COLUMN
-- appends; CREATE INDEX appends).
"20260529_delivery_job_senders"
"20260529_delivery_job_senders",
-- group_domain is removed
"20260603_simplex_name"
]
getSchema :: FilePath -> FilePath -> IO String
+6
View File
@@ -8,6 +8,7 @@ import Bots.DirectoryTests
import ChatClient
import ChatTests
import ChatTests.DBUtils
import ChatTests.Names (chatNamesTests)
import ChatTests.Utils (xdescribe'')
import Control.Logger.Simple
import Data.Time.Clock.System
@@ -71,6 +72,9 @@ main = do
describe "Message batching" batchingTests
describe "Operators" operatorTests
describe "Random servers" randomServersTests
#if !defined(dbPostgres)
around (tmpTestBracket chatQueryStats agentQueryStats) $ describe "names tests" chatNamesTests
#endif
#if defined(dbPostgres)
createdDropDb . around testBracket
#else
@@ -96,6 +100,8 @@ main = do
#else
testBracket chatQueryStats agentQueryStats test =
withSmpServer $ tmpBracket $ \tmpPath -> test TestParams {tmpPath, chatQueryStats, agentQueryStats, printOutput = False}
tmpTestBracket chatQueryStats agentQueryStats test =
tmpBracket $ \tmpPath -> test TestParams {tmpPath, chatQueryStats, agentQueryStats, printOutput = False}
#endif
tmpBracket test = do
t <- getSystemTime