use domains for relay data, path is fixed

This commit is contained in:
Evgeny @ SimpleX Chat
2026-06-01 21:03:57 +00:00
parent 3fd7ed244c
commit fee2724bf3
19 changed files with 98 additions and 95 deletions
@@ -116,16 +116,16 @@ struct ChannelWebAccessView: View {
return webPage != currentWebPage || allowEmbedding != currentEmbedding
}
private var relayUrls: [String] {
groupRelays.compactMap { $0.relayCap.baseWebUrl }
private var relayDomains: [String] {
groupRelays.compactMap { $0.relayCap.webDomain }
}
private var embedCode: String? {
if let pg = groupInfo.groupProfile.publicGroup,
!relayUrls.isEmpty {
!relayDomains.isEmpty {
"""
<div data-simplex-group-preview
data-relay-urls="\(relayUrls.joined(separator: ","))"
data-relay-domains="\(relayDomains.joined(separator: ","))"
data-public-group-id="\(pg.publicGroupId)"
data-group-link="\(pg.groupLink)"></div>
<script src="https://simplex.chat/js/channel-preview.js"></script>
@@ -172,11 +172,11 @@ private fun ChannelWebPageLayout(
private fun embedCode(groupRelays: List<GroupRelay>, groupInfo: GroupInfo): String? {
val pg = groupInfo.groupProfile.publicGroup ?: return null
val relayUrls = groupRelays.mapNotNull { it.relayCap.baseWebUrl }
if (relayUrls.isEmpty()) return null
val urls = relayUrls.joinToString(",")
val relayDomains = groupRelays.mapNotNull { it.relayCap.webDomain }
if (relayDomains.isEmpty()) return null
val domains = relayDomains.joinToString(",")
return """<div data-simplex-group-preview
data-relay-urls="$urls"
data-relay-domains="$domains"
data-public-group-id="${pg.publicGroupId}"
data-group-link="${pg.groupLink}"></div>
<script src="https://simplex.chat/js/channel-preview.js"></script>"""
+2 -2
View File
@@ -138,7 +138,7 @@ library
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
Simplex.Chat.Store.Postgres.Migrations.M20260531_relay_sent_web_url
Simplex.Chat.Store.Postgres.Migrations.M20260531_relay_sent_web_domain
else
exposed-modules:
Simplex.Chat.Archive
@@ -298,7 +298,7 @@ library
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
Simplex.Chat.Store.SQLite.Migrations.M20260531_relay_sent_web_url
Simplex.Chat.Store.SQLite.Migrations.M20260531_relay_sent_web_domain
other-modules:
Paths_simplex_chat
hs-source-dirs:
+1 -1
View File
@@ -171,7 +171,7 @@ data ChatConfig = ChatConfig
}
data WebPreviewConfig = WebPreviewConfig
{ baseWebUrl :: Text,
{ webDomain :: Text,
webJsonDir :: FilePath,
webCorsFile :: Maybe FilePath,
webUpdateInterval :: Int -- seconds
+5 -5
View File
@@ -4880,14 +4880,14 @@ runRelayGroupLinkChecks user = do
sendRelayCapIfNeeded vr gInfo
sendRelayCapIfNeeded vr gInfo = do
ChatConfig {webPreviewConfig} <- asks config
let currentWebUrl = (\WebPreviewConfig {baseWebUrl} -> baseWebUrl) <$> webPreviewConfig
sentWebUrl <- withStore' (`getRelaySentWebUrl` gInfo)
when (currentWebUrl /= sentWebUrl) $ do
let currentWebDomain = (\WebPreviewConfig {webDomain} -> webDomain) <$> webPreviewConfig
sentWebDomain <- withStore' (`getRelaySentWebDomain` gInfo)
when (currentWebDomain /= sentWebDomain) $ do
owners <- withStore' $ \db -> getGroupOwners db vr user gInfo
let capableOwners = filter (\m -> memberCurrent m && m `supportsVersion` relayWebCapVersion) owners
unless (null capableOwners) $ do
void $ sendGroupMessage' user gInfo capableOwners (XGrpRelayCap RelayCapabilities {baseWebUrl = currentWebUrl})
withStore' $ \db -> updateRelaySentWebUrl db gInfo currentWebUrl
void $ sendGroupMessage' user gInfo capableOwners (XGrpRelayCap RelayCapabilities {webDomain = currentWebDomain})
withStore' $ \db -> updateRelaySentWebDomain db gInfo currentWebDomain
checkRelayInactiveGroups = do
vr <- chatVersionRange
ttl <- asks (relayInactiveTTL . config)
+2 -2
View File
@@ -1046,8 +1046,8 @@ acceptRelayJoinRequestAsync
cReqChatVRange
relayLink = do
ChatConfig {webPreviewConfig} <- asks config
let webUrl = (\WebPreviewConfig {baseWebUrl} -> baseWebUrl) <$> webPreviewConfig
msg = XGrpRelayAcpt relayLink RelayCapabilities {baseWebUrl = webUrl}
let webDomain_ = (\WebPreviewConfig {webDomain} -> webDomain) <$> webPreviewConfig
msg = XGrpRelayAcpt relayLink RelayCapabilities {webDomain = webDomain_}
subMode <- chatReadVar subscriptionMode
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
+7 -7
View File
@@ -242,12 +242,12 @@ coreChatOptsP appDir defaultDbName = do
<> help "Run as a chat relay client"
)
webPreviewConfig <- do
baseWebUrl_ <-
webDomain_ <-
optional $
strOption
( long "relay-web-url"
<> metavar "URL"
<> help "Base URL for channel web previews (relay only)"
( long "relay-web-domain"
<> metavar "DOMAIN"
<> help "Domain for channel web previews (relay only)"
)
webJsonDir_ <-
optional $
@@ -270,10 +270,10 @@ coreChatOptsP appDir defaultDbName = do
<> help "Interval between web preview regeneration in seconds (relay only)"
<> value 300
)
pure $ case (baseWebUrl_, webJsonDir_) of
(Just baseWebUrl, Just webJsonDir) -> Just WebPreviewConfig {baseWebUrl, webJsonDir, webCorsFile, webUpdateInterval}
pure $ case (webDomain_, webJsonDir_) of
(Just webDomain, Just webJsonDir) -> Just WebPreviewConfig {webDomain, webJsonDir, webCorsFile, webUpdateInterval}
(Nothing, Nothing) -> Nothing
_ -> error "--relay-web-url and --relay-web-dir must both be provided"
_ -> error "--relay-web-domain and --relay-web-dir must both be provided"
highlyAvailable <-
switch
( long "ha"
+8 -8
View File
@@ -99,8 +99,8 @@ module Simplex.Chat.Store.Groups
createRelayRequestGroup,
updateRelayOwnStatusFromTo,
updateRelayOwnStatus_,
getRelaySentWebUrl,
updateRelaySentWebUrl,
getRelaySentWebDomain,
updateRelaySentWebDomain,
isRelayGroupRejected,
allowRelayGroup,
getRelayServedGroups,
@@ -1644,13 +1644,13 @@ updateRelayOwnStatus_ db GroupInfo {groupId} relayStatus = do
let inactiveAt_ = if relayStatus == RSInactive then Just currentTs else Nothing
DB.execute db "UPDATE groups SET relay_own_status = ?, relay_inactive_at = ?, updated_at = ? WHERE group_id = ?" (relayStatus, inactiveAt_, currentTs, groupId)
getRelaySentWebUrl :: DB.Connection -> GroupInfo -> IO (Maybe Text)
getRelaySentWebUrl db GroupInfo {groupId} =
join <$> maybeFirstRow fromOnly (DB.query db "SELECT relay_sent_web_url FROM groups WHERE group_id = ?" (Only groupId))
getRelaySentWebDomain :: DB.Connection -> GroupInfo -> IO (Maybe Text)
getRelaySentWebDomain db GroupInfo {groupId} =
join <$> maybeFirstRow fromOnly (DB.query db "SELECT relay_sent_web_domain FROM groups WHERE group_id = ?" (Only groupId))
updateRelaySentWebUrl :: DB.Connection -> GroupInfo -> Maybe Text -> IO ()
updateRelaySentWebUrl db GroupInfo {groupId} webUrl =
DB.execute db "UPDATE groups SET relay_sent_web_url = ? WHERE group_id = ?" (webUrl, groupId)
updateRelaySentWebDomain :: DB.Connection -> GroupInfo -> Maybe Text -> IO ()
updateRelaySentWebDomain db GroupInfo {groupId} webDomain_ =
DB.execute db "UPDATE groups SET relay_sent_web_domain = ? WHERE group_id = ?" (webDomain_, groupId)
-- Flip every RSRejected row sharing the targeted group's relay_request_group_link
-- to RSInactive in one statement; returns the refreshed GroupInfo for the targeted groupId.
@@ -35,7 +35,7 @@ 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
import Simplex.Chat.Store.Postgres.Migrations.M20260531_relay_sent_web_url
import Simplex.Chat.Store.Postgres.Migrations.M20260531_relay_sent_web_domain
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -71,7 +71,7 @@ schemaMigrations =
("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),
("20260531_relay_sent_web_url", m20260531_relay_sent_web_url, Just down_m20260531_relay_sent_web_url)
("20260531_relay_sent_web_domain", m20260531_relay_sent_web_domain, Just down_m20260531_relay_sent_web_domain)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,19 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260531_relay_sent_web_domain where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260531_relay_sent_web_domain :: Text
m20260531_relay_sent_web_domain =
[r|
ALTER TABLE groups ADD COLUMN relay_sent_web_domain TEXT;
|]
down_m20260531_relay_sent_web_domain :: Text
down_m20260531_relay_sent_web_domain =
[r|
ALTER TABLE groups DROP COLUMN relay_sent_web_domain;
|]
@@ -1,19 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260531_relay_sent_web_url where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260531_relay_sent_web_url :: Text
m20260531_relay_sent_web_url =
[r|
ALTER TABLE groups ADD COLUMN relay_sent_web_url TEXT;
|]
down_m20260531_relay_sent_web_url :: Text
down_m20260531_relay_sent_web_url =
[r|
ALTER TABLE groups DROP COLUMN relay_sent_web_url;
|]
+2 -2
View File
@@ -158,7 +158,7 @@ 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
import Simplex.Chat.Store.SQLite.Migrations.M20260531_relay_sent_web_url
import Simplex.Chat.Store.SQLite.Migrations.M20260531_relay_sent_web_domain
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -317,7 +317,7 @@ schemaMigrations =
("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),
("20260531_relay_sent_web_url", m20260531_relay_sent_web_url, Just down_m20260531_relay_sent_web_url)
("20260531_relay_sent_web_domain", m20260531_relay_sent_web_domain, Just down_m20260531_relay_sent_web_domain)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260531_relay_sent_web_domain where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260531_relay_sent_web_domain :: Query
m20260531_relay_sent_web_domain =
[sql|
ALTER TABLE groups ADD COLUMN relay_sent_web_domain TEXT;
|]
down_m20260531_relay_sent_web_domain :: Query
down_m20260531_relay_sent_web_domain =
[sql|
ALTER TABLE groups DROP COLUMN relay_sent_web_domain;
|]
@@ -1,18 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260531_relay_sent_web_url where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260531_relay_sent_web_url :: Query
m20260531_relay_sent_web_url =
[sql|
ALTER TABLE groups ADD COLUMN relay_sent_web_url TEXT;
|]
down_m20260531_relay_sent_web_url :: Query
down_m20260531_relay_sent_web_url =
[sql|
ALTER TABLE groups DROP COLUMN relay_sent_web_url;
|]
+2 -2
View File
@@ -1206,8 +1206,8 @@ viewReceivedContactRequest c Profile {fullName, shortDescr} =
]
showRelay :: GroupRelay -> StyledString
showRelay GroupRelay {groupRelayId, relayStatus, relayCap = RelayCapabilities {baseWebUrl}} =
" - relay id " <> sShow groupRelayId <> ": " <> plain (relayStatusText relayStatus) <> maybe "" (\url -> ", web: " <> plain url) baseWebUrl
showRelay GroupRelay {groupRelayId, relayStatus, relayCap = RelayCapabilities {webDomain}} =
" - relay id " <> sShow groupRelayId <> ": " <> plain (relayStatusText relayStatus) <> maybe "" (\d -> ", web: " <> plain d) webDomain
viewGroupRelays :: GroupInfo -> [GroupRelay] -> [StyledString]
viewGroupRelays g relays =
+7 -4
View File
@@ -227,6 +227,9 @@ corsEntry publicGroupId PublicGroupAccess {groupWebPage, allowEmbedding} =
| otherwise = CorsOrigins []
in (fName, origin)
channelPath :: Text
channelPath = "/channel/"
writeCorsConfig :: [(Text, CorsOrigin)] -> FilePath -> IO ()
writeCorsConfig entries path =
TIO.writeFile path $ T.unlines $
@@ -234,15 +237,15 @@ writeCorsConfig entries path =
<> map corsLine entries
<> [ " default \"\"",
"}",
"header /preview/*.json Access-Control-Allow-Origin {cors_origin}",
"header /preview/*.json Access-Control-Allow-Methods \"GET, OPTIONS\""
"header " <> channelPath <> "*.json Access-Control-Allow-Origin {cors_origin}",
"header " <> channelPath <> "*.json Access-Control-Allow-Methods \"GET, OPTIONS\""
]
where
corsLine (fName, origin) = case origin of
CorsAny -> " /preview/" <> fName <> " \"*\""
CorsAny -> " " <> channelPath <> fName <> " \"*\""
CorsOrigins origins -> case origins of
[] -> " # " <> fName <> " (no origin configured)"
(o : _) -> " /preview/" <> fName <> " \"" <> o <> "\""
(o : _) -> " " <> channelPath <> fName <> " \"" <> o <> "\""
publicGroupIdFileName :: B64UrlByteString -> String
publicGroupIdFileName = B.unpack . strEncode
+1 -1
View File
@@ -165,7 +165,7 @@ relayTestOpts :: ChatOpts
relayTestOpts = testOpts {coreOptions = testCoreOpts {chatRelay = True}}
relayWebTestOpts :: Text -> FilePath -> ChatOpts
relayWebTestOpts webUrl webDir = testOpts {coreOptions = testCoreOpts {chatRelay = True, webPreviewConfig = Just WebPreviewConfig {baseWebUrl = webUrl, webJsonDir = webDir, webCorsFile = Nothing, webUpdateInterval = 300}}}
relayWebTestOpts webDomain webDir = testOpts {coreOptions = testCoreOpts {chatRelay = True, webPreviewConfig = Just WebPreviewConfig {webDomain, webJsonDir = webDir, webCorsFile = Nothing, webUpdateInterval = 300}}}
#if !defined(dbPostgres)
getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts
+7 -7
View File
@@ -35,7 +35,7 @@ chatRelayTests = do
it "test chat relay" testChatRelayTest
it "relay profile updated in address" testRelayProfileUpdateInAddress
describe "relay capabilities" $ do
it "relay sends baseWebUrl in capabilities" testRelayWebCapabilities
it "relay sends webDomain in capabilities" testRelayWebCapabilities
describe "web preview" $ do
it "render web preview JSON for channel" testWebPreviewRender
it "generate CORS config" testWebPreviewCors
@@ -339,7 +339,7 @@ getTermLine2 c = (,) <$> getTermLine c <*> getTermLine c
testRelayWebCapabilities :: HasCallStack => TestParams -> IO ()
testRelayWebCapabilities ps =
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps (relayWebTestOpts "https://relay.example.com/preview" (tmpPath ps </> "web_cap")) "bob" bobProfile $ \relay -> do
withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" (tmpPath ps </> "web_cap")) "bob" bobProfile $ \relay -> do
rName <- userName relay
relay ##> "/ad"
(relaySLink, _cLink) <- getContactLinks relay True
@@ -351,7 +351,7 @@ testRelayWebCapabilities ps =
concurrentlyN_
[ do
alice <## "#news: group link relays updated, current relays:"
alice <## " - relay id 1: active, web: https://relay.example.com/preview"
alice <## " - relay id 1: active, web: relay.example.com"
alice <## "group link:"
_ <- getTermLine alice
pure (),
@@ -362,7 +362,7 @@ testWebPreviewRender :: HasCallStack => TestParams -> IO ()
testWebPreviewRender ps = do
let webDir = tmpPath ps </> "web_preview"
withNewTestChat ps "alice" aliceProfile $ \alice ->
withNewTestChatOpts ps (relayWebTestOpts "https://relay.example.com/preview" webDir) "bob" bobProfile $ \relay -> do
withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" webDir) "bob" bobProfile $ \relay -> do
_ <- setupRelay alice relay
alice ##> "/public group relays=1 #news"
alice <## "group #news is created"
@@ -370,7 +370,7 @@ testWebPreviewRender ps = do
concurrentlyN_
[ do
alice <## "#news: group link relays updated, current relays:"
alice <## " - relay id 1: active, web: https://relay.example.com/preview"
alice <## " - relay id 1: active, web: relay.example.com"
alice <## "group link:"
_ <- getTermLine alice
pure (),
@@ -411,8 +411,8 @@ testWebPreviewCors ps = do
]
writeCorsConfig entries corsFile
corsContent <- readFile corsFile
corsContent `shouldContain` "/preview/abc123.json \"*\""
corsContent `shouldContain` "/preview/def456.json \"https://owner-site.com\""
corsContent `shouldContain` "/channel/abc123.json \"*\""
corsContent `shouldContain` "/channel/def456.json \"https://owner-site.com\""
corsContent `shouldContain` "# ghi789.json (no origin configured)"
corsContent `shouldContain` "Access-Control-Allow-Origin"
+7 -7
View File
@@ -432,12 +432,12 @@ const VOICE_ICON_SVG = `<svg class="simplex-preview-file-icon" viewBox="0 0 24 2
const FORWARD_ICON_SVG = `<svg width="14" height="14" viewBox="0 -960 960 960" fill="currentColor"><path transform="scale(-1,1) translate(-960,0)" d="m236.5-495.5 142.5 143q8.5 8.5 8.25 20.25T378.5-312q-9 8.5-21 8.5t-20.5-9L145.5-504q-9-8.5-9-20t9-20.5L338-737q9-9 20.75-8.75T379.5-737q8.5 8.5 8.5 20.5t-8.5 20.5l-143 143h403q84 0 139.75 56T835-357.5V-234q0 12.5-8.25 20.75T806.5-205q-12.5 0-20.75-8.25T777.5-234v-123.5q0-60-39-99t-99-39h-403Z"/></svg>`;
function initChannelPreview(container) {
const relayUrls = (container.dataset.relayUrls || '').split(',').map(u => u.trim()).filter(Boolean);
const relayDomains = (container.dataset.relayDomains || '').split(',').map(u => u.trim()).filter(Boolean);
const groupId = container.dataset.publicGroupId || '';
const groupLink = container.dataset.groupLink || '';
if (!relayUrls.length || !groupId) {
container.innerHTML = '<p class="simplex-preview-empty">Missing configuration: data-relay-urls and data-public-group-id required.</p>';
if (!relayDomains.length || !groupId) {
container.innerHTML = '<p class="simplex-preview-empty">Missing configuration: data-relay-domains and data-public-group-id required.</p>';
return;
}
@@ -445,7 +445,7 @@ function initChannelPreview(container) {
container.classList.add('simplex-preview-container');
container.innerHTML = '<p class="simplex-preview-empty">Loading channel...</p>';
fetchPreview(relayUrls, groupId).then(data => {
fetchPreview(relayDomains, groupId).then(data => {
if (!data) {
container.innerHTML = '<p class="simplex-preview-empty">Failed to load channel preview.</p>';
return;
@@ -463,10 +463,10 @@ function injectStyles() {
document.head.appendChild(style);
}
async function fetchPreview(relayUrls, groupId) {
for (const baseUrl of relayUrls) {
async function fetchPreview(relayDomains, groupId) {
for (const domain of relayDomains) {
try {
const url = baseUrl.replace(/\/$/, '') + '/' + groupId + '.json';
const url = `https://${domain}/channel/${groupId}.json`;
const resp = await fetch(url);
if (!resp.ok) continue;
return await resp.json();