Compare commits

..

1 Commits

Author SHA1 Message Date
JRoberts
e099d08325 docs: ephemeral conversations rfc 2022-12-10 14:09:55 +04:00
20 changed files with 351 additions and 303 deletions

View File

@@ -248,7 +248,6 @@ It is possible to donate via:
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.

View File

@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 29
targetSdk 32
versionCode 78
versionName "4.3.2"
versionCode 77
versionName "4.3.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {

View File

@@ -11,7 +11,8 @@ import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationsMode
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.*
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
@@ -172,18 +173,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
val s = Semaphore(0)
thread(name="stdout/stderr pipe") {
Log.d(TAG, "starting server")
var server: LocalServerSocket? = null
for (i in 0..100) {
try {
server = LocalServerSocket(socketName + i)
break
} catch (e: IOException) {
Log.e(TAG, e.stackTraceToString())
}
}
if (server == null) {
throw Error("Unable to setup local server socket. Contact developers")
}
val server = LocalServerSocket(socketName)
Log.d(TAG, "started server")
s.release()
val receiver = server.accept()

View File

@@ -1259,7 +1259,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 100;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1280,7 +1280,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.3.2;
MARKETING_VERSION = 4.3.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1301,7 +1301,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 100;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1322,7 +1322,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.3.2;
MARKETING_VERSION = 4.3.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1380,7 +1380,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 100;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1393,7 +1393,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 4.3.2;
MARKETING_VERSION = 4.3.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -1410,7 +1410,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101;
CURRENT_PROJECT_VERSION = 100;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1423,7 +1423,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 4.3.2;
MARKETING_VERSION = 4.3.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;

View File

@@ -0,0 +1,208 @@
# Ephemeral conversations with existing contacts
Ephemeral conversation inside existing conversation with stricter security properties.
- additional level of encryption for message bodies & text, chat items content & text (more?) inside chat db
- ephemeral conversation key not persisted - only stored in-memory of current chat session, cleared on exiting to background
- separate tables for chat items and messages to avoid gaps in ids? TBC db pages on deletion
- don't persist chat items and messages at all? if yes - how to support multiple ephemeral conversations with different contacts in the same session - holding chat items in memory may become expensive. though only "not yet seen" items may have to be held in memory - after opening ephemeral conversation no longer keep them in memory; in this case closing ephemeral conversation screen (not fully exiting but keeping it in list of current ephemeral conversations) and re-opening also does not restore chat items, though connection and key are preserved
- if multiple ephemeral conversations are allowed - how to know they have new messages - should there be notifications for them? only local or push notifications too? not a regular notification but indication in chat list?
- disabled features? e.g. "reply" if messages aren't persisted. voice messages, files, etc.? if files are supported they are deleted exiting, should also be a part of chat start cleanup process (see below)
- contact is required to be verified to start ephemeral conversation - improves guarantee that the key for ephemeral conversation is agreed in a secure context
- no visibility of contact profile in UI
- separated with a blank screen / transition from a main conversation to prevent them appearing in the same screen
- new entity - not a contact?
- new chat type & direction, or additional dimension?
- api to start new and open existing (limited by chat session lifespan) ephemeral conversation
- api to join - also requires verified connection? one party can have contact verified, second not - prohibit until verified?
- join via special chat item? join via same button that is used to start? allow both? chat item and negotiation messages should be automatically deleted on end or on cleanup
- api to end - any side can initiate, both sides client cooperate and delete?
- a new connection is created for the conversation, deleted upon end, incognito mode doesn't affect - no profile is shared at all
- on chat start - deletes ephemeral conversations that were not ended, due to crash or another reason (get synchronously before starting?)
- controller has state of all "active" ephemeral conversations, saved are not loaded - what if one party crashes and not ends, then creates a new ephemeral conversation - previous is ended for another party?
## Design
\***
Track current ephemeral conversations in ChatController.
``` haskell
data ChatController = ChatController {
...
currentECs :: TMap ContactId EphemeralConversation
...
}
data EphemeralConversation = EphemeralConversation
{ chatItemId :: Int64,
ecState :: ECState
}
deriving (Show)
data ECState
= ECInvitationSent { localDhPrivKey :: C.PrivateKeyX25519 }
| ECInvitationReceived { localDhPubKey :: C.PublicKeyX25519 }
| ECAcptSent { sharedKey :: Maybe C.Key }
| ECAcptReceived { sharedKey :: Maybe C.Key }
| ECNegotiated { sharedKey :: Maybe C.Key }
data ECStateTag
= ECSTInvitationSent
| ECSTInvitationReceived
| ECSTAcptSent
| ECSTAcptReceived
| ECSTNegotiated
ecStateTag :: ECState -> ECStateTag
```
\***
Protocol messages:
- `XECInv C.PublicKeyX25519` - invite to ephemeral conversation, other properties except key? ECInvitation type to contain properties?
- on send: add to Controller's `currentECs` in state `ECInvitationSent` and create `CIECInvitation` chat item.
- on receive: `processXECInv` - add to Controller's `currentECs` in state `ECInvitationReceived` and create `CIECInvitation` chat item.
- `XECAcpt C.PublicKeyX25519 ConnReqInvitation` - accept ephemeral conversation, send link to join.
- on send: update Controller's `currentECs` record to state `ECAcptSent`, update chat item.
- on receive: `processXECAcpt` - update Controller's `currentECs` record to state `ECAcptReceived`, update chat item.
- `XECEnd` - message to end ephemeral conversation. Send in main connection or new one? Main may be better as it may signal cancel as well if ephemeral conversation wasn't yet accepted/negotiated.
Race condition if both parties send `XECInv` simultaneously - if `XECInv` is received when there is ephemeral conversation in `currentECs` in state `ECInvitationSent`, just remove it and signal error `CEECNegotiationError`.
\***
APIs:
- `APIStartEC ContactId` - sends `XECInv`, in UI ephemeral chat view is opened, disabled/progress indicator until ephemeral conversation is negotiated.
- `APIJoinEC ContactId` - sends `XECAcpt`, in UI ephemeral chat view is opened, disabled/progress indicator until ephemeral conversation is negotiated.
- `APIOpenEC ContactId` - loads chat items (?) for current ephemeral conversation, opens ephemeral chat view.
- api to reject? or just allow to delete chat item?
- `APIEndEC ContactId` - sends `XECEnd`, deletes connection, ephemeral conversation entity and chat items, removes from `currentECs` state, deletes `CIECInvitation` chat item, in UI chat is closed.
- terminal counterparts
ChatResponses (mirroring chat item updates for terminal):
- `CRECInvitationSent {contact :: Contact}`
- `CRECInvitationReceived {contact :: Contact}`
- `CRECAccepted {contact :: Contact}`
- `CRECAcceptReceived {contact :: Contact}`
- `CRECEnded {contact :: Contact}`
ChatErrors:
- `CEECNegotiationError {contactId :: ContactId}` - both sent `XECInv`, failed to establish connection, etc.
- `CENoCurrentEC` - on trying to open, accept, end.
- `CEECState {currentECState :: ECStateTag}` - on state errors
\***
Chat item content:
``` haskell
data CIContent (d :: MsgDirection) where
...
CIRcvECInvitation ECStateTag -> CIContent 'MDRcv
CISndECInvitation ECStateTag -> CIContent 'MDSnd
...
```
is there a need for more detailed `CIECStatus` or state tag will suffice? (see CICallStatus)
\***
New chat type, direction, chat info:
``` haskell
data ChatType = ... | CTEphemeral ...
-- new ChatType requires processing cases on ChatRef in all APIs based on it, which may be a good thing
-- e.g. automatically requires separate APISendMessage api
data ChatInfo (c :: ChatType) where
...
EphemeralChat :: ChatInfo 'CTEphemeral -- no additional information required?
...
data CIDirection (c :: ChatType) (d :: MsgDirection) where
...
CIEphemeralSnd :: CIDirection 'CTEphemeral 'MDSnd
CIEphemeralRcv :: CIDirection 'CTEphemeral 'MDRcv
...
-- same for `data CIQDirection (c :: ChatType)`
-- same for `data SChatType (c :: ChatType)`
```
Maybe it should be a new dimension and not ChatType, though it may have to be drastically different for groups and easier expressed as a separate `CTEphemeralGroup` ChatType.
Pros of not having it as contact's flag/dimension:
- no special casing in loading chat previews
- no special casing in APIs, instead it's fully fledged ChatType
- separate table for entity - cleaner deletion, no gaps
- can have separate ConnectionEntity, though it may be a con
\***
New ConnectionEntity?
- `RcvDirectEphemeralMsgConnection {entityConnection :: Connection}`
can allow to easily prohibit many protocol messages, e.g. calls, groups, etc.
\***
Database changes:
```sql
CREATE TABLE ephemeral_conversations(
ephemeral_conversation_id INTEGER PRIMARY KEY,
contact_id INTEGER REFERENCES contacts(contact_id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT CHECK(updated_at NOT NULL)
);
-- separate ec_messages and ec_chat_items tables?
-- or foreign_keys to ephemeral_conversations in existing messages and chat_items tables?
-- if files allowed: same question
-- don't save chat items and messages at all? see above. if it's separate ConnectionEntity it's not hard
ALTER TABLE connections ADD COLUMN ephemeral_conversation_id INTEGER DEFAULT NULL
REFERENCES ephemeral_conversations (ephemeral_conversation_id) ON DELETE CASCADE;
-- add logic on loading entities, e.g. for subscriptions
```
If tables for chat items and messages are separate - logic for saving encrypted message/item content, text, etc. doesn't affect existing queries and code. If it's a new connection entity type it's separate cases in api/processing anyway.
If tables for chat items and messages are reused - To/FromField don't auto convert using To/FromJSON, instead saved/loaded as string, decrypted based on flag?

View File

@@ -1,5 +1,5 @@
name: simplex-chat
version: 4.3.2
version: 4.3.1
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme

View File

@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 4.3.2
version: 4.3.1
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -67,7 +67,6 @@ library
Simplex.Chat.Migrations.M20221130_delete_item_deleted
Simplex.Chat.Migrations.M20221209_verified_connection
Simplex.Chat.Migrations.M20221210_idxs
Simplex.Chat.Migrations.M20221211_group_description
Simplex.Chat.Mobile
Simplex.Chat.Options
Simplex.Chat.ProfileGenerator

View File

@@ -1020,12 +1020,9 @@ processChatCommand = \case
APIUpdateGroupProfile groupId p' -> withUser $ \user -> do
g <- withStore $ \db -> getGroup db user groupId
runUpdateGroupProfile user g p'
UpdateGroupNames gName GroupProfile {displayName, fullName} ->
updateGroupProfileByName gName $ \p -> p {displayName, fullName}
ShowGroupProfile gName -> withUser $ \user ->
CRGroupProfile <$> withStore (\db -> getGroupInfoByName db user gName)
UpdateGroupDescription gName description ->
updateGroupProfileByName gName $ \p -> p {description}
UpdateGroupProfile gName profile -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName
processChatCommand $ APIUpdateGroupProfile groupId profile
APICreateGroupLink groupId -> withUser $ \user -> withChatLock "createGroupLink" $ do
gInfo@GroupInfo {membership = membership@GroupMember {memberRole = userRole}} <- withStore $ \db -> getGroupInfo db user groupId
when (userRole < GRAdmin) $ throwChatError CEGroupUserRole
@@ -1120,9 +1117,10 @@ processChatCommand = \case
ct@Contact {userPreferences} <- withStore $ \db -> getContactByName db user cName
let prefs' = setPreference f allowed_ $ Just userPreferences
updateContactPrefs user ct prefs'
SetGroupFeature f gName enabled ->
updateGroupProfileByName gName $ \p ->
p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p}
SetGroupFeature f gName enabled -> withUser $ \user -> do
g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> getGroup db user =<< getGroupIdByName db user gName
let p' = p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p}
runUpdateGroupProfile user g p'
QuitChat -> liftIO exitSuccess
ShowVersion -> pure $ CRVersionInfo versionNumber
DebugLocks -> do
@@ -1166,16 +1164,12 @@ processChatCommand = \case
getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd
getConnectionCode :: ConnId -> m Text
getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId)
verifyConnectionCode :: User -> Connection -> Maybe Text -> m ChatResponse
verifyConnectionCode user conn@Connection {connId} (Just code) = do
verifyConnectionCode :: User -> Connection -> Text -> m ChatResponse
verifyConnectionCode user conn@Connection {connId} code = do
code' <- getConnectionCode $ aConnId conn
let verified = sameVerificationCode code code'
when verified . withStore' $ \db -> setConnectionVerified db user connId $ Just code'
pure $ CRConnectionVerified verified code'
verifyConnectionCode user conn@Connection {connId} _ = do
code' <- getConnectionCode $ aConnId conn
withStore' $ \db -> setConnectionVerified db user connId Nothing
pure $ CRConnectionVerified False code'
pure $ CRCodeVerification verified code'
getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of
CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd (safeDecodeUtf8 msg)
@@ -1263,11 +1257,6 @@ processChatCommand = \case
toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat g') ci
createGroupFeatureChangedItems user cd CISndGroupFeature p p'
pure $ CRGroupUpdated g g' Nothing
updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> m ChatResponse
updateGroupProfileByName gName update = withUser $ \user -> do
g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db ->
getGroupIdByName db user gName >>= getGroup db user
runUpdateGroupProfile user g $ update p
isReady :: Contact -> Bool
isReady ct =
let s = connStatus $ activeConn (ct :: Contact)
@@ -1968,9 +1957,7 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
GCHostMember -> do
toView $ CRUserJoinedGroup gInfo {membership = membership {memberStatus = GSMemConnected}} m {memberStatus = GSMemConnected}
createGroupFeatureItems gInfo m
let GroupInfo {groupProfile = GroupProfile {description}} = gInfo
memberConnectedChatItem gInfo m
forM_ description $ groupDescriptionChatItem gInfo m
setActive $ ActiveG gName
showToast ("#" <> gName) "you are connected to group"
GCInviteeMember -> do
@@ -2246,10 +2233,6 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
-- ts should be broker ts but we don't have it for CON
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing
groupDescriptionChatItem :: GroupInfo -> GroupMember -> Text -> m ()
groupDescriptionChatItem gInfo m descr =
createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvMsgContent $ MCText descr) Nothing
notifyMemberConnected :: GroupInfo -> GroupMember -> m ()
notifyMemberConnected gInfo m@GroupMember {localDisplayName = c} = do
memberConnectedChatItem gInfo m
@@ -3395,7 +3378,7 @@ chatCommandP =
"/_accept " *> (APIAcceptContact <$> A.decimal),
"/_reject " *> (APIRejectContact <$> A.decimal),
"/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP),
"/call " *> char_ '@' *> (SendCallInvitation <$> displayName <*> pure defaultCallType),
("/call @" <|> "/call ") *> (SendCallInvitation <$> displayName <*> pure defaultCallType),
"/_call reject @" *> (APIRejectCall <$> A.decimal),
"/_call offer @" *> (APISendCallOffer <$> A.decimal <* A.space <*> jsonP),
"/_call answer @" *> (APISendCallAnswer <$> A.decimal <* A.space <*> jsonP),
@@ -3437,43 +3420,42 @@ chatCommandP =
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
"/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal),
"/_info @" *> (APIContactInfo <$> A.decimal),
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName),
("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName),
("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
("/info @" <|> "/info " <|> "/i @" <|> "/i ") *> (ContactInfo <$> displayName),
"/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal),
"/_switch @" *> (APISwitchContact <$> A.decimal),
"/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName),
"/switch " *> char_ '@' *> (SwitchContact <$> displayName),
"/switch #" *> (SwitchGroupMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
("/switch @" <|> "/switch ") *> (SwitchContact <$> displayName),
"/_get code @" *> (APIGetContactCode <$> A.decimal),
"/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal),
"/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> textP)),
"/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> textP)),
"/code " *> char_ '@' *> (GetContactCode <$> displayName),
"/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* char_ '@' <*> displayName),
"/verify " *> char_ '@' *> (VerifyContact <$> displayName <*> optional (A.space *> textP)),
"/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> optional (A.space *> textP)),
"/_verify code @" *> (APIVerifyContact <$> A.decimal <* A.space <*> textP),
"/_verify code @" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <* A.space <*> textP),
("/code @" <|> "/code ") *> (GetContactCode <$> displayName),
"/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
("/verify @" <|> "/verify ") *> (VerifyContact <$> displayName <* A.space <*> textP),
"/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName <* A.space <*> textP),
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
("/help messages" <|> "/hm") $> ChatHelp HSMessages,
("/help settings" <|> "/hs") $> ChatHelp HSSettings,
("/help" <|> "/h") $> ChatHelp HSMain,
("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile),
("/group #" <|> "/group " <|> "/g #" <|> "/g ") *> (NewGroup <$> groupProfile),
"/_group " *> (NewGroup <$> jsonP),
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName),
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayName),
("/add #" <|> "/add " <|> "/a #" <|> "/a ") *> (AddMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName <*> memberRole),
("/join #" <|> "/join " <|> "/j #" <|> "/j ") *> (JoinGroup <$> displayName),
("/member role #" <|> "/member role " <|> "/mr #" <|> "/mr ") *> (MemberRole <$> displayName <* A.space <* optional (A.char '@') <*> displayName <*> memberRole),
("/remove #" <|> "/remove " <|> "/rm #" <|> "/rm ") *> (RemoveMember <$> displayName <* A.space <* optional (A.char '@') <*> displayName),
("/leave #" <|> "/leave " <|> "/l #" <|> "/l ") *> (LeaveGroup <$> displayName),
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName),
("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName),
("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName),
"/clear #" *> (ClearGroup <$> displayName),
"/clear " *> char_ '@' *> (ClearContact <$> displayName),
("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayName),
("/clear @" <|> "/clear ") *> (ClearContact <$> displayName),
("/members #" <|> "/members " <|> "/ms #" <|> "/ms ") *> (ListMembers <$> displayName),
("/groups" <|> "/gs") $> ListGroups,
"/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP),
("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile),
("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName),
"/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> (jsonP <|> textP))),
-- TODO group profile update via terminal should not reset image and preferences to Nothing (now it does)
("/group_profile #" <|> "/gp #" <|> "/group_profile " <|> "/gp ") *> (UpdateGroupProfile <$> displayName <* A.space <*> groupProfile),
"/_create link #" *> (APICreateGroupLink <$> A.decimal),
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
"/_get link #" *> (APIGetGroupLink <$> A.decimal),
@@ -3481,7 +3463,7 @@ chatCommandP =
"/delete link #" *> (DeleteGroupLink <$> displayName),
"/show link #" *> (ShowGroupLink <$> displayName),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> A.takeByteString),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> A.takeByteString),
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* optional (A.char '@') <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> A.takeByteString),
("/contacts" <|> "/cs") $> ListContacts,
("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)),
("/connect" <|> "/c") $> AddContact,
@@ -3505,8 +3487,8 @@ chatCommandP =
("/delete_address" <|> "/da") $> DeleteMyAddress,
("/show_address" <|> "/sa") $> ShowMyAddress,
"/auto_accept " *> (AddressAutoAccept <$> autoAcceptP),
("/accept " <|> "/ac ") *> char_ '@' *> (AcceptContact <$> displayName),
("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayName),
("/accept @" <|> "/accept " <|> "/ac @" <|> "/ac ") *> (AcceptContact <$> displayName),
("/reject @" <|> "/reject " <|> "/rc @" <|> "/rc ") *> (RejectContact <$> displayName),
("/markdown" <|> "/m") $> ChatHelp HSMarkdown,
("/welcome" <|> "/w") $> Welcome,
"/profile_image " *> (UpdateProfileImage . Just . ImageData <$> imageP),
@@ -3555,7 +3537,7 @@ chatCommandP =
gName <- displayName
fullName <- fullNameP gName
let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) {directMessages = Just GroupPreference {enable = FEOn}}
pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences}
pure GroupProfile {displayName = gName, fullName, image = Nothing, groupPreferences}
fullNameP name = do
n <- (A.space *> A.takeByteString) <|> pure ""
pure $ if B.null n then name else safeDecodeUtf8 n
@@ -3593,7 +3575,6 @@ chatCommandP =
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP)))
(pure Nothing)
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
char_ = optional . A.char
adminContactReq :: ConnReqContact
adminContactReq =

View File

@@ -202,8 +202,8 @@ data ChatCommand
| APISwitchGroupMember GroupId GroupMemberId
| APIGetContactCode ContactId
| APIGetGroupMemberCode GroupId GroupMemberId
| APIVerifyContact ContactId (Maybe Text)
| APIVerifyGroupMember GroupId GroupMemberId (Maybe Text)
| APIVerifyContact ContactId Text
| APIVerifyGroupMember GroupId GroupMemberId Text
| ShowMessages ChatName Bool
| ContactInfo ContactName
| GroupMemberInfo GroupName ContactName
@@ -211,8 +211,8 @@ data ChatCommand
| SwitchGroupMember GroupName ContactName
| GetContactCode ContactName
| GetGroupMemberCode GroupName ContactName
| VerifyContact ContactName (Maybe Text)
| VerifyGroupMember GroupName ContactName (Maybe Text)
| VerifyContact ContactName Text
| VerifyGroupMember GroupName ContactName Text
| ChatHelp HelpSection
| Welcome
| AddContact
@@ -242,9 +242,7 @@ data ChatCommand
| ClearGroup GroupName
| ListMembers GroupName
| ListGroups
| UpdateGroupNames GroupName GroupProfile
| ShowGroupProfile GroupName
| UpdateGroupDescription GroupName (Maybe Text)
| UpdateGroupProfile GroupName GroupProfile
| CreateGroupLink GroupName
| DeleteGroupLink GroupName
| ShowGroupLink GroupName
@@ -288,7 +286,7 @@ data ChatResponse
| CRGroupMemberSwitch {groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress}
| CRContactCode {contact :: Contact, connectionCode :: Text}
| CRGroupMemberCode {groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
| CRConnectionVerified {verified :: Bool, expectedCode :: Text}
| CRCodeVerification {verified :: Bool, expectedCode :: Text}
| CRNewChatItem {chatItem :: AChatItem}
| CRChatItemStatusUpdated {chatItem :: AChatItem}
| CRChatItemUpdated {chatItem :: AChatItem}
@@ -370,7 +368,6 @@ data ChatResponse
| CRGroupRemoved {groupInfo :: GroupInfo}
| CRGroupDeleted {groupInfo :: GroupInfo, member :: GroupMember}
| CRGroupUpdated {fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
| CRGroupProfile {groupInfo :: GroupInfo}
| CRGroupLinkCreated {groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
| CRGroupLink {groupInfo :: GroupInfo, connReqContact :: ConnReqContact}
| CRGroupLinkDeleted {groupInfo :: GroupInfo}

View File

@@ -123,12 +123,9 @@ groupsHelpInfo =
indent <> highlight "/leave <group> " <> " - leave group",
indent <> highlight "/delete <group> " <> " - delete group",
indent <> highlight "/members <group> " <> " - list group members",
indent <> highlight "/gp <group> " <> " - view group profile",
indent <> highlight "/gp <group> <new_name> [<full_name>] " <> " - update group profile",
indent <> highlight "/group_descr <group> [<descr>] " <> " - update/remove group description",
indent <> highlight "/groups " <> " - list groups",
indent <> highlight "#<group> <message> " <> " - send message to group",
indent <> highlight "/create link #<group> " <> " - create public group link",
"",
"The commands may be abbreviated: " <> listHighlight ["/g", "/a", "/j", "/rm", "/l", "/d", "/ms", "/gs"]
]

View File

@@ -1,12 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20221211_group_description where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20221211_group_description :: Query
m20221211_group_description =
[sql|
ALTER TABLE group_profiles ADD COLUMN description TEXT NULL;
|]

View File

@@ -116,8 +116,7 @@ CREATE TABLE group_profiles(
updated_at TEXT CHECK(updated_at NOT NULL),
image TEXT,
user_id INTEGER DEFAULT NULL REFERENCES users ON DELETE CASCADE,
preferences TEXT,
description TEXT NULL
preferences TEXT
);
CREATE TABLE groups(
group_id INTEGER PRIMARY KEY, -- local group ID

View File

@@ -307,7 +307,6 @@ import Simplex.Chat.Migrations.M20221129_delete_group_feature_items
import Simplex.Chat.Migrations.M20221130_delete_item_deleted
import Simplex.Chat.Migrations.M20221209_verified_connection
import Simplex.Chat.Migrations.M20221210_idxs
import Simplex.Chat.Migrations.M20221211_group_description
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..))
@@ -360,8 +359,7 @@ schemaMigrations =
("20221129_delete_group_feature_items", m20221129_delete_group_feature_items),
("20221130_delete_item_deleted", m20221130_delete_item_deleted),
("20221209_verified_connection", m20221209_verified_connection),
("20221210_idxs", m20221210_idxs),
("20221211_group_description", m20221211_group_description)
("20221210_idxs", m20221210_idxs)
]
-- | The list of migrations in ascending order by date
@@ -1511,7 +1509,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
[sql|
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1612,7 +1610,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
[sql|
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1652,15 +1650,15 @@ updateConnectionStatus db Connection {connId} connStatus = do
-- | creates completely new group with a single member - the current user
createNewGroup :: DB.Connection -> TVar ChaChaDRG -> User -> GroupProfile -> ExceptT StoreError IO GroupInfo
createNewGroup db gVar user@User {userId} groupProfile = ExceptT $ do
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences
currentTs <- getCurrentTime
withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
groupId <- liftIO $ do
DB.execute
db
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
"INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
@@ -1696,7 +1694,7 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId)
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId)
createGroupInvitation_ = do
let GroupProfile {displayName, fullName, description, image, groupPreferences} = groupProfile
let GroupProfile {displayName, fullName, image, groupPreferences} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences
ExceptT $
withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do
@@ -1704,8 +1702,8 @@ createGroupInvitation db user@User {userId} contact@Contact {contactId, activeCo
groupId <- liftIO $ do
DB.execute
db
"INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, description, image, userId, groupPreferences, currentTs, currentTs)
"INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)"
(displayName, fullName, image, userId, groupPreferences, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
@@ -1851,7 +1849,7 @@ getUserGroupDetails db User {userId, userContactId} =
<$> DB.query
db
[sql|
SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
mu.group_member_id, g.group_id, mu.member_id, mu.member_role, mu.member_category, mu.member_status,
mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.local_alias, pu.preferences
FROM groups g
@@ -1885,15 +1883,14 @@ getGroupInfoByName db user gName = do
gId <- getGroupIdByName db user gName
getGroupInfo db user gId
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe GroupPreferences, UTCTime, UTCTime) :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe GroupPreferences, UTCTime, UTCTime) :. GroupMemberRow
toGroupInfo :: Int64 -> GroupInfoRow -> GroupInfo
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, groupPreferences, createdAt, updatedAt) :. userMemberRow) =
toGroupInfo userContactId ((groupId, localDisplayName, displayName, fullName, image, hostConnCustomUserProfileId, enableNtfs_, groupPreferences, createdAt, updatedAt) :. userMemberRow) =
let membership = toGroupMember userContactId userMemberRow
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_}
fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
in GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, fullName, image, groupPreferences}, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt}
getGroupMember :: DB.Connection -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember
getGroupMember db user@User {userId} groupId groupMemberId =
@@ -2369,7 +2366,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
[sql|
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
-- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -3351,7 +3348,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do
[sql|
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -3717,7 +3714,7 @@ getGroupInfo db User {userId, userContactId} groupId =
[sql|
SELECT
-- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.image, g.host_conn_custom_user_profile_id, g.enable_ntfs, gp.preferences, g.created_at, g.updated_at,
-- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.member_role, mu.member_category,
mu.member_status, mu.invited_by, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -3731,7 +3728,7 @@ getGroupInfo db User {userId, userContactId} groupId =
(groupId, userId, userContactId)
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, image, groupPreferences}
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
updateGroupProfile_ currentTs $> (g :: GroupInfo) {groupProfile = p', fullGroupPreferences}
@@ -3748,14 +3745,14 @@ updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, grou
db
[sql|
UPDATE group_profiles
SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, updated_at = ?
SET display_name = ?, full_name = ?, image = ?, preferences = ?, updated_at = ?
WHERE group_profile_id IN (
SELECT group_profile_id
FROM groups
WHERE user_id = ? AND group_id = ?
)
|]
(newName, fullName, description, image, groupPreferences, currentTs, userId, groupId)
(newName, fullName, image, groupPreferences, currentTs, userId, groupId)
updateGroup_ ldn currentTs = do
DB.execute
db

View File

@@ -724,7 +724,6 @@ fromLocalProfile LocalProfile {displayName, fullName, image, preferences} =
data GroupProfile = GroupProfile
{ displayName :: GroupName,
fullName :: Text,
description :: Maybe Text,
image :: Maybe ImageData,
groupPreferences :: Maybe GroupPreferences
}

View File

@@ -74,7 +74,7 @@ responseToView user_ testView ts = \case
CRGroupMemberInfo g m cStats -> viewGroupMemberInfo g m cStats
CRContactSwitch ct progress -> viewContactSwitch ct progress
CRGroupMemberSwitch g m progress -> viewGroupMemberSwitch g m progress
CRConnectionVerified verified code -> [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
CRCodeVerification verified code -> [plain $ if verified then "connection verified" else "error: current connection code is " <> code]
CRContactCode ct code -> viewContactCode ct code testView
CRGroupMemberCode g m code -> viewGroupMemberCode g m code testView
CRNewChatItem (AChatItem _ _ chat item) -> unmuted chat item $ viewChatItem chat item False ts
@@ -184,7 +184,6 @@ responseToView user_ testView ts = \case
CRGroupRemoved g -> [ttyFullGroup g <> ": you are no longer a member or group deleted"]
CRGroupDeleted g m -> [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"]
CRGroupUpdated g g' m -> viewGroupUpdated g g' m
CRGroupProfile g -> viewGroupProfile g
CRGroupLinkCreated g cReq -> groupLink_ "Group link is created!" g cReq
CRGroupLink g cReq -> groupLink_ "Group link:" g cReq
CRGroupLinkDeleted g -> viewGroupLinkDeleted g
@@ -489,9 +488,9 @@ viewReceivedContactRequest c Profile {fullName} =
]
viewGroupCreated :: GroupInfo -> [StyledString]
viewGroupCreated g@GroupInfo {localDisplayName = n} =
viewGroupCreated g@GroupInfo {localDisplayName} =
[ "group " <> ttyFullGroup g <> " is created",
"to add members use " <> highlight ("/a " <> n <> " <name>") <> " or " <> highlight ("/create link #" <> n)
"use " <> highlight ("/a " <> localDisplayName <> " <name>") <> " to add members"
]
viewCannotResendInvitation :: GroupInfo -> ContactName -> [StyledString]
@@ -582,7 +581,7 @@ viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView
where
message =
[ ttyFullContact ct <> ": contact is connected, your incognito profile for this contact is " <> incognitoProfile' profile,
"use " <> highlight ("/i " <> localDisplayName) <> " to print out this incognito profile again"
"use " <> highlight ("/info " <> localDisplayName) <> " to print out this incognito profile again"
]
Nothing ->
[ttyFullContact ct <> ": contact is connected"]
@@ -810,8 +809,8 @@ viewCountactUserPref = \case
viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> [StyledString]
viewGroupUpdated
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, description, image, groupPreferences = gps}}
g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', description = description', image = image', groupPreferences = gps'}}
GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, image, groupPreferences = gps}}
g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', image = image', groupPreferences = gps'}}
m = do
let update = groupProfileUpdated <> groupPrefsUpdated
if null update
@@ -819,35 +818,21 @@ viewGroupUpdated
else memberUpdated <> update
where
memberUpdated = maybe [] (\m' -> [ttyMember m' <> " updated group " <> ttyGroup n <> ":"]) m
groupProfileUpdated =
["changed to " <> ttyFullGroup g' | n /= n']
<> ["full name " <> if T.null fullName' || fullName' == n' then "removed" else "changed to: " <> plain fullName' | n == n' && fullName /= fullName']
<> ["profile image " <> maybe "removed" (const "updated") image' | image /= image']
<> (if description == description' then [] else maybe ["description removed"] ((bold' "description changed to:" :) . map plain . T.lines) description')
groupProfileUpdated
| n == n' && fullName == fullName' && image == image' = []
| n == n' && fullName == fullName' = ["profile image " <> (if isNothing image' then "removed" else "updated")]
| n == n' = ["full name " <> if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName']
| otherwise = ["changed to " <> ttyFullGroup g']
groupPrefsUpdated
| null prefs = []
| otherwise = bold' "updated group preferences:" : prefs
| otherwise = "updated group preferences:" : prefs
where
prefs = mapMaybe viewPref allGroupFeatures
viewPref pt
| pref gps == pref gps' = Nothing
| otherwise = Just $ plain (groupFeatureToText pt) <> " enabled: " <> plain (groupPrefToText $ pref gps')
where
pref = getGroupPreference pt . mergeGroupPreferences
viewGroupProfile :: GroupInfo -> [StyledString]
viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {description, image, groupPreferences = gps}} =
[ttyFullGroup g]
<> maybe [] (const ["has profile image"]) image
<> maybe [] ((bold' "description:" :) . map plain . T.lines) description
<> (bold' "group preferences:" : map viewPref allGroupFeatures)
where
viewPref pt = plain (groupFeatureToText pt) <> " enabled: " <> plain (groupPrefToText $ pref gps)
where
pref = getGroupPreference pt . mergeGroupPreferences
bold' :: String -> StyledString
bold' = styled Bold
pref pss = getGroupPreference pt $ mergeGroupPreferences pss
viewContactAliasUpdated :: Contact -> [StyledString]
viewContactAliasUpdated Contact {localDisplayName = n, profile = LocalProfile {localAlias}}

View File

@@ -215,7 +215,7 @@ getTermLine :: TestCC -> IO String
getTermLine cc =
5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case
Just s -> do
-- uncomment 2 lines below to echo virtual terminal
-- uncomment code below to echo virtual terminal
-- name <- userName cc
-- putStrLn $ name <> ": " <> s
pure s

View File

@@ -76,7 +76,6 @@ chatTests = do
it "update group profile" testUpdateGroupProfile
it "update member role" testUpdateMemberRole
it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts
it "group description is shown as the first message to new members" testGroupDescription
describe "async group connections" $ do
xit "create and join group when clients go offline" testGroupAsync
describe "user profiles" $ do
@@ -499,7 +498,7 @@ testGroupShared alice bob cath checkMessages = do
connectUsers alice cath
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -634,7 +633,7 @@ testGroup2 =
connectUsers alice dan
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice <## "use /a club <name> to add members"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
@@ -852,10 +851,10 @@ testGroupSameName =
\alice _ -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/g team"
alice <## "group #team_1 (team) is created"
alice <## "to add members use /a team_1 <name> or /create link #team_1"
alice <## "use /a team_1 <name> to add members"
testGroupDeleteWhenInvited :: IO ()
testGroupDeleteWhenInvited =
@@ -864,7 +863,7 @@ testGroupDeleteWhenInvited =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -891,7 +890,7 @@ testGroupReAddInvited =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -926,7 +925,7 @@ testGroupReAddInvitedChangeRole =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -966,7 +965,7 @@ testGroupDeleteInvitedContact =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -997,7 +996,7 @@ testDeleteGroupMemberProfileKept =
-- group 1
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -1016,7 +1015,7 @@ testDeleteGroupMemberProfileKept =
-- group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice <## "use /a club <name> to add members"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
@@ -1109,7 +1108,7 @@ testGroupList =
createGroup2 "team" alice bob
alice ##> "/g tennis"
alice <## "group #tennis is created"
alice <## "to add members use /a tennis <name> or /create link #tennis"
alice <## "use /a tennis <name> to add members"
alice ##> "/a tennis bob"
concurrentlyN_
[ alice <## "invitation to join the group #tennis sent to bob",
@@ -1378,7 +1377,7 @@ testUpdateMemberRole =
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
addMember "team" alice bob GRAdmin
alice ##> "/mr team bob member"
alice <## "#team: you changed the role of bob from admin to member"
@@ -1426,7 +1425,7 @@ testGroupDeleteUnusedContacts =
-- create group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice <## "use /a club <name> to add members"
alice ##> "/a club bob"
concurrentlyN_
[ alice <## "invitation to join the group #club sent to bob",
@@ -1501,70 +1500,6 @@ testGroupDeleteUnusedContacts =
cath ##> ("/d #" <> group)
cath <## ("#" <> group <> ": you deleted the group")
testGroupDescription :: IO ()
testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
addMember "team" alice bob GRAdmin
bob ##> "/j team"
concurrentlyN_
[ alice <## "#team: bob joined the group",
bob <## "#team: you joined the group"
]
alice ##> "/group_profile team"
alice <## "#team"
groupInfo alice
alice ##> "/group_descr team Welcome to the team!"
alice <## "description changed to:"
alice <## "Welcome to the team!"
bob <## "alice updated group #team:"
bob <## "description changed to:"
bob <## "Welcome to the team!"
alice ##> "/group_profile team"
alice <## "#team"
alice <## "description:"
alice <## "Welcome to the team!"
groupInfo alice
connectUsers alice cath
addMember "team" alice cath GRMember
cath ##> "/j team"
concurrentlyN_
[ alice <## "#team: cath joined the group",
do
cath <## "#team: you joined the group"
cath <# "#team alice> Welcome to the team!"
cath <## "#team: member bob (Bob) is connected",
do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected"
]
connectUsers bob dan
addMember "team" bob dan GRMember
dan ##> "/j team"
concurrentlyN_
[ bob <## "#team: dan joined the group",
do
dan <## "#team: you joined the group"
dan <# "#team bob> Welcome to the team!"
dan
<### [ "#team: member alice (Alice) is connected",
"#team: member cath (Catherine) is connected"
],
bobAddedDan alice,
bobAddedDan cath
]
where
groupInfo alice = do
alice <## "group preferences:"
alice <## "Direct messages enabled: on"
alice <## "Full deletion enabled: off"
alice <## "Voice messages enabled: on"
bobAddedDan cc = do
cc <## "#team: bob added dan (Daniel) to the group (connecting...)"
cc <## "#team: new member dan is connected"
testGroupAsync :: IO ()
testGroupAsync = withTmpFiles $ do
print (0 :: Integer)
@@ -1573,7 +1508,7 @@ testGroupAsync = withTmpFiles $ do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/a team bob"
concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob",
@@ -2756,7 +2691,7 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $
do
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice
<### [ "use /i bob to print out this incognito profile again",
<### [ "use /info bob to print out this incognito profile again",
WithTime "i @bob hello!"
]
]
@@ -2775,10 +2710,10 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi
concurrentlyN_
[ do
bob <## (aliceIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i " <> aliceIncognito <> " to print out this incognito profile again"),
bob <## ("use /info " <> aliceIncognito <> " to print out this incognito profile again"),
do
alice <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
alice <## ("use /info " <> bobIncognito <> " to print out this incognito profile again")
]
-- after turning incognito mode off conversation is incognito
alice #$> ("/incognito off", id, "ok")
@@ -2857,7 +2792,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
concurrentlyN_
[ do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i alice to print out this incognito profile again",
bob <## "use /info alice to print out this incognito profile again",
alice <## (bobIncognito <> ": contact is connected")
]
-- after turning incognito mode off conversation is incognito
@@ -2893,7 +2828,7 @@ testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $
[ bob <## (aliceIncognito <> ": contact is connected"),
do
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## "use /i bob to print out this incognito profile again"
alice <## "use /info bob to print out this incognito profile again"
]
-- after turning incognito mode off conversation is incognito
alice #$> ("/incognito off", id, "ok")
@@ -2932,13 +2867,13 @@ testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfil
concurrentlyN_
[ do
cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito)
cath <## "use /i alice to print out this incognito profile again",
cath <## "use /info alice to print out this incognito profile again",
alice <## (cathIncognito <> ": contact is connected")
]
-- alice creates group
alice ##> "/g secret_club"
alice <## "group #secret_club is created"
alice <## "to add members use /a secret_club <name> or /create link #secret_club"
alice <## "use /a secret_club <name> to add members"
-- alice invites bob
alice ##> "/a secret_club bob"
concurrentlyN_
@@ -3118,13 +3053,13 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $
[ bob <## (aliceIncognito <> ": contact is connected"),
do
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## "use /i bob to print out this incognito profile again"
alice <## "use /info bob to print out this incognito profile again"
]
-- alice creates group non incognito
alice #$> ("/incognito off", id, "ok")
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice <## "use /a club <name> to add members"
alice ##> "/a club bob"
alice <## "you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"
-- bob doesn't receive invitation
@@ -3148,7 +3083,7 @@ testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathPr
[ bob <## (aliceIncognito <> ": contact is connected"),
do
alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito)
alice <## "use /i bob to print out this incognito profile again",
alice <## "use /info bob to print out this incognito profile again",
do
cath <## "alice (Alice): contact is connected"
]
@@ -3197,12 +3132,12 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr
[ alice <## (bobIncognito <> ": contact is connected"),
do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i alice to print out this incognito profile again"
bob <## "use /info alice to print out this incognito profile again"
]
-- bob joins group using incognito profile
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> ("/a team " <> bobIncognito)
concurrentlyN_
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
@@ -3249,12 +3184,12 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr
[ alice <## (bobIncognito <> ": contact is connected"),
do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i alice to print out this incognito profile again"
bob <## "use /info alice to print out this incognito profile again"
]
-- bob joins group using incognito profile
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> ("/a team " <> bobIncognito)
concurrentlyN_
[ alice <## ("invitation to join the group #team sent to " <> bobIncognito),
@@ -3514,19 +3449,19 @@ testProhibitDirectMessages =
addMember "team" cath dan GRMember
dan ##> "/j #team"
concurrentlyN_
[ cath <## "#team: dan joined the group",
[ cath <## ("#team: dan joined the group"),
do
dan <## "#team: you joined the group"
dan <## ("#team: you joined the group")
dan
<### [ "#team: member alice (Alice) is connected",
"#team: member bob (Bob) is connected"
],
do
alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
alice <## "#team: new member dan is connected",
alice <## ("#team: cath added dan (Daniel) to the group (connecting...)")
alice <## ("#team: new member dan is connected"),
do
bob <## "#team: cath added dan (Daniel) to the group (connecting...)"
bob <## "#team: new member dan is connected"
bob <## ("#team: cath added dan (Daniel) to the group (connecting...)")
bob <## ("#team: new member dan is connected")
]
alice ##> "@dan hi"
alice <## "direct messages to indirect contact dan are prohibited"
@@ -3573,7 +3508,7 @@ testTestSMPServerConnection =
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001"
alice <## "SMP server test passed"
alice ##> "/smp test smp://LcJU@localhost:5001"
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:5001 NETWORK"
alice <## ("SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:5001 NETWORK")
alice <## "Possibly, certificate fingerprint in server address is incorrect"
testAsyncInitiatingOffline :: IO ()
@@ -4166,7 +4101,7 @@ testGroupLink =
\alice bob cath -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/show link #team"
alice <## "no group link, to create: /create link #team"
alice ##> "/create link #team"
@@ -4266,7 +4201,7 @@ testGroupLinkDeleteGroupRejoin =
\alice bob -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" True
bob ##> ("/c " <> gLink)
@@ -4322,7 +4257,7 @@ testGroupLinkContactUsed =
\alice bob -> do
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/create link #team"
gLink <- getGroupLink alice "team" True
bob ##> ("/c " <> gLink)
@@ -4361,14 +4296,14 @@ testGroupLinkIncognitoMembership =
concurrentlyN_
[ do
bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i alice to print out this incognito profile again",
bob <## "use /info alice to print out this incognito profile again",
alice <## (bobIncognito <> ": contact is connected")
]
bob #$> ("/incognito off", id, "ok")
-- alice creates group
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
-- alice invites bob
alice ##> ("/a team " <> bobIncognito)
concurrentlyN_
@@ -4391,7 +4326,7 @@ testGroupLinkIncognitoMembership =
concurrentlyN_
[ do
bob <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## "use /i cath to print out this incognito profile again"
bob <## "use /info cath to print out this incognito profile again"
bob <## "cath invited to group #team via your group link"
bob <## "#team: cath joined the group",
do
@@ -4417,12 +4352,12 @@ testGroupLinkIncognitoMembership =
concurrentlyN_
[ do
bob <## (danIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i " <> danIncognito <> " to print out this incognito profile again")
bob <## ("use /info " <> danIncognito <> " to print out this incognito profile again")
bob <## (danIncognito <> " invited to group #team via your group link")
bob <## ("#team: " <> danIncognito <> " joined the group"),
do
dan <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> danIncognito)
dan <## ("use /i " <> bobIncognito <> " to print out this incognito profile again")
dan <## ("use /info " <> bobIncognito <> " to print out this incognito profile again")
dan <## ("#team: you joined the group incognito as " <> danIncognito)
dan
<### [ "#team: member alice (Alice) is connected",
@@ -4472,7 +4407,7 @@ testGroupLinkUnusedHostContactDeleted =
-- create group 1
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice <## "use /a team <name> to add members"
alice ##> "/create link #team"
gLinkTeam <- getGroupLink alice "team" True
bob ##> ("/c " <> gLinkTeam)
@@ -4490,7 +4425,7 @@ testGroupLinkUnusedHostContactDeleted =
-- create group 2
alice ##> "/g club"
alice <## "group #club is created"
alice <## "to add members use /a club <name> or /create link #club"
alice <## "use /a club <name> to add members"
alice ##> "/create link #club"
gLinkClub <- getGroupLink alice "club" True
bob ##> ("/c " <> gLinkClub)
@@ -4563,7 +4498,7 @@ testGroupLinkIncognitoUnusedHostContactsDeleted =
createGroupBobIncognito alice bob group bobsAliceContact = do
alice ##> ("/g " <> group)
alice <## ("group #" <> group <> " is created")
alice <## ("to add members use /a " <> group <> " <name> or /create link #" <> group)
alice <## ("use /a " <> group <> " <name> to add members")
alice ##> ("/create link #" <> group)
gLinkTeam <- getGroupLink alice group True
bob ##> ("/c " <> gLinkTeam)
@@ -4578,7 +4513,7 @@ testGroupLinkIncognitoUnusedHostContactsDeleted =
alice <## ("#" <> group <> ": " <> bobIncognito <> " joined the group"),
do
bob <## (bobsAliceContact <> " (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito)
bob <## ("use /i " <> bobsAliceContact <> " to print out this incognito profile again")
bob <## ("use /info " <> bobsAliceContact <> " to print out this incognito profile again")
bob <## ("#" <> group <> ": you joined the group incognito as " <> bobIncognito)
]
pure bobIncognito
@@ -4637,17 +4572,12 @@ testMarkContactVerified =
aCode <- getTermLine bob
bCode `shouldBe` aCode
alice ##> "/verify bob 123"
alice <##. "connection not verified, current code is "
alice <##. "error: current connection code is "
alice ##> ("/verify bob " <> aCode)
alice <## "connection verified"
alice ##> "/i bob"
bobInfo alice
alice <## "connection verified"
alice ##> "/verify bob"
alice <##. "connection not verified, current code is "
alice ##> "/i bob"
bobInfo alice
alice <## "connection not verified, use /code command to see security code"
where
bobInfo alice = do
alice <## "contact ID: 2"
@@ -4668,17 +4598,12 @@ testMarkGroupMemberVerified =
aCode <- getTermLine bob
bCode `shouldBe` aCode
alice ##> "/verify #team bob 123"
alice <##. "connection not verified, current code is "
alice <##. "error: current connection code is "
alice ##> ("/verify #team bob " <> aCode)
alice <## "connection verified"
alice ##> "/i #team bob"
bobInfo alice
alice <## "connection verified"
alice ##> "/verify #team bob"
alice <##. "connection not verified, current code is "
alice ##> "/i #team bob"
bobInfo alice
alice <## "connection not verified, use /code command to see security code"
where
bobInfo alice = do
alice <## "group ID: 1"
@@ -4776,7 +4701,7 @@ createGroup2 gName cc1 cc2 = do
name2 <- userName cc2
cc1 ##> ("/g " <> gName)
cc1 <## ("group #" <> gName <> " is created")
cc1 <## ("to add members use /a " <> gName <> " <name> or /create link #" <> gName)
cc1 <## ("use /a " <> gName <> " <name> to add members")
addMember gName cc1 cc2 GRAdmin
cc2 ##> ("/j " <> gName)
concurrently_

View File

@@ -89,7 +89,7 @@ testProfile :: Profile
testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), preferences = testChatPreferences}
testGroupProfile :: GroupProfile
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, image = Nothing, groupPreferences = testGroupPreferences}
testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", image = Nothing, groupPreferences = testGroupPreferences}
decodeChatMessageTest :: Spec
decodeChatMessageTest = describe "Chat message encoding/decoding" $ do

View File

@@ -1,15 +0,0 @@
# .well-known
This website files allow opening SimpleX Chat links (1-time invitations, contact addresses and groups) directly in the app.
## Android
File `assetlinks.json` includes certificate hashes for:
- Play Store (5E:3E:DC:C2:00:FB:A8:D5:F4:88:F3:CA:4C:32:5B:05:78:C5:6A:9C:03:A1:CC:B5:92:9C:D7:5C:7E:57:E2:4D)
- APK in GitHub releases (3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85)
- F-Droid (AE:C1:95:DC:FD:46:14:BD:3A:91:EC:26:D1:D5:14:C8:75:71:C5:CC:8D:CF:48:08:3F:92:83:14:3C:A2:B9:A6)
## iOS
`apple-app-site-association` currently does not work, as it needs to be served with `Content-type: application/json; charset=utf-8` and GitHub pages do not support adding this header to files without JSON extension.

View File

@@ -8,8 +8,7 @@
"package_name": "chat.simplex.app",
"sha256_cert_fingerprints": [
"5E:3E:DC:C2:00:FB:A8:D5:F4:88:F3:CA:4C:32:5B:05:78:C5:6A:9C:03:A1:CC:B5:92:9C:D7:5C:7E:57:E2:4D",
"3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85",
"AE:C1:95:DC:FD:46:14:BD:3A:91:EC:26:D1:D5:14:C8:75:71:C5:CC:8D:CF:48:08:3F:92:83:14:3C:A2:B9:A6"
"3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85"
]
}
}