Merge branch 'ep/fix-async-race' into ep/conc-msgs

This commit is contained in:
Evgeny Poberezkin
2026-06-01 17:35:33 +01:00
29 changed files with 973 additions and 208 deletions
+13
View File
@@ -2533,10 +2533,22 @@ public enum GroupType: Codable, Hashable {
}
}
public struct PublicGroupAccess: Codable, Hashable {
public var groupWebPage: String?
public var groupDomain: String?
public var domainWebPage: Bool = false
public var allowEmbedding: Bool = false
}
public struct RelayCapabilities: Codable, Hashable {
public var baseWebUrl: String?
}
public struct PublicGroupProfile: Codable, Hashable {
public var groupType: GroupType
public var groupLink: String
public var publicGroupId: String
public var publicGroupAccess: PublicGroupAccess?
}
public struct GroupProfile: Codable, NamedChat, Hashable {
@@ -2705,6 +2717,7 @@ public struct GroupRelay: Identifiable, Decodable, Equatable, Hashable {
public var userChatRelay: UserChatRelay
public var relayStatus: RelayStatus
public var relayLink: String?
public var relayCap: RelayCapabilities
public var id: Int64 { groupRelayId }
}
@@ -2240,11 +2240,25 @@ object GroupTypeSerializer : KSerializer<GroupType> {
}
}
@Serializable
data class PublicGroupAccess(
val groupWebPage: String? = null,
val groupDomain: String? = null,
val domainWebPage: Boolean = false,
val allowEmbedding: Boolean = false
)
@Serializable
data class RelayCapabilities(
val baseWebUrl: String? = null
)
@Serializable
data class PublicGroupProfile(
val groupType: GroupType,
val groupLink: String,
val publicGroupId: String
val publicGroupId: String,
val publicGroupAccess: PublicGroupAccess? = null
)
@Serializable
@@ -2368,7 +2382,8 @@ data class GroupRelay(
val groupMemberId: Long,
val userChatRelay: UserChatRelay,
val relayStatus: RelayStatus,
val relayLink: String? = null
val relayLink: String? = null,
val relayCap: RelayCapabilities
) {
val id: Long get() = groupRelayId
}
+23
View File
@@ -146,6 +146,7 @@ This file is generated automatically.
- [Profile](#profile)
- [ProxyClientError](#proxyclienterror)
- [ProxyError](#proxyerror)
- [PublicGroupAccess](#publicgroupaccess)
- [PublicGroupData](#publicgroupdata)
- [PublicGroupProfile](#publicgroupprofile)
- [RCErrorType](#rcerrortype)
@@ -157,6 +158,7 @@ This file is generated automatically.
- [RcvFileTransfer](#rcvfiletransfer)
- [RcvGroupEvent](#rcvgroupevent)
- [RcvMsgError](#rcvmsgerror)
- [RelayCapabilities](#relaycapabilities)
- [RelayProfile](#relayprofile)
- [RelayStatus](#relaystatus)
- [ReportReason](#reportreason)
@@ -2496,6 +2498,7 @@ UpdateRequired:
- userChatRelay: [UserChatRelay](#userchatrelay)
- relayStatus: [RelayStatus](#relaystatus)
- relayLink: string?
- relayCap: [RelayCapabilities](#relaycapabilities)
---
@@ -3066,6 +3069,17 @@ NO_SESSION:
- type: "NO_SESSION"
---
## PublicGroupAccess
**Record type**:
- groupWebPage: string?
- groupDomain: string?
- domainWebPage: bool
- allowEmbedding: bool
---
## PublicGroupData
@@ -3082,6 +3096,7 @@ NO_SESSION:
- groupType: [GroupType](#grouptype)
- groupLink: string
- publicGroupId: string
- publicGroupAccess: [PublicGroupAccess](#publicgroupaccess)?
---
@@ -3339,6 +3354,14 @@ ParseError:
- parseError: string
---
## RelayCapabilities
**Record type**:
- baseWebUrl: string?
---
## RelayProfile
+4
View File
@@ -327,6 +327,7 @@ chatTypesDocsData =
(sti @Profile, STRecord, "", [], "", ""),
(sti @ProxyClientError, STUnion, "Proxy", [], "", ""),
(sti @ProxyError, STUnion, "", [], "", ""),
(sti @PublicGroupAccess, STRecord, "", [], "", ""),
(sti @PublicGroupData, STRecord, "", [], "", ""),
(sti @PublicGroupProfile, STRecord, "", [], "", ""),
(sti @RatchetSyncState, STEnum, "RS", [], "", ""),
@@ -338,6 +339,7 @@ chatTypesDocsData =
(sti @RcvFileTransfer, STRecord, "", [], "", ""),
(sti @RcvGroupEvent, STUnion, "RGE", [], "", ""),
(sti @RcvMsgError, STUnion, "RME", [], "", ""),
(sti @RelayCapabilities, STRecord, "", [], "", ""),
(sti @RelayProfile, STRecord, "", [], "", ""),
(sti @RelayStatus, STEnum, "RS", [], "", ""),
(sti @ReportReason, STEnum' (dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""),
@@ -546,6 +548,7 @@ deriving instance Generic PreparedGroup
deriving instance Generic Profile
deriving instance Generic ProxyClientError
deriving instance Generic ProxyError
deriving instance Generic PublicGroupAccess
deriving instance Generic PublicGroupData
deriving instance Generic PublicGroupProfile
deriving instance Generic RatchetSyncState
@@ -557,6 +560,7 @@ deriving instance Generic RcvFileStatus
deriving instance Generic RcvFileTransfer
deriving instance Generic RcvGroupEvent
deriving instance Generic RcvMsgError
deriving instance Generic RelayCapabilities
deriving instance Generic RelayProfile
deriving instance Generic RelayStatus
deriving instance Generic ReportReason
+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: 906da42de095b59965859d9397d2fd01f6c128c1
tag: 4922812707d791a4e3bfa11d4ce0c77ea5ebb67e
source-repository-package
type: git
@@ -2770,6 +2770,7 @@ export interface GroupRelay {
userChatRelay: UserChatRelay
relayStatus: RelayStatus
relayLink?: string
relayCap: RelayCapabilities
}
export type GroupRootKey = GroupRootKey.Private | GroupRootKey.Public
@@ -3351,6 +3352,13 @@ export namespace ProxyError {
}
}
export interface PublicGroupAccess {
groupWebPage?: string
groupDomain?: string
domainWebPage: boolean
allowEmbedding: boolean
}
export interface PublicGroupData {
publicMemberCount: number // int64
}
@@ -3359,6 +3367,7 @@ export interface PublicGroupProfile {
groupType: GroupType
groupLink: string
publicGroupId: string
publicGroupAccess?: PublicGroupAccess
}
export type RCErrorType =
@@ -3747,6 +3756,10 @@ export namespace RcvMsgError {
}
}
export interface RelayCapabilities {
baseWebUrl?: string
}
export interface RelayProfile {
displayName: string
fullName: string
@@ -1945,6 +1945,7 @@ class GroupRelay(TypedDict):
userChatRelay: "UserChatRelay"
relayStatus: "RelayStatus"
relayLink: NotRequired[str]
relayCap: "RelayCapabilities"
class GroupRootKey_private(TypedDict):
type: Literal["private"]
@@ -2353,6 +2354,12 @@ ProxyError = ProxyError_PROTOCOL | ProxyError_BROKER | ProxyError_BASIC_AUTH | P
ProxyError_Tag = Literal["PROTOCOL", "BROKER", "BASIC_AUTH", "NO_SESSION"]
class PublicGroupAccess(TypedDict):
groupWebPage: NotRequired[str]
groupDomain: NotRequired[str]
domainWebPage: bool
allowEmbedding: bool
class PublicGroupData(TypedDict):
publicMemberCount: int # int64
@@ -2360,6 +2367,7 @@ class PublicGroupProfile(TypedDict):
groupType: "GroupType"
groupLink: str
publicGroupId: str
publicGroupAccess: NotRequired["PublicGroupAccess"]
class RCErrorType_internal(TypedDict):
type: Literal["internal"]
@@ -2628,6 +2636,9 @@ RcvMsgError = RcvMsgError_dropped | RcvMsgError_parseError
RcvMsgError_Tag = Literal["dropped", "parseError"]
class RelayCapabilities(TypedDict):
baseWebUrl: NotRequired[str]
class RelayProfile(TypedDict):
displayName: str
fullName: str
+596
View File
@@ -0,0 +1,596 @@
# Channel Web Preview
## Context
SimpleX channels are public - anybody with the link to join and chat relays rebroadcasting the messages can see content. To grow channels, owners need a public web preview (like Telegram's `t.me/s/channelname`) showing the last 50 messages. This lets potential subscribers browse before joining.
The relay already stores all messages in its database. The web preview is a periodic read-and-render loop that writes JSON files served by Caddy, with CORS controlling which domains can embed the preview.
This feature integrates with the `.simplex` namespace (ENS-based names resolving to channel links). A channel's registered domain (`groupDomain`) lives in `PublicGroupAccess` inside `PublicGroupProfile` and is disseminated with the profile. On-chain verification of the domain is deferred until RSLV resolution protocol ships.
## Architecture
```
simplex-chat CLI (--relay --web-json-dir=... --web-base-url=...)
├── Main chat loop (existing)
├── Relay logic (existing, gated by --relay)
└── Web preview thread (new, gated by relayWebOptions)
├── Periodic: load publishable groups → render JSON → write files
└── Regenerate Caddy CORS config → caddy reload
Caddy (operator-configured)
├── Serves JSON at <baseWebUrl>/<publicGroupId>.json
└── Imports generated CORS config file
Channel page (static HTML+JS, hosted by owner or on GitHub)
├── Fetches JSON from relay(s) with fallback
└── Renders messages, shows join button
```
## Data Model Changes
### 1. Extend `PublicGroupProfile` with domain and web access settings
**File:** `src/Simplex/Chat/Types.hs` (line 796)
Current:
```haskell
data PublicGroupProfile = PublicGroupProfile
{ groupType :: GroupType,
groupLink :: ShortLinkContact,
publicGroupId :: B64UrlByteString
}
```
New:
```haskell
data PublicGroupAccess = PublicGroupAccess
{ groupWebPage :: Maybe Text, -- channel's web page URL (adds CORS origin)
groupDomain :: Maybe Text, -- domain for this channel (must have link set in domain record in the contract)
domainWebPage :: Bool, -- show on the domain's page (e.g. simplexnetwork.org site for simplex TLD domains, or domain site for web domains)
allowEmbeding :: Bool -- allow embedding from any origin (CORS: *)
}
data PublicGroupProfile = PublicGroupProfile
{ groupType :: GroupType,
groupLink :: ShortLinkContact,
publicGroupId :: B64UrlByteString,
publicGroupAccess :: Maybe PublicGroupAccess -- NEW: web preview settings
}
```
`groupDomain` stores the channel's registered `.simplex` domain name or another supported TLD. It is:
- Set by the owner after registering a name on-chain
- Disseminated to all members via `GroupProfile` (nested in `publicGroup`)
- Used by `simplexnetwork.org/c/<name>` to route to the channel's web preview (for .simplex domain)
JSON instances: TH-derived `$(JQ.deriveJSON defaultJSON ''PublicGroupAccess)`. Existing `$(JQ.deriveJSON defaultJSON ''PublicGroupProfile)` covers the new optional field.
**Migration (SQLite/Postgres):** separate columns, same pattern as `group_type`/`group_link`/`public_group_id`:
```sql
ALTER TABLE group_profiles ADD COLUMN group_web_page TEXT;
ALTER TABLE group_profiles ADD COLUMN group_domain TEXT;
ALTER TABLE group_profiles ADD COLUMN domain_web_page INTEGER;
ALTER TABLE group_profiles ADD COLUMN allow_embedding INTEGER;
ALTER TABLE group_profiles ADD COLUMN group_domain_verified_at TEXT;
```
`group_domain_verified_at` is relay-local verification state (nullable timestamp, NULL = unverified).
**Store changes:**
`src/Simplex/Chat/Store/Shared.hs` line 693 - new constructor alongside `toPublicGroupProfile`:
```haskell
toPublicGroupAccess :: Maybe Text -> Maybe Text -> Maybe BoolInt -> Maybe BoolInt -> Maybe PublicGroupAccess
toPublicGroupAccess groupWebPage groupDomain domainWebPage_ allowEmbeding_
| isJust groupWebPage || isJust groupDomain || fromBI domainWebPage_ || fromBI allowEmbeding_ =
Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage = fromBI domainWebPage_, allowEmbeding = fromBI allowEmbeding_}
| otherwise = Nothing
where fromBI = maybe False unBI
```
Extend `toPublicGroupProfile` to accept and pass through `Maybe PublicGroupAccess`.
`GroupInfoRow` type (line 668) gains columns for: `group_web_page`, `group_domain`, `domain_web_page`, `allow_embedding`, `group_domain_verified_at`.
`src/Simplex/Chat/Store/Groups.hs`:
- INSERT (line 367): add all new columns
- SELECT (line 2375): add `gp.group_web_page`, `gp.group_domain`, `gp.domain_web_page`, `gp.allow_embedding`, `gp.group_domain_verified_at`
- UPDATE (line 1922): include new columns in `updateGroupProfile_`
### 2. `RelayCapabilities` record, extend `XGrpRelayAcpt`, new `XGrpRelayCap`
**File:** `src/Simplex/Chat/Protocol.hs`
New record for relay capabilities (extensible for future fields):
```haskell
data RelayCapabilities = RelayCapabilities
{ baseWebUrl :: Maybe Text
}
```
TH-derived JSON. All fields optional so old relays produce `{}` and new fields are backward compatible.
**`XGrpRelayAcpt`** - carries capabilities at acceptance time:
Current (line 444): `XGrpRelayAcpt :: ShortLinkContact -> ChatMsgEvent 'Json`
New: `XGrpRelayAcpt :: ShortLinkContact -> RelayCapabilities -> ChatMsgEvent 'Json`
Parsing: `XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink" <*> (p "relayCap" <|> pure defaultRelayCap)`
Encoding: `XGrpRelayAcpt relayLink cap -> o ["relayLink" .= relayLink, "relayCap" .= cap]`
Backward compatible: old relays omit `relayCap`, parsed as default (all `Nothing`).
**`XGrpRelayCap`** - new message for ongoing capability updates:
```haskell
XGrpRelayCap :: RelayCapabilities -> ChatMsgEvent 'Json
```
Tag: `"x.grp.relay.cap"`
Parsing: `XGrpRelayCap_ -> XGrpRelayCap <$> p "relayCap"`
Encoding: `XGrpRelayCap cap -> o ["relayCap" .= cap]`
Sent by relay to owner only when capabilities change (not periodic). Relay detects change by comparing current config against persisted state on startup.
### 3. Store `baseWebUrl` per relay
**File:** `src/Simplex/Chat/Operators.hs` (line 278)
Current:
```haskell
data GroupRelay = GroupRelay
{ groupRelayId :: Int64,
groupMemberId :: Int64,
userChatRelay :: UserChatRelay,
relayStatus :: RelayStatus,
relayLink :: Maybe ShortLinkContact
}
```
Add: `relayCap :: Maybe RelayCapabilities`
Stored as separate columns (same pattern as `PublicGroupAccess`):
**Migration:** `ALTER TABLE group_relays ADD COLUMN base_web_url TEXT`
`relayCap` constructed from columns: `Just RelayCapabilities {baseWebUrl}` when any capability column is non-NULL, `Nothing` otherwise.
**Handlers in `src/Simplex/Chat/Library/Subscriber.hs`:**
- `XGrpRelayAcpt` (line 770): store `RelayCapabilities` in relay record on acceptance
- `XGrpRelayCap` (new handler): update `RelayCapabilities` in relay record; only accepted from relay members (`isRelay m`), owner receives
**Relay-side persistence:** relay persists its current `RelayCapabilities` (derived from `RelayWebOptions`) so it can detect config changes on restart. On startup, if persisted capabilities differ from config, relay sends `XGrpRelayCap` to all group owners it serves.
### 4. CLI options for web preview
**File:** `src/Simplex/Chat/Options.hs`
New record bundling all web preview options:
```haskell
data RelayWebOptions = RelayWebOptions
{ webJsonDir :: FilePath, -- --web-json-dir: where to write JSON files
webBaseUrl :: Text, -- --web-base-url: public URL prefix (sent in XGrpRelayAcpt)
webCorsFile :: FilePath, -- --web-cors-file: generated Caddy CORS config path
webUpdateInterval :: Int -- --web-update-interval: seconds (default 300)
}
```
Add as a proper field in `CoreChatOpts`:
```haskell
data CoreChatOpts = CoreChatOpts
{ ...existing...,
relayWebOptions :: Maybe RelayWebOptions
}
```
Parsed from CLI: when `--web-json-dir` is provided, all other `--web-*` flags are required. `Nothing` when no web preview flags are set. Only meaningful when `--relay` is also set.
### 5. Web preview thread startup
**File:** `src/Simplex/Chat/Core.hs` (line 74)
Current:
```haskell
runSimplexChat ... = do
a1 <- runReaderT (startChatController True True) cc
when (chatRelay && not testView) $ askCreateRelayAddress cc u
forM_ (postStartHook chatHooks) ($ cc)
a2 <- async $ chat u cc
waitEither_ a1 a2
```
Add web preview thread as a third async when config is present:
```haskell
runSimplexChat ... = do
a1 <- runReaderT (startChatController True True) cc
when (chatRelay && not testView) $ askCreateRelayAddress cc u
forM_ (postStartHook chatHooks) ($ cc)
a2 <- async $ chat u cc
case relayWebOptions coreOptions of
Nothing -> waitEither_ a1 a2
Just webOpts -> do
a3 <- async $ webPreviewThread webOpts cc
void $ waitAnyCancel [a1, a2, a3]
```
## New Types for JSON Serialization
**File:** new module `src/Simplex/Chat/Web/Preview.hs`
### Reuse as-is (existing ToJSON instances)
- `GroupProfile` (Types.hs:803) - channel metadata (displayName, fullName, shortDescr, description, image, publicGroup incl. groupDomain)
- `MsgContent` (Protocol.hs:689) - tagged union: MCText, MCLink, MCImage, MCVideo, etc.
- `LinkPreview` (Protocol.hs:256) - `{uri, title, description, image, content}`
- `FormattedText` / `MarkdownList` (Markdown.hs:133/139) - parsed markdown
- `QuotedMsg` / `MsgRef` (Protocol.hs:589) - quoted message context
- `MsgMentions` = `Map MemberName CIMention` (Messages.hs:264)
- `CIMention` (Messages.hs:272) - `{memberId, memberRef}`
- `CIReactionCount` (Messages.hs:338) - `{reaction, userReacted, totalReacted}`
### New types
```haskell
data WebFileInfo = WebFileInfo
{ fileName :: String,
fileSize :: Integer
}
data WebMemberProfile = WebMemberProfile
{ memberId :: MemberId,
displayName :: Text,
image :: Maybe ImageData
}
data WebMessage = WebMessage
{ sender :: Maybe MemberId, -- Nothing for CIChannelRcv (forwarded-from-channel)
ts :: UTCTime,
content :: MsgContent,
formattedText :: Maybe MarkdownList,
file :: Maybe WebFileInfo,
quote :: Maybe QuotedMsg,
mentions :: Map MemberName CIMention,
reactions :: [CIReactionCount],
forwarded :: Maybe CIForwardedFrom,
edited :: Bool
}
data WebChannelPreview = WebChannelPreview
{ channel :: GroupProfile, -- NOTE: render loop strips groupDomain until verified
subscriberCount :: Maybe Int,
members :: [WebMemberProfile],
messages :: [WebMessage],
updatedAt :: UTCTime
}
```
TH-derived JSON for `WebFileInfo`, `WebMemberProfile`, `WebMessage`, `WebChannelPreview`.
## Render Loop
**File:** new module `src/Simplex/Chat/Web.hs`
Pattern from directory service's `updateListingsThread_` (Service.hs:185-194).
```haskell
webPreviewThread :: RelayWebOptions -> ChatController -> IO ()
webPreviewThread opts cc = forever $ do
u_ <- readTVarIO $ currentUser cc
forM_ u_ $ \user -> do
groups <- getWebPublishGroups cc user
corsEntries <- forM groups $ \gInfo -> do
renderGroupPreview opts cc user gInfo
pure (corsEntry gInfo)
writeCorsConfig opts corsEntries
threadDelay (webUpdateInterval opts * 1_000_000)
```
### Loading groups
New store function `getWebPublishGroups`:
```sql
SELECT ... FROM groups g
JOIN group_profiles gp ON g.group_profile_id = gp.group_profile_id
WHERE gp.group_web_page IS NOT NULL
AND g.user_id = ?
```
Returns `[GroupInfo]`. For each, call `getGroupChat` with `CPLast 50` (Store/Messages.hs:1436) to get chat items.
### Converting CChatItem to WebMessage
For each `CChatItem SMDRcv (ChatItem {chatDir, meta, content, mentions, formattedText, quotedItem, reactions, file})`:
1. **Skip if:**
- `itemDeleted meta` is `Just _`
- `itemTimed meta` is `Just _`
- `content` is not `CIRcvMsgContent mc` (skip `CIRcvGroupEvent`, `CIRcvIntegrityError`, etc.)
- `mc` is `MCReport` or `MCUnknown`
2. **Extract sender:**
- `CIGroupRcv member` -> `Just (memberId member)`, collect member into profiles array
- `CIChannelRcv` -> `Nothing` (channel-forwarded message, no individual sender)
3. **Extract file info:**
- `file :: Maybe (CIFile 'MDRcv)` has `fileName :: String`, `fileSize :: Integer`
- Strip `fileSource`, `fileStatus`, `fileProtocol` (download metadata irrelevant for web)
4. **Build WebMessage:**
```haskell
WebMessage
{ sender = senderMemberId
, ts = itemTs meta
, content = mc
, formattedText = formattedText
, file = (\f -> WebFileInfo (fileName f) (fileSize f)) <$> file
, quote = quotedItem -- QuotedMsg reused directly
, mentions = mentions
, reactions = reactions
, forwarded = itemForwarded meta
, edited = itemEdited meta
}
```
5. **Collect unique senders** into `[WebMemberProfile]` from `GroupMember` records in `CIGroupRcv`.
Also include `CIGroupSnd` items (relay's own sent messages, if any - unlikely but possible for admin announcements).
### Filtering unverified domains
Before serializing, the render loop strips `groupDomain` from the `PublicGroupAccess` included in the profile when not verified:
```haskell
stripUnverifiedDomain :: Maybe UTCTime -> GroupProfile -> GroupProfile
stripUnverifiedDomain verifiedAt gp = case verifiedAt of
Just _ -> gp -- domain verified, include as-is
Nothing -> gp {publicGroup = clearDomain <$> publicGroup gp}
where
clearDomain pgp = pgp {publicGroupAccess = clearAccess <$> publicGroupAccess pgp}
clearAccess acc = acc {groupDomain = ""} -- or strip the access record entirely
```
The `group_domain_verified_at` timestamp is loaded alongside the group info. Until RSLV ships, this column is always NULL, so all domains are stripped from web export.
`domainWebPage` in CORS config is also gated on verified domain - unverified means no domain-site origin in CORS.
### Writing JSON
- Serialize `WebChannelPreview` to JSON via `Data.Aeson.encode`
- Write atomically (write to temp, rename) to `<webJsonDir>/<publicGroupId>.json`
- `publicGroupId` from `PublicGroupProfile` (base64url-encoded, existing field)
### Generating Caddy CORS config
Write a single file with Caddy `map` directive:
```caddy
map {path} {cors_origin} {
/<publicGroupId1>.json "https://owner-domain.com"
/<publicGroupId2>.json "*"
default ""
}
header /*.json Access-Control-Allow-Origin {cors_origin}
header /*.json Access-Control-Allow-Methods "GET, OPTIONS"
```
CORS origin derivation from `PublicGroupAccess`:
- `allowEmbeding = True` -> `*`
- `groupWebPage = Just url` -> extract origin from URL (+ domain site origin if `domainWebPage` and domain verified)
- `groupWebPage = Nothing, domainWebPage = True` -> domain site origin only (when domain is verified)
- No web page, no embedding, no domain page -> omit from config
After writing, run `caddy reload` if file content changed (compare hash before/after).
## Namespace Integration
`groupDomain` ships now in the profile (inside `PublicGroupAccess`). What's deferred is on-chain verification (RSLV protocol).
### What ships now
1. **`groupDomain :: Text` in `PublicGroupAccess`** - owner sets the registered domain, disseminated to all members
2. **`domainWebPage :: Bool` in `PublicGroupAccess`** - flag stored but has no effect until domain is verified
3. **Relay strips `groupDomain` from web export** - no verification means domain is cleared in JSON, no domain-site CORS origin
### What ships with RSLV
1. **RSLV protocol** - relay queries name servers via SMP proxy to verify domain ownership
2. **`domainWebPage` becomes functional** - enables domain-site hosting (e.g. `simplexnetwork.org/c/<name>`) for verified domains
3. **In-app resolution** - `#name` markdown (already parsed by namespace branch) resolves and connects
### Verification flow (relay-side)
When owner updates profile with `groupDomain`:
1. **Trigger:** Relay receives profile update on owner's connection containing `groupDomain` field
2. **Initiate:** Relay sends `RSLV <namehash>` through SMP proxy (async, on the same owner connection context)
3. **Pending state:** `group_domain_verified_at = NULL` in DB. Web export excludes domain while pending.
4. **Resolution arrives:** `NAME <record>` agent event arrives on the owner's connection (continuation bound to the connection that sent the profile update)
5. **Verify:** Check if `channelLinks` in the NAME response includes this group's `groupLink`
6. **Store result:** Set `group_domain_verified_at = <current_time>` on success, leave NULL on failure
7. **Effect:** Web render loop includes domain in JSON and enables domain-site CORS only when `group_domain_verified_at IS NOT NULL`
Re-verification: periodic (e.g. daily or on each web update cycle) to catch expired/transferred domains. Clear `group_domain_verified_at` when re-verification fails.
### What the namespace branch already provides
- `SimplexNameInfo {nameType, namespace, domain, subDomain}` in Markdown.hs
- `SimplexName` variant in `Format` ADT
- Parser for `#name` / `#name.simplex` / `:name.simplex` syntax
- Forward-compatibility alerts in Kotlin/Swift UI (shows "requires newer app" until resolution is implemented)
## UI Changes (Kotlin/Swift)
### Kotlin types
**File:** `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt`
```kotlin
@Serializable
data class PublicGroupAccess(
val groupWebPage: String? = null,
val groupDomain: String? = null,
val domainWebPage: Boolean = false,
val allowEmbeding: Boolean = false
)
// Extend existing PublicGroupProfile (currently at line 2213):
@Serializable
data class PublicGroupProfile(
val groupType: GroupType,
val groupLink: String,
val publicGroupId: String,
val publicGroupAccess: PublicGroupAccess? = null // NEW
)
@Serializable
data class RelayCapabilities(
val baseWebUrl: String? = null
)
// Extend existing GroupRelay:
@Serializable
data class GroupRelay(
...existing fields...,
val relayCap: RelayCapabilities? = null // NEW
)
```
### Owner: Channel info page
**File:** `GroupChatInfoView.kt` (around line 604-606)
After existing `ChannelLinkButton(manageGroupLink)`:
```kotlin
ChannelWebPageButton(openChannelWebPage) // owner only
```
New nav destination opens `ChannelWebPageView`.
### Owner: Channel web page screen
**File:** new `apps/multiplatform/.../views/chat/group/ChannelWebPageView.kt`
- Text field: web page URL (`groupWebPage`)
- Text field: domain (`groupDomain`)
- Toggle: allow embedding (`allowEmbeding`)
- Toggle: show on domain's page (`domainWebPage`) - stored but inert until RSLV ships
- Section: embed snippet (read-only, auto-generated from relay `baseWebUrl` values + `publicGroupId`)
- Save button -> `apiUpdateGroup` with updated `GroupProfile`
### Subscriber: Channel info page
In the top section (around line 607-614), after channel link QR:
```kotlin
val webPageUrl = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage
if (webPageUrl != null) {
WebPageLinkRow(webPageUrl) // clickable, opens browser
}
```
## Build Configuration
Web preview code compiles into the main `simplex-chat` library (not conditional). The thread only starts when `relayWebOptions` is set in `CoreChatOpts`. Mobile apps never set this.
No cabal flag needed - the thread startup is gated by `Maybe RelayWebOptions` at runtime (same pattern as `chatRelay` gating relay behavior).
## Caddy Setup (operator documentation)
Main Caddyfile (operator writes once):
```caddy
relay.example.com {
import /etc/caddy/simplex-cors.conf
handle /preview/* {
root * /var/lib/simplex/web/preview
file_server
}
}
```
Relay CLI invocation:
```
simplex-chat --relay \
--web-json-dir /var/lib/simplex/web/preview \
--web-base-url https://relay.example.com/preview \
--web-cors-file /etc/caddy/simplex-cors.conf \
--web-update-interval 300
```
## Channel Page and Embed Code
### Embed snippet (shown to owner)
The "Channel web page" screen auto-generates this from the channel's relay `baseWebUrl` values and `publicGroupId`. Owner copies it into their page:
```html
<div id="simplex-channel"
data-channel-id="<publicGroupId>"
data-relays="<baseWebUrl1>,<baseWebUrl2>">
</div>
<script src="https://simplex.chat/channel-preview.js"></script>
```
Example with real values:
```html
<div id="simplex-channel"
data-channel-id="a1b2c3d4"
data-relays="https://relay1.example.com/preview,https://relay2.example.com/preview">
</div>
<script src="https://simplex.chat/channel-preview.js"></script>
```
The script fetches `<relay>/a1b2c3d4.json`, renders the preview into the `div`. Tries relays in order, falls back on failure. The owner's domain must match the CORS origin configured by the relay (derived from `groupWebPage`), or `allowEmbeding` must be `True` for `*`.
For iframe embedding (when allowed), the snippet is simpler - just an iframe pointing to the owner's hosted channel page.
### Channel page (static JS)
Separate repo or folder. `channel-preview.js` + minimal CSS:
- Reads config from `data-` attributes on the container div
- Fetches JSON from relays with fallback (try first, fall back to second)
- Renders: channel header (name, avatar, description, subscriber count), message list (text with FormattedText markdown, link previews, file indicators, reactions, quotes)
- Join button: `simplex://` deep link on mobile, QR code on desktop
- Reuses directory page's markdown rendering approach
## Files to Create/Modify
### New files
- `src/Simplex/Chat/Web/Preview.hs` - types: `WebChannelPreview`, `WebMessage`, `WebFileInfo`, `WebMemberProfile`
- `src/Simplex/Chat/Web.hs` - render loop, JSON writing, Caddy config generation
- `apps/multiplatform/.../views/chat/group/ChannelWebPageView.kt`
- `apps/ios/Shared/Views/Chat/Group/ChannelWebPageView.swift`
- Migration files (SQLite + Postgres): `group_web_page`, `group_domain`, `domain_web_page`, `allow_embedding`, `group_domain_verified_at` in group_profiles; `base_web_url` in group_relays
- Channel page static site (separate repo/folder)
### Modified files
- `src/Simplex/Chat/Types.hs` - `PublicGroupAccess` type, extend `PublicGroupProfile` with `publicGroupAccess`
- `src/Simplex/Chat/Protocol.hs` - `RelayCapabilities` record, extend `XGrpRelayAcpt`, add `XGrpRelayCap`
- `src/Simplex/Chat/Options.hs` - `RelayWebOptions` record, `relayWebOptions :: Maybe RelayWebOptions` in `CoreChatOpts`
- `src/Simplex/Chat/Core.hs` - start web preview thread in `runSimplexChat`
- `src/Simplex/Chat/Operators.hs` - `baseWebUrl` in `GroupRelay`
- `src/Simplex/Chat/Store/Groups.hs` - read/write `PublicGroupAccess` columns; `getWebPublishGroups`
- `src/Simplex/Chat/Store/Shared.hs` - `toPublicGroupAccess`, extend `toPublicGroupProfile` and `GroupInfoRow`
- `src/Simplex/Chat/Library/Subscriber.hs` - handle `RelayCapabilities` in `XGrpRelayAcpt` and `XGrpRelayCap`
- `apps/multiplatform/.../model/ChatModel.kt` - `PublicGroupAccess`, `RelayCapabilities`, `PublicGroupProfile.publicGroupAccess`, `GroupRelay.relayCap`
- `apps/multiplatform/.../views/chat/group/GroupChatInfoView.kt` - nav link for web page
- `simplex-chat.cabal` - add `Simplex.Chat.Web.Preview`, `Simplex.Chat.Web` to exposed-modules
## Implementation Order
1. **Data model** - `PublicGroupAccess` in `PublicGroupProfile`, migrations (separate columns), store functions
2. **Protocol** - `RelayCapabilities`, extend `XGrpRelayAcpt`, add `XGrpRelayCap`, handlers in Subscriber.hs
3. **CLI options** - `RelayWebOptions` record, `relayWebOptions` field in `CoreChatOpts`
4. **Web types** - `WebChannelPreview`, `WebMessage`, etc. in new module
5. **Render loop** - thread startup in Core.hs, periodic JSON generation, Caddy config
6. **UI (owner)** - "Channel web page" settings screen
7. **UI (subscriber)** - web page link in channel info
8. **Channel page** - static HTML+JS template
9. **Documentation** - operator setup guide
## Verification
1. **Build**: `cabal build simplex-chat` with new modules compiles
2. **Unit test**: serialize `WebChannelPreview` with sample data, verify JSON matches expected structure
3. **Integration test**: create channel with `publicGroupAccess` set, run relay with `--web-json-dir`, verify JSON file appears at correct path with correct content
4. **CORS test**: verify generated config produces correct `Access-Control-Allow-Origin` for configured domains
5. **UI test**: owner can set web page URL and domain, see embed snippet; subscriber sees clickable link
6. **Channel page test**: serve static page locally against relay's JSON, verify rendering
7. **Domain stripping test**: set `groupDomain` on a channel, verify it is stripped from web export JSON (unverified, `group_domain_verified_at IS NULL`)
-111
View File
@@ -1,111 +0,0 @@
# CLI terminal: event loss root cause analysis
## Two distinct problems
### Problem 1: Paste — TMVar capacity-1 bottleneck
When copy-pasting text, the capacity-1 `TMVar` event channel between the keyboard input reader and the consumer loop throttles stdin reading to terminal redraw speed.
**Root cause:** `events <- liftIO newEmptyTMVarIO` (`Platform.hsc:64`). Producer blocks on `putTMVar` after each event until consumer finishes redrawing. Consumer does a full terminal redraw per event (`Input.hs:161`).
**Fix:** Replace `TMVar` with `TQueue` in `Platform.hsc` (6 line changes on POSIX, matching changes on Windows). Decouples producer from consumer — stdin is drained at full speed regardless of redraw speed.
See previous analysis in git history for full details on this issue.
---
### Problem 2: Heavy load — `outputQ` backpressure blocks `agentSubscriber`
When the CLI is used as a heavy client (e.g., 1M connections), incoming chat events overwhelm the terminal display, causing cascading backpressure that blocks message acknowledgments and stalls the entire event processing pipeline.
**This is the more severe problem.** It causes actual message loss at the protocol level, not just UI slowness.
## Root cause: bounded `outputQ` + single-threaded `agentSubscriber`
### The queue chain
```
Network (SMP/XFTP connections)
→ agent internal queues
→ subQ (TBQueue, capacity 1024) ← agent → chat boundary
→ agentSubscriber (single-threaded) ← Commands.hs:4167
→ processAgentMessage ← Subscriber.hs:109
→ toView_ → writeTBQueue outputQ ← Controller.hs:1528, BLOCKS when full
→ outputQ (TBQueue, capacity 1024) ← Chat.hs:152
→ runTerminalOutput ← Output.hs:146
→ printToTerminal (acquires termLock) ← Output.hs:298-303
→ terminal I/O (slow)
```
All queues are bounded `TBQueue` with default capacity 1024 (`Options.hs:226`). All writes use `writeTBQueue` which **blocks when full** — no events are dropped within the application, but backpressure cascades upstream.
### The blocking chain under heavy load
1. **Terminal I/O is the bottleneck.** `runTerminalOutput` (`Output.hs:146`) reads one event at a time from `outputQ`, acquires `termLock`, prints the message + redraws input, releases lock. Each iteration involves ANSI escape sequences, cursor manipulation, and `flush` syscalls. Throughput: ~hundreds of events/sec at best.
2. **`outputQ` fills up.** With 1M connections generating events, the arrival rate far exceeds terminal display speed. The 1024-element TBQueue fills in seconds.
3. **`toView_` blocks.** `Controller.hs:1528`: `writeTBQueue localQ (Nothing, event)` blocks when the queue is full. This call happens inside `processAgentMessage``processAgentMessageConn`, which runs within the `agentSubscriber` loop.
4. **`agentSubscriber` blocks — head-of-line blocking.** `Commands.hs:4164-4167`:
```haskell
agentSubscriber = do
q <- asks $ subQ . smpAgent
forever (atomically (readTBQueue q) >>= process)
```
Single-threaded. When `process` blocks on `toView_`, ALL events for ALL connections queue up behind it. Events for 1M other connections — including time-critical ACKs, keepalives, and handshakes — are stuck.
5. **ACKs are never sent.** The message receive path (`Subscriber.hs:1537-1540`) calls `toView` BEFORE `ackMsg`:
```haskell
-- Inside withAckMessage's action:
saveRcvChatItem' ... -- save to DB (succeeds)
toView $ CEvtNewChatItems ... -- BLOCKS here (outputQ full)
-- returns (withRcpt, shouldDelConns)
-- After action returns (Subscriber.hs:1396-1397):
ackMsg msgMeta ... -- NEVER REACHED while toView blocks
```
The developers explicitly acknowledge this at `Subscriber.hs:122-123`:
> *without ACK the message delivery will be stuck*
6. **`subQ` fills up.** The agent can't deliver events to `subQ` (also capacity 1024) because `agentSubscriber` isn't reading. Agent-level processing stalls.
7. **Network-level failure.** Connections time out due to unprocessed keepalives and unacknowledged messages. Messages are lost at the protocol level.
### `termLock` contention worsens the bottleneck
`termLock` (`Output.hs:55`) is a `TMVar ()` mutex shared between:
- **Output thread** (`runTerminalOutput``printToTerminal`): acquires lock for each displayed message
- **Input thread** (`receiveFromTTY``updateInput`): acquires lock after each keystroke
- **Live prompt thread** (`blinkLivePrompt``updateInputView`): acquires lock every 1 second
Under heavy load, the output thread dominates the lock (constant stream of messages). The input thread is starved — user keystrokes are delayed. This also slows the output thread itself (lock contention overhead).
Note: `withTermLock` (`Output.hs:138-142`) is not exception-safe — no `bracket`/`finally`. If the action throws, the lock leaks and all threads deadlock.
### Error reporting also blocks
When `processAgentMessage` encounters an error, the error handler (`Commands.hs:4179`) calls `eToView'``toView_``writeTBQueue outputQ`. If `outputQ` is already full, even error reporting blocks. There is no escape path.
## Impact summary
| Load level | `outputQ` state | Effect |
|---|---|---|
| Light (few connections) | Nearly empty | No issues |
| Moderate (hundreds) | Partially filled | Occasional display lag |
| Heavy (thousands+) | Full (1024) | `toView_` blocks → `agentSubscriber` blocks → head-of-line blocking for ALL connections → ACKs delayed → message delivery stuck |
| Extreme (1M connections) | Permanently full | Cascading failure: all event processing stops, connections time out, messages lost at protocol level |
## Fix
The core fix: **`toView_` must never block the event processing pipeline on terminal display.**
Options (in order of simplicity):
1. **Make `outputQ` unbounded** — replace `TBQueue` with `TQueue` in `Chat.hs:152`. `writeTQueue` never blocks. Events accumulate in memory under heavy load but the event processing pipeline (including ACKs) is never stalled. Tradeoff: unbounded memory growth under sustained heavy load.
2. **Non-blocking write with drop** — use `tryWriteTBQueue` in `toView_`. When `outputQ` is full, drop the display event (or a coalesced summary). ACKs and network processing proceed unblocked. Tradeoff: some events not displayed, but none lost at protocol level.
3. **Separate ACK from display** — restructure `withAckMessage` to send ACK immediately after DB save, before `toView`. This decouples protocol correctness from display. `toView` can still block, but ACKs are always timely. Tradeoff: requires careful restructuring of the message processing path.
4. **Increase queue capacity** — increase `tbqSize` from 1024 to a larger value. Delays the problem but doesn't fix it. Under sustained heavy load, any finite queue eventually fills.
+2
View File
@@ -133,6 +133,7 @@ library
Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries
Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at
Simplex.Chat.Store.Postgres.Migrations.M20260514_relay_request_group_link_index
Simplex.Chat.Store.Postgres.Migrations.M20260515_public_group_access
Simplex.Chat.Store.Postgres.Migrations.M20260529_delivery_job_senders
Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services
Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at
@@ -291,6 +292,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries
Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at
Simplex.Chat.Store.SQLite.Migrations.M20260514_relay_request_group_link_index
Simplex.Chat.Store.SQLite.Migrations.M20260515_public_group_access
Simplex.Chat.Store.SQLite.Migrations.M20260529_delivery_job_senders
Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services
Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at
+3 -3
View File
@@ -2533,7 +2533,8 @@ processChatCommand vr nm = \case
-- generate owner key, OwnerAuth signed by root key
memberId <- MemberId <$> liftIO (encodedRandomBytes gVar 12)
(memberPrivKey, ownerAuth) <- liftIO $ SL.newOwnerAuth gVar (unMemberId memberId) rootPrivKey
let groupProfile' = (groupProfile :: GroupProfile) {publicGroup = Just PublicGroupProfile {groupType = GTChannel, groupLink = sLnk, publicGroupId = B64UrlByteString entityId}}
-- TODO [channel web] pass publicGroupAccess from owner's profile
let groupProfile' = (groupProfile :: GroupProfile) {publicGroup = Just PublicGroupProfile {groupType = GTChannel, groupLink = sLnk, publicGroupId = B64UrlByteString entityId, publicGroupAccess = Nothing}}
userData = encodeShortLinkData $ GroupShortLinkData {groupProfile = groupProfile', publicGroupData = Just (PublicGroupData 1)}
userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData}
-- create connection with prepared link (single network call)
@@ -2651,8 +2652,7 @@ processChatCommand vr nm = \case
Nothing -> throwChatError $ CEContactNotActive ct
APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do
(gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId
-- TODO check that user's role is > role, possibly restrict role to only observer and member
assertUserGroupRole gInfo GRModerator
assertUserGroupRole gInfo $ max GRModerator role
case memberStatus m of
GSMemPendingApproval | memberCategory m == GCInviteeMember -> do -- only host can approve
let GroupInfo {groupProfile = GroupProfile {memberAdmission}} = gInfo
+51 -28
View File
@@ -919,13 +919,15 @@ acceptContactRequestAsync
let profileToSend = userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
(cmdId, acId) <- agentAcceptContactAsync user True cReqInvId (XInfo profileToSend) subMode cReqPQSup chatV
(cmdId, acId) <- prepareAgentAccept user True cReqInvId cReqPQSup
currentTs <- liftIO getCurrentTime
withStore $ \db -> do
ct' <- withStore $ \db -> do
forM_ xContactId $ \xcId -> liftIO $ setContactAcceptedXContactId db ct xcId
Connection {connId} <- liftIO $ createAcceptedContactConn db user (Just uclId) contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs
liftIO $ setCommandConnId db user cmdId connId
getContact db vr user contactId
agentAcceptContactAsync user cmdId acId True cReqInvId (XInfo profileToSend) cReqPQSup chatV subMode
pure ct'
acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> CM GroupMember
acceptGroupJoinRequestAsync
@@ -963,10 +965,11 @@ acceptGroupJoinRequestAsync
subMode <- chatReadVar subscriptionMode
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV
withStore $ \db -> do
liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode
getGroupMemberById db vr user groupMemberId
(cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff
withStore $ \db ->
liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode
agentAcceptContactAsync user cmdId acId True cReqInvId msg PQSupportOff chatV subMode
withStore $ \db -> getGroupMemberById db vr user groupMemberId
acceptGroupJoinSendRejectAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupRejectionReason -> CM GroupMember
acceptGroupJoinSendRejectAsync
@@ -993,10 +996,11 @@ acceptGroupJoinSendRejectAsync
subMode <- chatReadVar subscriptionMode
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
connIds <- agentAcceptContactAsync user False cReqInvId msg subMode PQSupportOff chatV
withStore $ \db -> do
liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode
getGroupMemberById db vr user groupMemberId
(cmdId, acId) <- prepareAgentAccept user False cReqInvId PQSupportOff
withStore $ \db ->
liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode
agentAcceptContactAsync user cmdId acId False cReqInvId msg PQSupportOff chatV subMode
withStore $ \db -> getGroupMemberById db vr user groupMemberId
acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember)
acceptBusinessJoinRequestAsync
@@ -1025,10 +1029,11 @@ acceptBusinessJoinRequestAsync
}
subMode <- chatReadVar subscriptionMode
let chatV = vr `peerConnChatVersion` cReqChatVRange
connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV
(cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff
withStore' $ \db -> do
forM_ xContactId $ \xcId -> setBusinessChatAcceptedXContactId db gInfo xcId
createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode
createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode
agentAcceptContactAsync user cmdId acId True cReqInvId msg PQSupportOff chatV subMode
let cd = CDGroupSnd gInfo Nothing
-- TODO [short links] move to profileContactRequest?
createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing
@@ -1045,16 +1050,19 @@ acceptRelayJoinRequestAsync
cReqInvId
cReqChatVRange
relayLink = do
let msg = XGrpRelayAcpt relayLink
-- TODO [channel web] derive RelayCapabilities from relay config (RelayWebOptions)
let msg = XGrpRelayAcpt relayLink defaultRelayCapabilities
subMode <- chatReadVar subscriptionMode
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV
withStore $ \db -> do
liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode
(cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff
r <- withStore $ \db -> do
liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode
gInfo' <- liftIO $ updateRelayOwnStatusFromTo db gInfo RSInvited RSAccepted
ownerMember' <- getGroupMemberById db vr user groupMemberId
pure (gInfo', ownerMember')
agentAcceptContactAsync user cmdId acId True cReqInvId msg PQSupportOff chatV subMode
pure r
rejectRelayInvitationAsync
:: User
@@ -1074,9 +1082,10 @@ rejectRelayInvitationAsync user uclId vr groupRelayInv invId reqChatVRange initi
subMode <- chatReadVar subscriptionMode
chatVR <- chatVersionRange
let chatV = chatVR `peerConnChatVersion` reqChatVRange
connIds <- agentAcceptContactAsync user False invId msg subMode PQSupportOff chatV
(cmdId, acId) <- prepareAgentAccept user False invId PQSupportOff
withStore' $ \db ->
createJoiningMemberConnection db user uclId connIds chatV reqChatVRange groupMemberId subMode
createJoiningMemberConnection db user uclId (cmdId, acId) chatV reqChatVRange groupMemberId subMode
agentAcceptContactAsync user cmdId acId False invId msg PQSupportOff chatV subMode
businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile
businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences =
@@ -2478,18 +2487,28 @@ msgContentHasLink mc ft_ = case msgContentTag mc of
MCLink_ -> True
_ -> maybe False hasLinks ft_
createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId)
createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do
prepareAgentCreation :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> CM (CommandId, ConnId)
prepareAgentCreation user cmdFunction enableNtfs cMode = do
cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction
connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode IKPQOff subMode
connId <- withAgent $ \a -> prepareConnectionToCreate a (aUserId user) enableNtfs cMode PQSupportOff
pure (cmdId, connId)
joinAgentConnectionAsync :: User -> Maybe Connection -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM (CommandId, ConnId)
joinAgentConnectionAsync user conn_ enableNtfs cReqUri cInfo subMode = do
createAgentConnectionAsync :: ConnectionModeI c => User -> CommandId -> ConnId -> Bool -> SConnectionMode c -> SubscriptionMode -> CM ()
createAgentConnectionAsync user cmdId connId enableNtfs cMode subMode =
withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) connId enableNtfs cMode IKPQOff subMode
prepareAgentJoin :: User -> Maybe Connection -> Bool -> ConnectionRequestUri c -> CM (CommandId, ConnId)
prepareAgentJoin user conn_ enableNtfs cReqUri = do
cmdId <- withStore' $ \db -> createCommand db user (dbConnId <$> conn_) CFJoinConn
connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) (aConnId <$> conn_) enableNtfs cReqUri cInfo PQSupportOff subMode
connId <- case conn_ of
Just conn -> pure $ aConnId conn
Nothing -> withAgent $ \a -> prepareConnectionToJoin a (aUserId user) enableNtfs cReqUri PQSupportOff
pure (cmdId, connId)
joinAgentConnectionAsync :: ConnectionModeI c => User -> CommandId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM ()
joinAgentConnectionAsync user cmdId updateConn connId enableNtfs cReqUri cInfo subMode =
withAgent $ \a -> joinConnectionAsync a (aCorrId cmdId) updateConn connId enableNtfs cReqUri cInfo PQSupportOff subMode
allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM ()
allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn
@@ -2497,13 +2516,17 @@ allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersi
withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm
withStore' $ \db -> updateConnectionStatus db conn ConnAccepted
agentAcceptContactAsync :: MsgEncodingI e => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> PQSupport -> VersionChat -> CM (CommandId, ConnId)
agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do
prepareAgentAccept :: User -> Bool -> InvitationId -> PQSupport -> CM (CommandId, ConnId)
prepareAgentAccept user enableNtfs invId pqSup = do
cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact
dm <- encodeConnInfoPQ pqSup chatV msg
connId <- withAgent $ \a -> acceptContactAsync a (aUserId user) (aCorrId cmdId) enableNtfs invId dm pqSup subMode
connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) enableNtfs invId pqSup
pure (cmdId, connId)
agentAcceptContactAsync :: MsgEncodingI e => User -> CommandId -> ConnId -> Bool -> InvitationId -> ChatMsgEvent e -> PQSupport -> VersionChat -> SubscriptionMode -> CM ()
agentAcceptContactAsync user cmdId connId enableNtfs invId msg pqSup chatV subMode = do
dm <- encodeConnInfoPQ pqSup chatV msg
withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) connId enableNtfs invId dm pqSup subMode
deleteAgentConnectionAsync :: ConnId -> CM ()
deleteAgentConnectionAsync acId = deleteAgentConnectionAsync' acId False
{-# INLINE deleteAgentConnectionAsync #-}
+34 -14
View File
@@ -627,9 +627,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do
groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId
subMode <- chatReadVar subscriptionMode
groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode
groupConnIds <- prepareAgentCreation user CFCreateConnGrpInv True SCMInvitation
gVar <- asks random
withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds connChatVersion peerChatVRange subMode
uncurry (createAgentConnectionAsync user) groupConnIds True SCMInvitation subMode
-- TODO REMOVE LEGACY ^^^
SENT msgId proxy -> do
void $ continueSending connEntity conn
@@ -774,9 +775,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
allowAgentConnectionAsync user conn' confId XOk
| otherwise -> messageError "x.grp.acpt: memberId is different from expected"
XGrpRelayAcpt relayLink
XGrpRelayAcpt relayLink relayCap
| memberRole' membership == GROwner && isRelay m -> do
withStore' $ \db -> setRelayLinkConfId db m confId relayLink
withStore' $ \db -> do
setRelayLinkConfId db m confId relayLink
updateRelayCapabilities db m relayCap
void $ getAgentConnShortLinkAsync user CFGetRelayDataAccept (Just conn') relayLink
| otherwise -> messageError "x.grp.relay.acpt: only owner can add relay"
XGrpRelayReject reason
@@ -1045,6 +1048,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
XGrpLinkMem p -> Nothing <$ xGrpLinkMem gInfo' m'' conn' p
XGrpLinkAcpt acceptance role memberId -> Nothing <$ xGrpLinkAcpt gInfo' m'' acceptance role memberId msg brokerTs
XGrpRelayNew rl -> fmap ctx <$> xGrpRelayNew gInfo' m'' rl
XGrpRelayCap relayCap
| memberRole' membership == GROwner && isRelay m'' ->
Nothing <$ withStore' (\db -> updateRelayCapabilities db m'' relayCap)
| otherwise -> Nothing <$ messageWarning "x.grp.relay.cap: only owner should receive relay capabilities"
XGrpMemNew memInfo msgScope -> fmap ctx <$> xGrpMemNew gInfo' m'' memInfo msgScope msg brokerTs
XGrpMemIntro memInfo memRestrictions_ -> Nothing <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_
XGrpMemInv memId introInv -> Nothing <$ xGrpMemInv gInfo' m'' memId introInv
@@ -1193,7 +1200,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
Nothing -> throwChatError $ CEInternalError "no group keys for channel membership"
dm <- encodeConnInfo $ XMember profileToSend membershipMemId (MemberKey memberPubKey)
subMode <- chatReadVar subscriptionMode
void $ joinAgentConnectionAsync user (Just conn) True cReq dm subMode
(cmdId, connId) <- prepareAgentJoin user (Just conn) True cReq
joinAgentConnectionAsync user cmdId True connId True cReq dm subMode
CFGetRelayDataAccept -> do
let GroupMember {memberId = MemberId expectedMemberId} = m
if linkEntityId == Just expectedMemberId
@@ -1577,10 +1585,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
subMode <- chatReadVar subscriptionMode
chatVR <- chatVersionRange
let chatV = chatVR `peerConnChatVersion` chatVRange
(cmdId, acId) <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV
(cmdId, acId) <- prepareAgentAccept user True invId PQSupportOff
withStore $ \db -> do
Connection {connId = testCId} <- createRelayTestConnection db vr user acId ConnAccepted chatV subMode
liftIO $ setCommandConnId db user cmdId testCId
agentAcceptContactAsync user cmdId acId True invId msg PQSupportOff chatV subMode
-- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays
-- TODO [relays] relay: TBC communicate rejection when memberId already exists (currently checked in createJoiningMember)
memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Profile -> MemberId -> MemberKey -> CM ()
@@ -2501,12 +2510,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
then do
subMode <- chatReadVar subscriptionMode
dm <- encodeConnInfo $ XGrpAcpt membershipMemId
connIds <- joinAgentConnectionAsync user Nothing True connRequest dm subMode
connIds@(cmdId, acId) <- prepareAgentJoin user Nothing True connRequest
withStore' $ \db -> do
setViaGroupLinkUri db groupId connId
createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode
updateGroupMemberStatusById db userId hostId GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted
joinAgentConnectionAsync user cmdId False acId True connRequest dm subMode
toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct)
else do
let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole
@@ -2609,6 +2619,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xGrpLinkAcpt :: GroupInfo -> GroupMember -> GroupAcceptance -> GroupMemberRole -> MemberId -> RcvMessage -> UTCTime -> CM ()
xGrpLinkAcpt gInfo@GroupInfo {membership} m acceptance role memberId msg brokerTs
| memberRole' m < GRModerator || memberRole' m < role =
messageError "x.grp.link.acpt with insufficient member permissions"
| sameMemberId memberId membership = processUserAccepted
| otherwise =
withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case
@@ -3039,15 +3051,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
| maxVersion mcvr >= groupDirectInvVersion -> do
subMode <- chatReadVar subscriptionMode
-- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second
groupConnIds <- createConn subMode
groupConnIds <- prepareConn
let chatV = maybe (minVersion vr) (\peerVR -> vr `peerConnChatVersion` fromChatVRange peerVR) memChatVRange
void $ withStore $ \db -> do
reMember <- createIntroReMember db user gInfo memInfo memRestrictions
createIntroReMemberConn db user m reMember chatV memInfo groupConnIds subMode
uncurry (createAgentConnectionAsync user) groupConnIds (chatHasNtfs chatSettings) SCMInvitation subMode
| otherwise -> messageError "x.grp.mem.intro: member chat version range incompatible"
_ -> messageError "x.grp.mem.intro can be only sent by host member"
where
createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation subMode
prepareConn = prepareAgentCreation user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation
sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> CM ()
sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do
@@ -3091,12 +3104,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo
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
directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user Nothing True dcr dm subMode
let enableNtfsGrp = chatHasNtfs chatSettings
groupConnIds@(gCmdId, gAcId) <- prepareAgentJoin user Nothing enableNtfsGrp groupConnReq
directConnIds <- forM directConnReq $ \dcr -> prepareAgentJoin user Nothing True dcr
let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo
mcvr = maybe chatInitialVRange fromChatVRange memChatVRange
chatV = vr `peerConnChatVersion` mcvr
withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode
joinAgentConnectionAsync user gCmdId False gAcId enableNtfsGrp groupConnReq dm subMode
forM_ ((,) <$> directConnIds <*> directConnReq) $ \((dCmdId, dAcId), dcr) ->
joinAgentConnectionAsync user dCmdId False dAcId True dcr dm subMode
xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg@RcvMessage {msgSigned} brokerTs
@@ -3351,11 +3368,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
}
joinExistingContact subMode mCt@Contact {contactId = mContactId}
| autoAcceptMemberContacts user = do
(cmdId, acId) <- joinConn subMode
(cmdId, acId) <- prepareJoinMemberContact
mCt' <- withStore $ \db -> do
updateMemberContactInvited db user mCt groupDirectInv
void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode
getContact db vr user mContactId
joinMemberContactAsync cmdId acId subMode
securityCodeChanged mCt'
createItems mCt' m
| otherwise = do
@@ -3369,13 +3387,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
createItems mCt' m
createNewContact subMode
| autoAcceptMemberContacts user = do
(cmdId, acId) <- joinConn subMode
(cmdId, acId) <- prepareJoinMemberContact
-- [incognito] reuse membership incognito profile
(mCt, m') <- withStore $ \db -> do
(mContactId, m') <- liftIO $ createMemberContactInvited db user g m groupDirectInv
void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode
mCt <- getContact db vr user mContactId
pure (mCt, m')
joinMemberContactAsync cmdId acId subMode
createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart)
createItems mCt m'
| otherwise = do
@@ -3388,12 +3407,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart)
createInternalChatItem user (CDDirectRcv mCt) (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing
createItems mCt m'
joinConn subMode = do
prepareJoinMemberContact = prepareAgentJoin user Nothing True connReq
joinMemberContactAsync cmdId acId subMode = do
-- [incognito] send membership incognito profile
let p = userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing True
-- TODO PQ should negotitate contact connection with PQSupportOn? (use encodeConnInfoPQ)
dm <- encodeConnInfo $ XInfo p
joinAgentConnectionAsync user Nothing True connReq dm subMode
joinAgentConnectionAsync user cmdId False acId True connReq dm subMode
createItems mCt' m' = do
(g', m'', scopeInfo) <- mkGroupChatScope g m'
createInternalChatItem user (CDGroupRcv g' scopeInfo m'') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing
+3 -2
View File
@@ -46,7 +46,7 @@ import Data.Time (addUTCTime)
import Data.Time.Clock (UTCTime, nominalDay)
import Language.Haskell.TH.Syntax (lift)
import Simplex.Chat.Operators.Conditions
import Simplex.Chat.Protocol (RelayProfile (..))
import Simplex.Chat.Protocol (RelayCapabilities (..), RelayProfile (..))
import Simplex.Chat.Types (ShortLinkContact, User)
import Simplex.Chat.Types.Shared (RelayStatus)
import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles)
@@ -280,7 +280,8 @@ data GroupRelay = GroupRelay
groupMemberId :: Int64,
userChatRelay :: UserChatRelay,
relayStatus :: RelayStatus,
relayLink :: Maybe ShortLinkContact
relayLink :: Maybe ShortLinkContact,
relayCap :: RelayCapabilities
}
deriving (Eq, Show)
+25 -4
View File
@@ -262,6 +262,14 @@ data LinkContent = LCPage | LCImage | LCVideo {duration :: Maybe Int} | LCUnknow
data ReportReason = RRSpam | RRContent | RRCommunity | RRProfile | RROther | RRUnknown Text
deriving (Eq, Show)
data RelayCapabilities = RelayCapabilities
{ baseWebUrl :: Maybe Text
}
deriving (Eq, Show)
defaultRelayCapabilities :: RelayCapabilities
defaultRelayCapabilities = RelayCapabilities {baseWebUrl = Nothing}
$(pure [])
instance FromJSON LinkContent where
@@ -281,6 +289,12 @@ instance ToJSON LinkContent where
$(JQ.deriveJSON defaultJSON ''LinkPreview)
$(JQ.deriveToJSON defaultJSON ''RelayCapabilities)
instance FromJSON RelayCapabilities where
parseJSON = $(JQ.mkParseJSON defaultJSON ''RelayCapabilities)
omittedField = Just defaultRelayCapabilities
instance StrEncoding ReportReason where
strEncode = \case
RRSpam -> "spam"
@@ -441,10 +455,11 @@ data ChatMsgEvent (e :: MsgEncoding) where
XGrpLinkMem :: Profile -> ChatMsgEvent 'Json
XGrpLinkAcpt :: GroupAcceptance -> GroupMemberRole -> MemberId -> ChatMsgEvent 'Json
XGrpRelayInv :: GroupRelayInvitation -> ChatMsgEvent 'Json
XGrpRelayAcpt :: ShortLinkContact -> ChatMsgEvent 'Json
XGrpRelayAcpt :: ShortLinkContact -> RelayCapabilities -> ChatMsgEvent 'Json
XGrpRelayTest :: ByteString -> Maybe ByteString -> ChatMsgEvent 'Json
XGrpRelayNew :: ShortLinkContact -> ChatMsgEvent 'Json
XGrpRelayReject :: RelayRejectionReason -> ChatMsgEvent 'Json
XGrpRelayCap :: RelayCapabilities -> ChatMsgEvent 'Json
XGrpMemNew :: MemberInfo -> Maybe MsgScope -> ChatMsgEvent 'Json
XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json
XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json
@@ -991,6 +1006,7 @@ data CMEventTag (e :: MsgEncoding) where
XGrpRelayTest_ :: CMEventTag 'Json
XGrpRelayNew_ :: CMEventTag 'Json
XGrpRelayReject_ :: CMEventTag 'Json
XGrpRelayCap_ :: CMEventTag 'Json
XGrpMemNew_ :: CMEventTag 'Json
XGrpMemIntro_ :: CMEventTag 'Json
XGrpMemInv_ :: CMEventTag 'Json
@@ -1050,6 +1066,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where
XGrpRelayTest_ -> "x.grp.relay.test"
XGrpRelayNew_ -> "x.grp.relay.new"
XGrpRelayReject_ -> "x.grp.relay.reject"
XGrpRelayCap_ -> "x.grp.relay.cap"
XGrpMemNew_ -> "x.grp.mem.new"
XGrpMemIntro_ -> "x.grp.mem.intro"
XGrpMemInv_ -> "x.grp.mem.inv"
@@ -1110,6 +1127,7 @@ instance StrEncoding ACMEventTag where
"x.grp.relay.test" -> XGrpRelayTest_
"x.grp.relay.new" -> XGrpRelayNew_
"x.grp.relay.reject" -> XGrpRelayReject_
"x.grp.relay.cap" -> XGrpRelayCap_
"x.grp.mem.new" -> XGrpMemNew_
"x.grp.mem.intro" -> XGrpMemIntro_
"x.grp.mem.inv" -> XGrpMemInv_
@@ -1162,10 +1180,11 @@ toCMEventTag msg = case msg of
XGrpLinkMem _ -> XGrpLinkMem_
XGrpLinkAcpt {} -> XGrpLinkAcpt_
XGrpRelayInv _ -> XGrpRelayInv_
XGrpRelayAcpt _ -> XGrpRelayAcpt_
XGrpRelayAcpt {} -> XGrpRelayAcpt_
XGrpRelayTest {} -> XGrpRelayTest_
XGrpRelayNew _ -> XGrpRelayNew_
XGrpRelayReject _ -> XGrpRelayReject_
XGrpRelayCap _ -> XGrpRelayCap_
XGrpMemNew {} -> XGrpMemNew_
XGrpMemIntro _ _ -> XGrpMemIntro_
XGrpMemInv _ _ -> XGrpMemInv_
@@ -1316,7 +1335,8 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
XGrpLinkMem_ -> XGrpLinkMem <$> p "profile"
XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "acceptance" <*> p "role" <*> p "memberId"
XGrpRelayInv_ -> XGrpRelayInv <$> p "groupRelayInvitation"
XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink"
XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink" <*> (fromMaybe defaultRelayCapabilities <$> opt "relayCap")
XGrpRelayCap_ -> XGrpRelayCap <$> p "relayCap"
XGrpRelayTest_ -> do
B64UrlByteString challenge <- p "challenge"
sig_ <- fmap (\(B64UrlByteString s) -> s) <$> opt "signature"
@@ -1388,7 +1408,8 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en
XGrpLinkMem profile -> o ["profile" .= profile]
XGrpLinkAcpt acceptance role memberId -> o ["acceptance" .= acceptance, "role" .= role, "memberId" .= memberId]
XGrpRelayInv groupRelayInv -> o ["groupRelayInvitation" .= groupRelayInv]
XGrpRelayAcpt relayLink -> o ["relayLink" .= relayLink]
XGrpRelayAcpt relayLink relayCap -> o ["relayLink" .= relayLink, "relayCap" .= relayCap]
XGrpRelayCap relayCap -> o ["relayCap" .= relayCap]
XGrpRelayTest challenge sig_ -> o $
("signature" .=? (B64UrlByteString <$> sig_))
["challenge" .= B64UrlByteString challenge]
+1
View File
@@ -139,6 +139,7 @@ getConnectionEntity db vr 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,
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,
+30 -11
View File
@@ -91,6 +91,7 @@ module Simplex.Chat.Store.Groups
updateRelayStatusFromTo,
setRelayLinkAccepted,
setRelayLinkConfId,
updateRelayCapabilities,
getRelayConfId,
updateRelayMemberData,
setGroupInProgressDone,
@@ -370,10 +371,11 @@ createNewGroup db vr 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,
user_id, preferences, member_admission, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_)
((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup
:. (userId, groupPreferences, memberAdmission, currentTs, currentTs))
profileId <- insertedRowId db
DB.execute
@@ -871,10 +873,11 @@ 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,
user_id, preferences, member_admission, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_)
((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup
:. (userId, groupPreferences, memberAdmission, currentTs, currentTs))
profileId <- insertedRowId db
DB.execute
@@ -1362,15 +1365,16 @@ groupRelayQuery =
[sql|
SELECT gr.group_relay_id, gr.group_member_id,
cr.chat_relay_id, cr.address, cr.display_name, cr.full_name, cr.short_descr, cr.image, cr.domains, cr.preset, cr.tested, cr.enabled, cr.deleted,
gr.relay_status, gr.relay_link
gr.relay_status, gr.relay_link, gr.base_web_url
FROM group_relays gr
JOIN chat_relays cr ON cr.chat_relay_id = gr.chat_relay_id
|]
toGroupRelay :: (Int64, GroupMemberId, DBEntityId, ShortLinkContact, Text, Text, Maybe Text, Maybe ImageData, Text, BoolInt) :. (Maybe BoolInt, BoolInt, BoolInt, RelayStatus, Maybe ShortLinkContact) -> GroupRelay
toGroupRelay ((groupRelayId, groupMemberId, chatRelayId, address, displayName, fullName, shortDescr, image, domains, BI preset) :. (tested, BI enabled, BI deleted, relayStatus, relayLink)) =
toGroupRelay :: (Int64, GroupMemberId, DBEntityId, ShortLinkContact, Text, Text, Maybe Text, Maybe ImageData, Text, BoolInt) :. (Maybe BoolInt, BoolInt, BoolInt, RelayStatus, Maybe ShortLinkContact, Maybe Text) -> GroupRelay
toGroupRelay ((groupRelayId, groupMemberId, chatRelayId, address, displayName, fullName, shortDescr, image, domains, BI preset) :. (tested, BI enabled, BI deleted, relayStatus, relayLink, baseWebUrl)) =
let userChatRelay = UserChatRelay {chatRelayId, address, relayProfile = toRelayProfile (displayName, fullName, shortDescr, image), domains = T.splitOn "," domains, preset, tested = unBI <$> tested, enabled, deleted}
in GroupRelay {groupRelayId, groupMemberId, userChatRelay, relayStatus, relayLink}
relayCap = RelayCapabilities {baseWebUrl}
in GroupRelay {groupRelayId, groupMemberId, userChatRelay, relayStatus, relayLink, relayCap}
createRelayForOwner :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupInfo -> UserChatRelay -> ExceptT StoreError IO GroupMember
createRelayForOwner db vr gVar user@User {userId, userContactId} GroupInfo {groupId, membership} UserChatRelay {relayProfile = RelayProfile {displayName}} = do
@@ -1510,6 +1514,18 @@ setRelayLinkConfId db m confId relayLink = do
|]
(relayLink, currentTs, groupMemberId' m)
updateRelayCapabilities :: DB.Connection -> GroupMember -> RelayCapabilities -> IO ()
updateRelayCapabilities db m RelayCapabilities {baseWebUrl} = do
currentTs <- getCurrentTime
DB.execute
db
[sql|
UPDATE group_relays
SET base_web_url = ?, updated_at = ?
WHERE group_member_id = ?
|]
(baseWebUrl, currentTs, groupMemberId' m)
getRelayConfId :: DB.Connection -> GroupMember -> ExceptT StoreError IO ConfirmationId
getRelayConfId db m =
ExceptT . firstRow fromOnly (SEGroupRelayNotFoundByMemberId $ groupMemberId' m) $
@@ -2358,6 +2374,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 = ?,
preferences = ?, member_admission = ?, updated_at = ?
WHERE group_profile_id IN (
SELECT group_profile_id
@@ -2365,7 +2382,7 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName,
WHERE user_id = ? AND group_id = ?
)
|]
((newName, fullName, shortDescr, description, image, groupType_, groupLink_) :. (groupPreferences, memberAdmission, currentTs, userId, groupId))
((newName, fullName, shortDescr, description, image, groupType_, groupLink_) :. publicGroupAccessRow publicGroup :. (groupPreferences, memberAdmission, currentTs, userId, groupId))
updateGroup_ ldn currentTs = do
DB.execute
db
@@ -2405,14 +2422,16 @@ 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.preferences, gp.member_admission
FROM group_profiles gp
JOIN groups g ON gp.group_profile_id = g.group_profile_id
WHERE g.group_id = ?
|]
(Only groupId)
toGroupProfile (displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_, groupPreferences, memberAdmission) =
GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_, groupPreferences, memberAdmission}
toGroupProfile ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (groupPreferences, memberAdmission)) =
let publicGroupAccess = toPublicGroupAccess accessRow
in GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ publicGroupAccess, groupPreferences, memberAdmission}
getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do
@@ -31,6 +31,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260403_item_viewed
import Simplex.Chat.Store.Postgres.Migrations.M20260429_relay_request_retries
import Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at
import Simplex.Chat.Store.Postgres.Migrations.M20260514_relay_request_group_link_index
import Simplex.Chat.Store.Postgres.Migrations.M20260515_public_group_access
import Simplex.Chat.Store.Postgres.Migrations.M20260529_delivery_job_senders
import Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services
import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at
@@ -65,6 +66,7 @@ schemaMigrations =
("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries),
("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at),
("20260514_relay_request_group_link_index", m20260514_relay_request_group_link_index, Just down_m20260514_relay_request_group_link_index),
("20260515_public_group_access", m20260515_public_group_access, Just down_m20260515_public_group_access),
("20260529_delivery_job_senders", m20260529_delivery_job_senders, Just down_m20260529_delivery_job_senders),
("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)
@@ -0,0 +1,29 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260515_public_group_access where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260515_public_group_access :: Text
m20260515_public_group_access =
[r|
ALTER TABLE group_profiles ADD COLUMN group_web_page TEXT;
ALTER TABLE group_profiles ADD COLUMN group_domain TEXT;
ALTER TABLE group_profiles ADD COLUMN domain_web_page BIGINT;
ALTER TABLE group_profiles ADD COLUMN allow_embedding BIGINT;
ALTER TABLE group_relays ADD COLUMN base_web_url TEXT;
|]
down_m20260515_public_group_access :: Text
down_m20260515_public_group_access =
[r|
ALTER TABLE group_relays DROP COLUMN base_web_url;
ALTER TABLE group_profiles DROP COLUMN allow_embedding;
ALTER TABLE group_profiles DROP COLUMN domain_web_page;
ALTER TABLE group_profiles DROP COLUMN group_domain;
ALTER TABLE group_profiles DROP COLUMN group_web_page;
|]
@@ -850,7 +850,11 @@ CREATE TABLE test_chat_schema.group_profiles (
short_descr text,
group_type text,
group_link bytea,
public_group_id bytea
public_group_id bytea,
group_web_page text,
group_domain text,
domain_web_page bigint,
allow_embedding bigint
);
@@ -875,7 +879,8 @@ CREATE TABLE test_chat_schema.group_relays (
relay_link bytea,
conf_id bytea,
created_at text DEFAULT now() NOT NULL,
updated_at text DEFAULT now() NOT NULL
updated_at text DEFAULT now() NOT NULL,
base_web_url text
);
@@ -154,6 +154,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260403_item_viewed
import Simplex.Chat.Store.SQLite.Migrations.M20260429_relay_request_retries
import Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at
import Simplex.Chat.Store.SQLite.Migrations.M20260514_relay_request_group_link_index
import Simplex.Chat.Store.SQLite.Migrations.M20260515_public_group_access
import Simplex.Chat.Store.SQLite.Migrations.M20260529_delivery_job_senders
import Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services
import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at
@@ -311,6 +312,7 @@ schemaMigrations =
("20260429_relay_request_retries", m20260429_relay_request_retries, Just down_m20260429_relay_request_retries),
("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at),
("20260514_relay_request_group_link_index", m20260514_relay_request_group_link_index, Just down_m20260514_relay_request_group_link_index),
("20260515_public_group_access", m20260515_public_group_access, Just down_m20260515_public_group_access),
("20260529_delivery_job_senders", m20260529_delivery_job_senders, Just down_m20260529_delivery_job_senders),
("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)
@@ -0,0 +1,28 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260515_public_group_access where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260515_public_group_access :: Query
m20260515_public_group_access =
[sql|
ALTER TABLE group_profiles ADD COLUMN group_web_page TEXT;
ALTER TABLE group_profiles ADD COLUMN group_domain TEXT;
ALTER TABLE group_profiles ADD COLUMN domain_web_page INTEGER;
ALTER TABLE group_profiles ADD COLUMN allow_embedding INTEGER;
ALTER TABLE group_relays ADD COLUMN base_web_url TEXT;
|]
down_m20260515_public_group_access :: Query
down_m20260515_public_group_access =
[sql|
ALTER TABLE group_relays DROP COLUMN base_web_url;
ALTER TABLE group_profiles DROP COLUMN allow_embedding;
ALTER TABLE group_profiles DROP COLUMN domain_web_page;
ALTER TABLE group_profiles DROP COLUMN group_domain;
ALTER TABLE group_profiles DROP COLUMN group_web_page;
|]
@@ -141,6 +141,7 @@ 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,
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,
@@ -970,6 +971,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.preferences, gp.member_admission
FROM group_profiles gp
JOIN groups g ON gp.group_profile_id = g.group_profile_id
@@ -1217,8 +1219,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,
user_id, preferences, member_admission, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
@@ -1741,6 +1744,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 = ?,
preferences = ?, member_admission = ?, updated_at = ?
WHERE group_profile_id IN (
SELECT group_profile_id
@@ -5107,6 +5111,14 @@ SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?)
LIST SUBQUERY 1
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE group_relays
SET base_web_url = ?, updated_at = ?
WHERE group_member_id = ?
Plan:
SEARCH group_relays USING INDEX idx_group_relays_group_member_id (group_member_id=?)
Query:
UPDATE group_relays
SET conf_id = ?, relay_link = ?, updated_at = ?
@@ -5291,6 +5303,7 @@ 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,
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,
@@ -5327,6 +5340,7 @@ 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,
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,
@@ -5356,6 +5370,7 @@ 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,
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,
@@ -5733,7 +5748,7 @@ SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?)
Query:
SELECT gr.group_relay_id, gr.group_member_id,
cr.chat_relay_id, cr.address, cr.display_name, cr.full_name, cr.short_descr, cr.image, cr.domains, cr.preset, cr.tested, cr.enabled, cr.deleted,
gr.relay_status, gr.relay_link
gr.relay_status, gr.relay_link, gr.base_web_url
FROM group_relays gr
JOIN chat_relays cr ON cr.chat_relay_id = gr.chat_relay_id
@@ -5750,7 +5765,7 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT gr.group_relay_id, gr.group_member_id,
cr.chat_relay_id, cr.address, cr.display_name, cr.full_name, cr.short_descr, cr.image, cr.domains, cr.preset, cr.tested, cr.enabled, cr.deleted,
gr.relay_status, gr.relay_link
gr.relay_status, gr.relay_link, gr.base_web_url
FROM group_relays gr
JOIN chat_relays cr ON cr.chat_relay_id = gr.chat_relay_id
WHERE gr.group_id = ?
@@ -5761,7 +5776,7 @@ SEARCH cr USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT gr.group_relay_id, gr.group_member_id,
cr.chat_relay_id, cr.address, cr.display_name, cr.full_name, cr.short_descr, cr.image, cr.domains, cr.preset, cr.tested, cr.enabled, cr.deleted,
gr.relay_status, gr.relay_link
gr.relay_status, gr.relay_link, gr.base_web_url
FROM group_relays gr
JOIN chat_relays cr ON cr.chat_relay_id = gr.chat_relay_id
WHERE gr.group_member_id = ?
@@ -5772,7 +5787,7 @@ SEARCH cr USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT gr.group_relay_id, gr.group_member_id,
cr.chat_relay_id, cr.address, cr.display_name, cr.full_name, cr.short_descr, cr.image, cr.domains, cr.preset, cr.tested, cr.enabled, cr.deleted,
gr.relay_status, gr.relay_link
gr.relay_status, gr.relay_link, gr.base_web_url
FROM group_relays gr
JOIN chat_relays cr ON cr.chat_relay_id = gr.chat_relay_id
WHERE gr.group_relay_id = ?
@@ -126,7 +126,11 @@ CREATE TABLE group_profiles(
short_descr TEXT,
group_type TEXT,
group_link BLOB,
public_group_id BLOB
public_group_id BLOB,
group_web_page TEXT,
group_domain TEXT,
domain_web_page INTEGER,
allow_embedding INTEGER
) STRICT;
CREATE TABLE groups(
group_id INTEGER PRIMARY KEY, -- local group ID
@@ -781,6 +785,8 @@ CREATE TABLE group_relays(
conf_id BLOB,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
base_web_url TEXT
) STRICT;
CREATE INDEX contact_profiles_index ON contact_profiles(
display_name,
+25 -7
View File
@@ -665,18 +665,20 @@ 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) :. (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 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 CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. GroupMemberRow
type PublicGroupAccessRow = (Maybe Text, Maybe Text, Maybe BoolInt, Maybe BoolInt)
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)
toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) =
toGroupInfo vr 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, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_
publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ (toPublicGroupAccess accessRow)
groupKeys = toGroupKeys publicGroupId_ groupKeysRow
groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission}
businessChat = toBusinessChatInfo businessRow
@@ -690,10 +692,25 @@ toPreparedGroup = \case
Just PreparedGroup {connLinkToConnect = CCLink fullLink shortLink_, connLinkPreparedConnection, connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId}
_ -> Nothing
toPublicGroupProfile :: Maybe GroupType -> Maybe ShortLinkContact -> Maybe B64UrlByteString -> Maybe PublicGroupProfile
toPublicGroupProfile (Just groupType) (Just groupLink) (Just publicGroupId) =
Just PublicGroupProfile {groupType, groupLink, publicGroupId}
toPublicGroupProfile _ _ _ = Nothing
toPublicGroupProfile :: Maybe GroupType -> Maybe ShortLinkContact -> Maybe B64UrlByteString -> Maybe PublicGroupAccess -> Maybe PublicGroupProfile
toPublicGroupProfile (Just groupType) (Just groupLink) (Just publicGroupId) publicGroupAccess =
Just PublicGroupProfile {groupType, groupLink, publicGroupId, publicGroupAccess}
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)
toPublicGroupAccess :: PublicGroupAccessRow -> Maybe PublicGroupAccess
toPublicGroupAccess (groupWebPage, groupDomain, domainWebPage_, allowEmbedding_)
| isJust groupWebPage || isJust groupDomain || domainWebPage || allowEmbedding =
Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding}
| otherwise = Nothing
where
domainWebPage = maybe False unBI domainWebPage_
allowEmbedding = maybe False unBI allowEmbedding_
toGroupKeys :: Maybe B64UrlByteString -> GroupKeysRow -> Maybe GroupKeys
toGroupKeys (Just publicGroupId) (rootPrivKey_, rootPubKey_, Just memberPrivKey) =
@@ -760,6 +777,7 @@ 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,
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,
+1 -9
View File
@@ -8,7 +8,6 @@
module Simplex.Chat.Terminal where
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import qualified Data.List.NonEmpty as L
import Simplex.Chat (defaultChatConfig)
import Simplex.Chat.Controller
@@ -23,8 +22,6 @@ import Simplex.Chat.Terminal.Output
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig)
import Simplex.Messaging.Util (raceAny_)
import System.Terminal (Key, Modifiers)
import UnliftIO.STM
#if !defined(dbPostgres)
import Control.Exception (handle, throwIO)
import qualified Data.ByteArray as BA
@@ -102,9 +99,4 @@ simplexChatTerminal cfg options t = run options
#endif
runChatTerminal :: ChatTerminal -> ChatController -> ChatOpts -> IO ()
runChatTerminal ct cc opts = do
keyQ <- newTQueueIO
raceAny_ [runKeyReader ct keyQ, runTerminalInput ct cc keyQ, runTerminalOutput ct cc opts, runInputLoop ct cc]
runKeyReader :: ChatTerminal -> TQueue (Key, Modifiers) -> IO ()
runKeyReader ct q = withChatTerm ct $ forever $ getKey >>= liftIO . atomically . writeTQueue q
runChatTerminal ct cc opts = raceAny_ [runTerminalInput ct cc, runTerminalOutput ct cc opts, runInputLoop ct cc]
+7 -7
View File
@@ -152,14 +152,14 @@ sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do
let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg
execChatCommand' cmd 0 `runReaderT` cc
runTerminalInput :: ChatTerminal -> ChatController -> TQueue (Key, Modifiers) -> IO ()
runTerminalInput ct cc keyQ = do
updateInputView ct
receiveFromTTY keyQ cc ct
runTerminalInput :: ChatTerminal -> ChatController -> IO ()
runTerminalInput ct cc = withChatTerm ct $ do
updateInput ct
receiveFromTTY cc ct
receiveFromTTY :: TQueue (Key, Modifiers) -> ChatController -> ChatTerminal -> IO ()
receiveFromTTY keyQ cc@ChatController {inputQ, currentUser, currentRemoteHost, chatStore} ct@ChatTerminal {termSize, termState, liveMessageState, activeTo} =
forever $ atomically (readTQueue keyQ) >>= processKey >> updateInputView ct
receiveFromTTY :: forall m. MonadTerminal m => ChatController -> ChatTerminal -> m ()
receiveFromTTY cc@ChatController {inputQ, currentUser, currentRemoteHost, chatStore} ct@ChatTerminal {termSize, termState, liveMessageState, activeTo} =
forever $ getKey >>= liftIO . processKey >> withTermLock ct (updateInput ct)
where
processKey :: (Key, Modifiers) -> IO ()
processKey key = case key of
+12 -1
View File
@@ -795,10 +795,19 @@ instance FromField GroupType where fromField = fromTextField_ textDecode
instance ToField GroupType where toField = toField . textEncode
data PublicGroupAccess = PublicGroupAccess
{ groupWebPage :: Maybe Text,
groupDomain :: Maybe Text,
domainWebPage :: Bool,
allowEmbedding :: Bool
}
deriving (Eq, Show)
data PublicGroupProfile = PublicGroupProfile
{ groupType :: GroupType,
groupLink :: ShortLinkContact,
publicGroupId :: B64UrlByteString -- group identity = sha256(genesis root key), immutable
publicGroupId :: B64UrlByteString, -- group identity = sha256(genesis root key), immutable
publicGroupAccess :: Maybe PublicGroupAccess
}
deriving (Eq, Show)
@@ -2086,6 +2095,8 @@ instance ToJSON GroupType where
toJSON = textToJSON
toEncoding = textToEncoding
$(JQ.deriveJSON defaultJSON ''PublicGroupAccess)
$(JQ.deriveJSON defaultJSON ''PublicGroupProfile)
$(JQ.deriveJSON defaultJSON ''GroupProfile)
+6
View File
@@ -3299,6 +3299,12 @@ testGLinkReviewMember =
alice ##> "/_delete member chat #1 5"
alice <## "bad chat command: member is pending"
-- moderator can't accept member with a role higher than their own
dan ##> "/_accept member #1 5 admin"
dan <## "#team: you have insufficient permissions for this action, the required role is admin"
dan ##> "/_accept member #1 5 owner"
dan <## "#team: you have insufficient permissions for this action, the required role is owner"
-- accept member
dan ##> "/_accept member #1 5 member"
concurrentlyN_