Compare commits

...

3 Commits

Author SHA1 Message Date
Alexander Bondarenko ec98ea051b debug: add thread stats 2024-03-08 20:29:27 +02:00
Evgeny Poberezkin 435ea9a453 core: api to pass additional information with standalone file URI (#3873)
* xftp: redirect for descriptions with more than one chunk

* handle errors

* core: api to pass additional information with standalone file URI

* cleanup

* test info with large file

* Apply suggestions from code review

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>

* remove db-mediated client data

* refactor

* fix

---------

Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com>
2024-03-08 13:38:48 +00:00
Evgeny Poberezkin 9ff11f886e website: add group link page 2024-03-07 15:40:06 +00:00
7 changed files with 176 additions and 46 deletions
+17 -17
View File
@@ -17,7 +17,7 @@ _Please note_: when you change the servers in the app configuration, it only aff
## Installation
0. First, install `smp-server`:
1. First, install `smp-server`:
- Manual deployment (see below)
@@ -28,7 +28,7 @@ _Please note_: when you change the servers in the app configuration, it only aff
Manual installation requires some preliminary actions:
0. Install binary:
1. Install binary:
- Using offical binaries:
@@ -40,20 +40,20 @@ Manual installation requires some preliminary actions:
Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution)
1. Create user and group for `smp-server`:
2. Create user and group for `smp-server`:
```sh
sudo useradd -m smp
```
2. Create necessary directories and assign permissions:
3. Create necessary directories and assign permissions:
```sh
sudo mkdir -p /var/opt/simplex /etc/opt/simplex
sudo chown smp:smp /var/opt/simplex /etc/opt/simplex
```
3. Allow `smp-server` port in firewall:
4. Allow `smp-server` port in firewall:
```sh
# For Ubuntu
@@ -63,7 +63,7 @@ Manual installation requires some preliminary actions:
sudo firewall-cmd --reload
```
4. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/smp-server.service` file with the following content:
5. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/smp-server.service` file with the following content:
```sh
[Unit]
@@ -398,20 +398,20 @@ To import `csv` to `Grafana` one should:
2. Allow local mode by appending following:
```sh
[plugin.marcusolsson-csv-datasource]
allow_local_mode = true
```
```sh
[plugin.marcusolsson-csv-datasource]
allow_local_mode = true
```
... to `/etc/grafana/grafana.ini`
... to `/etc/grafana/grafana.ini`
3. Add a CSV data source:
- In the side menu, click the Configuration tab (cog icon)
- Click Add data source in the top-right corner of the Data Sources tab
- Enter "CSV" in the search box to find the CSV data source
- Click the search result that says "CSV"
- In URL, enter a file that points to CSV content
- In the side menu, click the Configuration tab (cog icon)
- Click Add data source in the top-right corner of the Data Sources tab
- Enter "CSV" in the search box to find the CSV data source
- Click the search result that says "CSV"
- In URL, enter a file that points to CSV content
4. You're done! You should be able to create your own dashboard with statistics.
@@ -445,7 +445,7 @@ To update your smp-server to latest version, choose your installation method and
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
1. Stop and remove the container:
```sh
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/smp-server --format="{{.ID}}"))
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/smp-server --format="\{\{.ID\}\}"))
```
2. Pull latest image:
```sh
+1 -1
View File
@@ -448,7 +448,7 @@ To update your XFTP server to latest version, choose your installation method an
- [Docker container](https://github.com/simplex-chat/simplexmq#using-docker)
1. Stop and remove the container:
```sh
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/xftp-server --format="{{.ID}}"))
docker rm $(docker stop $(docker ps -a -q --filter ancestor=simplexchat/xftp-server --format="\{\{.ID\}\}"))
```
2. Pull latest image:
```sh
+68 -21
View File
@@ -1,3 +1,5 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
@@ -15,6 +17,9 @@
module Simplex.Chat where
#if MIN_VERSION_base(4,18,0)
import GHC.Conc.Sync (BlockReason (..), ThreadStatus (..), listThreads, threadLabel, threadStatus)
#endif
import Control.Applicative (optional, (<|>))
import Control.Concurrent.STM (retry)
import Control.Logger.Simple
@@ -330,7 +335,7 @@ startChatController mainApp = do
a1 <- async agentSubscriber
a2 <-
if mainApp
then Just <$> async (subscribeUsers False users)
then Just <$> async (labelMyThread "startChatController.subscribeUsers" >> subscribeUsers False users)
else pure Nothing
atomically . writeTVar s $ Just (a1, a2)
when mainApp $ do
@@ -561,8 +566,8 @@ processChatCommand' vr = \case
chatWriteVar chatActivated True
when restoreChat $ do
users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers
void . forkIO $ subscribeUsers True users
void . forkIO $ startFilesToReceive users
void . forkIO $ labelMyThread "APIActivateChat.subscribeUsers" >> subscribeUsers True users
void . forkIO $ labelMyThread "APIActivateChat.startFilesToReceive" >> startFilesToReceive users
setAllExpireCIFlags True
ok_
APISuspendChat t -> do
@@ -2008,6 +2013,7 @@ processChatCommand' vr = \case
when (fileSize > toInteger maxFileSizeHard) $ throwChatError $ CEFileSize filePath
(_, _, fileTransferMeta) <- xftpSndFileTransfer_ user file fileSize 1 Nothing
pure CRSndStandaloneFileCreated {user, fileTransferMeta}
APIStandaloneFileInfo FileDescriptionURI {clientData} -> pure . CRStandaloneFileInfo $ clientData >>= J.decodeStrict . encodeUtf8
APIDownloadStandaloneFile userId uri file -> withUserId userId $ \user -> do
ft <- receiveViaURI user uri file
pure $ CRRcvStandaloneFileCreated user ft
@@ -2024,6 +2030,10 @@ processChatCommand' vr = \case
pure CRDebugLocks {chatLockName, agentLocks}
GetAgentWorkers -> CRAgentWorkersSummary <$> withAgent getAgentWorkersSummary
GetAgentWorkersDetails -> CRAgentWorkersDetails <$> withAgent getAgentWorkersDetails
GetThreads -> do
(threadsRunning, threadsBlocked, threadsBlockedConc, threadsDone) <- liftIO getThreadsSummary
pure CRThreadsSummary {threadsRunning, threadsBlocked, threadsBlockedConc, threadsDone}
GetThreadsDetails -> CRThreadsDetails <$> liftIO getThreadsDetails
GetAgentStats -> CRAgentStats . map stat <$> withAgent getAgentStats
where
stat (AgentStatsKey {host, clientTs, cmd, res}, count) =
@@ -2889,6 +2899,7 @@ deleteGroupLink_ user gInfo conn = do
agentSubscriber :: forall m. (MonadUnliftIO m, MonadReader ChatController m) => m ()
agentSubscriber = do
labelMyThread "agentSubscriber"
q <- asks $ subQ . smpAgent
l <- asks chatLock
forever $ atomically (readTBQueue q) >>= void . process l
@@ -3051,6 +3062,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user@User {userId} =
forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do
forM_ err_ $ toView . CRSndFileSubError user ft
void . forkIO $ do
labelMyThread "sndFileSubsToView"
threadDelay 1000000
l <- asks chatLock
when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withLock l "subscribe sendFileChunk" $
@@ -3271,13 +3283,15 @@ processAgentMsgSndFile _corrId aFileId msg =
Nothing -> do
withAgent (`xftpDeleteSndFileInternal` aFileId)
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds)
case mapMaybe fileDescrURI rfds of
[] -> case rfds of
[] -> logError "File sent without receiver descriptions" -- should not happen
(rfd : _) -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft
uris -> do
ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor
toView $ CRSndStandaloneFileComplete user ft' uris
case rfds of
[] -> sendFileError "no receiver descriptions" fileId vr ft
rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of
[] -> case xftpRedirectFor of
Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft
Just _ -> sendFileError "Prohibit chaining redirects" fileId vr ft
rfds' -> do -- we have 1 chunk - use it as URI whether it is redirect or not
ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor
toView $ CRSndStandaloneFileComplete user ft' $ map (decodeLatin1 . strEncode . FD.fileDescriptionURI) rfds'
Just (AChatItem _ d cInfo _ci@ChatItem {meta = CIMeta {itemSharedMsgId = msgId_, itemDeleted}}) ->
case (msgId_, itemDeleted) of
(Just sharedMsgId, Nothing) -> do
@@ -3319,19 +3333,11 @@ processAgentMsgSndFile _corrId aFileId msg =
SFERR e
| temporaryAgentError e ->
throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e
| otherwise -> do
ci <- withStore $ \db -> do
liftIO $ updateFileCancelled db user fileId CIFSSndError
lookupChatItemByFileId db vr user fileId
withAgent (`xftpDeleteSndFileInternal` aFileId)
toView $ CRSndFileError user ci ft
| otherwise ->
sendFileError (tshow e) fileId vr ft
where
fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text
fileDescrText = safeDecodeUtf8 . strEncode
fileDescrURI :: ValidFileDescription 'FRecipient -> Maybe T.Text
fileDescrURI vfd = if T.length uri < FD.qrSizeLimit then Just uri else Nothing
where
uri = decodeLatin1 . strEncode $ FD.fileDescriptionURI vfd
sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> m (SndMessage, Int64)) -> m Int64
sendFileDescription sft rfd msgId sendMsg = do
let rfdText = fileDescrText rfd
@@ -3346,6 +3352,14 @@ processAgentMsgSndFile _corrId aFileId msg =
case L.nonEmpty fds of
Just fds' -> loopSend fds'
Nothing -> pure msgDeliveryId
sendFileError :: Text -> Int64 -> VersionRange -> FileTransferMeta -> m ()
sendFileError err fileId vr ft = do
logError $ "Sent file error: " <> err
ci <- withStore $ \db -> do
liftIO $ updateFileCancelled db user fileId CIFSSndError
lookupChatItemByFileId db vr user fileId
withAgent (`xftpDeleteSndFileInternal` aFileId)
toView $ CRSndFileError user ci ft
splitFileDescr :: ChatMonad m => RcvFileDescrText -> m (NonEmpty FileDescr)
splitFileDescr rfdText = do
@@ -6783,6 +6797,7 @@ chatCommandP =
"/stop remote ctrl" $> StopRemoteCtrl,
"/delete remote ctrl " *> (DeleteRemoteCtrl <$> A.decimal),
"/_upload " *> (APIUploadStandaloneFile <$> A.decimal <* A.space <*> cryptoFileP),
"/_download info " *> (APIStandaloneFileInfo <$> strP),
"/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP),
("/quit" <|> "/q" <|> "/exit") $> QuitChat,
("/version" <|> "/v") $> ShowVersion,
@@ -6792,7 +6807,9 @@ chatCommandP =
"/get subs" $> GetAgentSubs,
"/get subs details" $> GetAgentSubsDetails,
"/get workers" $> GetAgentWorkers,
"/get workers details" $> GetAgentWorkersDetails
"/get workers details" $> GetAgentWorkersDetails,
"/get threads" $> GetThreads,
"/get threads details" $> GetThreadsDetails
]
where
choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput)
@@ -6989,3 +7006,33 @@ xftpSndFileRedirect user ftId vfd = do
dummyFileDescr :: FileDescr
dummyFileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
getThreadsSummary :: IO (Int, Int, Int, Int)
getThreadsSummary = do
#if MIN_VERSION_base(4,18,0)
threads <- listThreads >>= mapM threadStatus
pure $ foldl' byStatus (0, 0, 0, 0) threads
where
byStatus (!r, !b, !bc, !d) = \case
ThreadRunning -> (r + 1, b, bc, d)
ThreadDied -> (r, b, bc, d + 1)
ThreadFinished -> (r, b, bc, d + 1)
ThreadBlocked br -> case br of
BlockedOnMVar -> (r, b, bc + 1, d)
BlockedOnSTM -> (r, b, bc + 1, d)
_ -> (r, b + 1, bc, d)
#else
getThreadsSummary = pure (0, 0, 0, 0)
#endif
getThreadsDetails :: IO [[String]]
#if MIN_VERSION_base(4,18,0)
getThreadsDetails = do
threads <- listThreads
forM threads $ \tid -> do
status <- threadStatus tid
label <- threadLabel tid
pure [drop 9 $ show tid, show status, show label]
#else
getThreadsDetails = pure []
#endif
+6
View File
@@ -455,6 +455,7 @@ data ChatCommand
| DeleteRemoteCtrl RemoteCtrlId -- Remove all local data associated with a remote controller session
| APIUploadStandaloneFile UserId CryptoFile
| APIDownloadStandaloneFile UserId FileDescriptionURI CryptoFile
| APIStandaloneFileInfo FileDescriptionURI
| QuitChat
| ShowVersion
| DebugLocks
@@ -464,6 +465,8 @@ data ChatCommand
| GetAgentSubsDetails
| GetAgentWorkers
| GetAgentWorkersDetails
| GetThreads
| GetThreadsDetails
deriving (Show)
allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal
@@ -594,6 +597,7 @@ data ChatResponse
| CRRcvFileAccepted {user :: User, chatItem :: AChatItem}
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileDescrNotReady {user :: User, chatItem :: AChatItem}
| CRStandaloneFileInfo {fileMeta :: Maybe J.Value}
| CRRcvStandaloneFileCreated {user :: User, rcvFileTransfer :: RcvFileTransfer} -- returned by _download
| CRRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats
| CRRcvFileProgressXFTP {user :: User, chatItem_ :: Maybe AChatItem, receivedSize :: Int64, totalSize :: Int64, rcvFileTransfer :: RcvFileTransfer}
@@ -700,6 +704,8 @@ data ChatResponse
| CRSQLResult {rows :: [Text]}
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
| CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks}
| CRThreadsSummary {threadsRunning :: Int, threadsBlocked :: Int, threadsBlockedConc :: Int, threadsDone :: Int}
| CRThreadsDetails {threads :: [[String]]}
| CRAgentStats {agentStats :: [[String]]}
| CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails}
| CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary}
+3
View File
@@ -218,6 +218,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRSndFileError u (Just ci) _ -> ttyUser u $ uploadingFile "error" ci
CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} ->
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [plain . LB.toStrict $ J.encode j]) info_
CRContactConnecting u _ -> ttyUser u []
CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView
CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"]
@@ -370,6 +371,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
[ "agent workers details:",
plain . LB.unpack $ J.encode agentWorkersDetails -- this would be huge, but copypastable when has its own line
]
CRThreadsSummary {threadsRunning, threadsBlocked, threadsBlockedConc, threadsDone} -> ["running: " <> sShow threadsRunning <> " blocked: " <> sShow threadsBlocked <> " waiting: " <> sShow threadsBlockedConc <> " done: " <> sShow threadsDone]
CRThreadsDetails rows -> map (plain . intercalate ",") rows
CRConnectionDisabled entity -> viewConnectionEntityDisabled entity
CRAgentRcvQueueDeleted acId srv aqId err_ ->
[ ("completed deleting rcv queue, agent connection id: " <> sShow acId)
+73 -7
View File
@@ -13,6 +13,7 @@ import Control.Logger.Simple
import qualified Data.Aeson as J
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Network.HTTP.Types.URI (urlEncode)
import Simplex.Chat (roundedFDCount)
import Simplex.Chat.Controller (ChatConfig (..))
import Simplex.Chat.Mobile.File
@@ -52,7 +53,9 @@ chatFileTests = do
it "should prohibit file transfers in groups based on preference" testProhibitFiles
describe "file transfer over XFTP without chat items" $ do
it "send and receive small standalone file" testXFTPStandaloneSmall
it "send and receive small standalone file with extra information" testXFTPStandaloneSmallInfo
it "send and receive large standalone file" testXFTPStandaloneLarge
it "send and receive large standalone file with extra information" testXFTPStandaloneLargeInfo
it "send and receive large standalone file using relative paths" testXFTPStandaloneRelativePaths
xit "removes sent file from server" testXFTPStandaloneCancelSnd -- no error shown in tests
it "removes received temporary files" testXFTPStandaloneCancelRcv
@@ -848,11 +851,11 @@ testXFTPStandaloneSmall :: HasCallStack => FilePath -> IO ()
testXFTPStandaloneSmall = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do
withXFTPServer $ do
logNote "sending"
src ##> "/_upload 1 ./tests/fixtures/test.jpg"
src <## "started standalone uploading file 1 (test.jpg)"
src ##> "/_upload 1 ./tests/fixtures/logo.jpg"
src <## "started standalone uploading file 1 (logo.jpg)"
-- silent progress events
threadDelay 250000
src <## "file 1 (test.jpg) upload complete. download with:"
src <## "file 1 (logo.jpg) upload complete. download with:"
-- file description fits, enjoy the direct URIs
_uri1 <- getTermLine src
_uri2 <- getTermLine src
@@ -860,13 +863,43 @@ testXFTPStandaloneSmall = testChat2 aliceProfile aliceDesktopProfile $ \src dst
_uri4 <- getTermLine src
logNote "receiving"
let dstFile = "./tests/tmp/test.jpg"
let dstFile = "./tests/tmp/logo.jpg"
dst ##> ("/_download 1 " <> uri3 <> " " <> dstFile)
dst <## "started standalone receiving file 1 (test.jpg)"
dst <## "started standalone receiving file 1 (logo.jpg)"
-- silent progress events
threadDelay 250000
dst <## "completed standalone receiving file 1 (test.jpg)"
srcBody <- B.readFile "./tests/fixtures/test.jpg"
dst <## "completed standalone receiving file 1 (logo.jpg)"
srcBody <- B.readFile "./tests/fixtures/logo.jpg"
B.readFile dstFile `shouldReturn` srcBody
testXFTPStandaloneSmallInfo :: HasCallStack => FilePath -> IO ()
testXFTPStandaloneSmallInfo = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do
withXFTPServer $ do
logNote "sending"
src ##> "/_upload 1 ./tests/fixtures/logo.jpg"
src <## "started standalone uploading file 1 (logo.jpg)"
-- silent progress events
threadDelay 250000
src <## "file 1 (logo.jpg) upload complete. download with:"
-- file description fits, enjoy the direct URIs
_uri1 <- getTermLine src
_uri2 <- getTermLine src
uri3 <- getTermLine src
_uri4 <- getTermLine src
let uri = uri3 <> "&data=" <> B.unpack (urlEncode False . LB.toStrict . J.encode $ J.object ["secret" J..= J.String "*********"])
logNote "info"
dst ##> ("/_download info " <> uri)
dst <## "{\"secret\":\"*********\"}"
logNote "receiving"
let dstFile = "./tests/tmp/logo.jpg"
dst ##> ("/_download 1 " <> uri <> " " <> dstFile) -- download sucessfully discarded extra info
dst <## "started standalone receiving file 1 (logo.jpg)"
-- silent progress events
threadDelay 250000
dst <## "completed standalone receiving file 1 (logo.jpg)"
srcBody <- B.readFile "./tests/fixtures/logo.jpg"
B.readFile dstFile `shouldReturn` srcBody
testXFTPStandaloneLarge :: HasCallStack => FilePath -> IO ()
@@ -896,6 +929,39 @@ testXFTPStandaloneLarge = testChat2 aliceProfile aliceDesktopProfile $ \src dst
srcBody <- B.readFile "./tests/tmp/testfile.in"
B.readFile dstFile `shouldReturn` srcBody
testXFTPStandaloneLargeInfo :: HasCallStack => FilePath -> IO ()
testXFTPStandaloneLargeInfo = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do
withXFTPServer $ do
xftpCLI ["rand", "./tests/tmp/testfile.in", "17mb"] `shouldReturn` ["File created: " <> "./tests/tmp/testfile.in"]
logNote "sending"
src ##> "/_upload 1 ./tests/tmp/testfile.in"
src <## "started standalone uploading file 1 (testfile.in)"
-- silent progress events
threadDelay 250000
src <## "file 1 (testfile.in) uploaded, preparing redirect file 2"
src <## "file 1 (testfile.in) upload complete. download with:"
uri1 <- getTermLine src
_uri2 <- getTermLine src
_uri3 <- getTermLine src
_uri4 <- getTermLine src
let uri = uri1 <> "&data=" <> B.unpack (urlEncode False . LB.toStrict . J.encode $ J.object ["secret" J..= J.String "*********"])
logNote "info"
dst ##> ("/_download info " <> uri)
dst <## "{\"secret\":\"*********\"}"
logNote "receiving"
let dstFile = "./tests/tmp/testfile.out"
dst ##> ("/_download 1 " <> uri <> " " <> dstFile)
dst <## "started standalone receiving file 1 (testfile.out)"
-- silent progress events
threadDelay 250000
dst <## "completed standalone receiving file 1 (testfile.out)"
srcBody <- B.readFile "./tests/tmp/testfile.in"
B.readFile dstFile `shouldReturn` srcBody
testXFTPStandaloneCancelSnd :: HasCallStack => FilePath -> IO ()
testXFTPStandaloneCancelSnd = testChat2 aliceProfile aliceDesktopProfile $ \src dst -> do
withXFTPServer $ do
+8
View File
@@ -0,0 +1,8 @@
---
layout: layouts/group_link.html
title: "SimpleX Chat - Finney Forum group"
description: "Join the group of attendees of Finney Forum 2024"
groupLink: "https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FTlom_0qzRaEWo_4cweE_hzj6KBmqXC8R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAZzyx3sm1tpGsYjXAOR2LxXD0ty1hlAR7Hg0fbCxEoig%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22IfdftVGf9odVOQImmz1I9A%3D%3D%22%7D"
groupLinkText: Open Finney Forum group link
templateEngineOverride: njk
---