Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin committed 2026-01-13 22:37:27 +00:00
commit 7356be0efc
103 files changed
+1917 -1607

No files matched your search

+71
View File
@@ -582,3 +582,74 @@ jobs:
bin_hash: ${{ steps.windows_desktop_build.outputs.package_hash }}
github_ref: ${{ github.ref }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# =========================
# NodeJS libs release
# =========================
# Downloads Desktop builds, extracts and archives libraries for NodeJS addon.
# Depends on Linux/MacOS, executes only on release.
# Secrets:
# -------
# NODEJS_REPO_TOKEN
# Only select repositories: simplex-chat-libs
# Permissions:
# * Contents (Read and Write)
release-nodejs-libs:
runs-on: ubuntu-latest
needs: [build-linux, build-macos]
if: startsWith(github.ref, 'refs/tags/v') && (!cancelled())
steps:
- name: Checkout current repository
uses: actions/checkout@v6
- name: Build archives
run: |
INIT_DIR='${{ runner.temp }}/artifacts'
RELEASE_DIR='${{ runner.temp }}/release-assets'
TAG='${{ github.ref_name }}'
URL='https://github.com/${{ github.repository }}/releases/download'
PREFIX='${{ github.event.repository.name }}-libs'
# Setup directories
mkdir "$INIT_DIR" "$RELEASE_DIR" && cd "$INIT_DIR"
# Downlaod desktop release
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-ubuntu-22_04-x86_64.deb" -o linux.deb
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-aarch64.dmg" -o macos-aarch64.dmg
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-x86_64.dmg" -o macos-x86_64.dmg
# Linux
# -----
# Extract libraries
dpkg-deb -R linux.deb linux-out/ && cd linux-out/opt/simplex/lib/app/resources
# Preprare directory
mkdir libs && cp *.so libs/
# Archive
zip -r "${PREFIX}-linux-x86_64.zip" libs
# Back to original dir
mv "${PREFIX}-linux-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
# MacOS: aarch64
# --------------
7z x macos-aarch64.dmg -omacos1-out/ && cd macos1-out/SimpleX/SimpleX.app/Contents/app/resources/
mkdir libs && cp *.dylib libs/
zip -r "${PREFIX}-macos-aarch64.zip" libs
mv "${PREFIX}-macos-aarch64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
# Macos: x86_64
# -------------
7z x macos-x86_64.dmg -omacos2-out/ && cd macos2-out/SimpleX/SimpleX.app/Contents/app/resources/
mkdir libs && cp *.dylib libs/
zip -r "${PREFIX}-macos-x86_64.zip" libs
mv "${PREFIX}-macos-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
- name: Create release in libs repo and upload artifacts
uses: softprops/action-gh-release@v2
with:
repository: ${{ github.repository }}-libs
tag_name: ${{ github.ref_name }}
files: ${{ runner.temp }}/release-assets/*
token: ${{ secrets.NODEJS_REPO_TOKEN }}
+5 -5
View File
@@ -32,11 +32,11 @@
 
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
- 🖲 Protects your messages and metadata - who you talk to and when.
- 🔐 Double ratchet end-to-end encryption, with additional encryption layer.
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
- 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows.
@@ -145,9 +145,9 @@ It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission).
- BTC: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
- XMR: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- XMR: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
- ETH/USDT (Ethereum, Arbitrum One): 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a
- ETH/USDT (Ethereum, Arbitrum One): 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth)
- ZEC: t1fwjQW5gpFhDqXNhxqDWyF9j9WeKvVS5Jg
- ZEC shielded: u16rnvkflumf5uw9frngc2lymvmzgdr2mmc9unyu0l44unwfmdcpfm0axujd2w34ct3ye709azxsqge45705lpvvqu264ltzvfay55ygyq
- DOGE: D99pV4n9TrPxBPCkQGx4w4SMSa6QjRBxPf
@@ -435,4 +435,4 @@ Graphic designs, artworks and layouts are not licensed for re-use. If you want t
&nbsp;
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
+9 -3
View File
@@ -41,6 +41,7 @@ enum ChatCommand: ChatCmdProtocol {
case apiGetChatTags(userId: Int64)
case apiGetChats(userId: Int64)
case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String)
case apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?)
case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64)
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
case apiCreateChatTag(tag: ChatTagData)
@@ -224,7 +225,8 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
case let .apiGetChat(chatId, scope, contentTag, pagination, search):
let tag = contentTag != nil ? " content=\(contentTag!.rawValue)" : ""
return "/_get chat \(chatId)\(scopeRef(scope: scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
return "/_get chat \(chatId)\(scopeRef(scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
case let .apiGetChatContentTypes(chatId, scope): return "/_get content types \(chatId)\(scopeRef(scope))"
case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)"
case let .apiSendMessages(type, id, scope, live, ttl, composedMessages):
let msgs = encodeJSON(composedMessages)
@@ -417,6 +419,7 @@ enum ChatCommand: ChatCmdProtocol {
case .apiGetChatTags: return "apiGetChatTags"
case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat"
case .apiGetChatContentTypes: return "apiGetChatContentTypes"
case .apiGetChatItemInfo: return "apiGetChatItemInfo"
case .apiSendMessages: return "apiSendMessages"
case .apiCreateChatTag: return "apiCreateChatTag"
@@ -559,10 +562,10 @@ enum ChatCommand: ChatCmdProtocol {
}
func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String {
"\(type.rawValue)\(id)\(scopeRef(scope: scope))"
"\(type.rawValue)\(id)\(scopeRef(scope))"
}
func scopeRef(scope: GroupChatScope?) -> String {
func scopeRef(_ scope: GroupChatScope?) -> String {
switch (scope) {
case .none: ""
case let .memberSupport(groupMemberId_):
@@ -648,6 +651,7 @@ enum ChatResponse0: Decodable, ChatAPIResult {
case chatStopped
case apiChats(user: UserRef, chats: [ChatData])
case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo?)
case chatContentTypes(contentTypes: [MsgContentTag])
case chatTags(user: UserRef, userTags: [ChatTag])
case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo)
case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?)
@@ -680,6 +684,7 @@ enum ChatResponse0: Decodable, ChatAPIResult {
case .chatStopped: "chatStopped"
case .apiChats: "apiChats"
case .apiChat: "apiChat"
case .chatContentTypes: "chatContentTypes"
case .chatTags: "chatTags"
case .chatItemInfo: "chatItemInfo"
case .serverTestResult: "serverTestResult"
@@ -714,6 +719,7 @@ enum ChatResponse0: Decodable, ChatAPIResult {
case .chatStopped: return noDetails
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
case let .apiChat(u, chat, navInfo): return withUser(u, "chat: \(String(describing: chat))\nnavInfo: \(String(describing: navInfo))")
case let .chatContentTypes(types): return "content types: \(String(describing: types))"
case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))")
case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))")
case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))")
+6
View File
@@ -444,6 +444,12 @@ func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTa
throw r.unexpected
}
func apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?) async throws -> [MsgContentTag] {
let r: ChatResponse0 = try await chatSendCmd(.apiGetChatContentTypes(chatId: chatId, scope: scope))
if case let .chatContentTypes(types) = r { return types }
throw r.unexpected
}
func loadChat(chat: Chat, im: ItemsModel, search: String = "", clearItems: Bool = true) async {
await loadChat(chatId: chat.chatInfo.id, im: im, search: search, clearItems: clearItems)
}
+1 -1
View File
@@ -4601,7 +4601,7 @@ extension MsgContent: Encodable {
}
}
public enum MsgContentTag: String {
public enum MsgContentTag: String, Decodable {
case text
case link
case image
@@ -54,11 +54,10 @@ add_library( # Sets the name of the library.
simplex-api.c)
add_library( simplex SHARED IMPORTED )
FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libsimplex.${OS_LIB_EXT})
if(WIN32)
FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT})
set_target_properties( simplex PROPERTIES IMPORTED_IMPLIB ${SIMPLEXLIB})
else()
FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex-chat*.${OS_LIB_EXT})
set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB})
endif()
@@ -1036,6 +1036,14 @@ object ChatController {
return null
}
suspend fun apiGetChatContentTypes(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): List<MsgContentTag>? {
val r = sendCmd(rh, CC.ApiGetChatContentTypes(type, id, scope))
if (r is API.Result && r.res is CR.ChatContentTypes) return r.res.contentTypes
Log.e(TAG, "apiGetChatContentTypes bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_loading_details), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiCreateChatTag(rh: Long?, tag: ChatTagData): List<ChatTag>? {
val r = sendCmd(rh, CC.ApiCreateChatTag(tag))
if (r is API.Result && r.res is CR.ChatTags) return r.res.userTags
@@ -3542,6 +3550,7 @@ sealed class CC {
class ApiGetChatTags(val userId: Long): CC()
class ApiGetChats(val userId: Long): CC()
class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC()
class ApiGetChatContentTypes(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC()
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC()
class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
class ApiCreateChatTag(val tag: ChatTagData): CC()
@@ -3726,6 +3735,7 @@ sealed class CC {
}
"/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
}
is ApiGetChatContentTypes -> "/_get content types ${chatRef(type, id, scope)})"
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId"
is ApiSendMessages -> {
val msgs = json.encodeToString(composedMessages)
@@ -3912,6 +3922,7 @@ sealed class CC {
is ApiGetChatTags -> "apiGetChatTags"
is ApiGetChats -> "apiGetChats"
is ApiGetChat -> "apiGetChat"
is ApiGetChatContentTypes -> "apiGetChatContentTypes"
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
is ApiSendMessages -> "apiSendMessages"
is ApiCreateChatTag -> "apiCreateChatTag"
@@ -6097,6 +6108,7 @@ sealed class CR {
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
@Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR()
@Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR()
@Serializable @SerialName("chatContentTypes") class ChatContentTypes(val contentTypes: List<MsgContentTag>): CR()
@Serializable @SerialName("chatTags") class ChatTags(val user: UserRef, val userTags: List<ChatTag>): CR()
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
@@ -6278,6 +6290,7 @@ sealed class CR {
is ChatStopped -> "chatStopped"
is ApiChats -> "apiChats"
is ApiChat -> "apiChat"
is ChatContentTypes -> "chatContentTypes"
is ChatTags -> "chatTags"
is ApiChatItemInfo -> "chatItemInfo"
is ServerTestResult -> "serverTestResult"
@@ -6451,6 +6464,7 @@ sealed class CR {
is ChatStopped -> noDetails()
is ApiChats -> withUser(user, json.encodeToString(chats))
is ApiChat -> withUser(user, "remoteHostId: ${chat.remoteHostId}\nchatInfo: ${chat.chatInfo}\nchatStats: ${chat.chatStats}\nnavInfo: ${navInfo}\nchatItems: ${chat.chatItems}")
is ChatContentTypes -> "content types: ${json.encodeToString(contentTypes)}"
is ChatTags -> withUser(user, "userTags: ${json.encodeToString(userTags)}")
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
@@ -40,6 +40,7 @@ compose {
}
mainClass = "chat.simplex.desktop.MainKt"
nativeDistributions {
copyright = "(c) 2020-2026 SimpleX Chat"
// For debugging via VisualVM
if (debugJava) {
modules("jdk.zipfs", "jdk.unsupported", "jdk.management.agent")
@@ -665,7 +665,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
where
rStatus = groupRolesStatus contactRole serviceRole
groupRef = groupReference g
ctRole = "*" <> strEncodeTxt contactRole <> "*"
ctRole = "*" <> textEncode contactRole <> "*"
suCtRole = "(user role is set to " <> ctRole <> ")."
deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO ()
@@ -691,7 +691,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
_ -> pure ()
where
groupRef = groupReference g
srvRole = "*" <> strEncodeTxt serviceRole <> "*"
srvRole = "*" <> textEncode serviceRole <> "*"
suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")."
whenContactIsOwner gr action =
getOwnerGroupMember groupId gr
@@ -801,7 +801,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let anotherRole = case acceptMemberRole of GRObserver -> GRMember; _ -> GRObserver
sendReply $
initialRole n acceptMemberRole
<> ("Send /'role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "' to change it.\n\n")
<> ("Send /'role " <> tshow gId <> " " <> textEncode anotherRole <> "' to change it.\n\n")
<> onlyViaLink gLink
Left _ -> sendReply $ "Error: failed reading the initial member role for the group " <> n
Just mRole -> do
@@ -809,7 +809,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
Just gLink -> sendReply $ initialRole n mRole <> "\n" <> onlyViaLink gLink
Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated."
where
initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n"
initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> textEncode mRole <> "*\n"
onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> groupLinkText gLink
DCGroupFilter gId gName_ acceptance_ ->
(if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do
@@ -852,7 +852,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
T.unlines $
[ "The link to join the group " <> groupRef <> ":",
groupLinkText gLink,
"New member role: " <> strEncodeTxt acceptMemberRole
"New member role: " <> textEncode acceptMemberRole
]
<> ["The link is being upgraded..." | shouldBeUpgraded]
when shouldBeUpgraded $ do
@@ -351,11 +351,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
pure (gs, n)
Just gId -> do
gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId)
pure (gs, n)
where
countQuery' = countQuery <> " WHERE r.group_reg_status = ? "
orderBy = " ORDER BY g.summary_current_members_count DESC "
orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC "
STRecent -> case lastGroup_ of
Nothing -> do
gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize)
@@ -363,11 +363,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
pure (gs, n)
Just gId -> do
gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId)
pure (gs, n)
where
countQuery' = countQuery <> " WHERE r.group_reg_status = ? "
orderBy = " ORDER BY r.created_at DESC "
orderBy = " ORDER BY r.created_at DESC, r.group_reg_id ASC "
STSearch search -> case lastGroup_ of
Nothing -> do
gs <- groups $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize)
@@ -375,12 +375,12 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa
pure (gs, n)
Just gId -> do
gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond <> orderBy) (GRSActive, gId, s, s, s, s)
n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s)
pure (gs, n)
where
s = T.toLower search
countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? "
orderBy = " ORDER BY g.summary_current_members_count DESC "
orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC "
where
groups = (map (toGroupInfoReg (vr cc) user) <$>)
count = maybeFirstRow' 0 fromOnly
@@ -113,7 +113,7 @@ It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat): it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat): it also accepts donations in crypto-currencies, but charges a commission.
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Monero wallet: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
Thank you,
@@ -99,7 +99,7 @@ It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat): it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat): it also accepts donations in crypto-currencies, but charges a commission.
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Monero wallet: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
Thank you,
@@ -132,8 +132,8 @@ It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat): it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat): it also accepts donations in crypto-currencies, but charges a commission.
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Monero wallet: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
- Bitcoin wallet: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
Thank you,
@@ -177,8 +177,8 @@ It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in many crypto-currencies.
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Monero wallet: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
- Bitcoin wallet: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
- please let us know, via GitHub issue or chat, if you want to make a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
@@ -127,9 +127,9 @@ It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in many crypto-currencies.
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Monero address: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
- Bitcoin address: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
- Ethereum address: 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth)
- please let us know, via GitHub issue or chat, if you want to make a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
@@ -91,9 +91,10 @@ It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [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
- Monero address: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
- Bitcoin address: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
- BCH address: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
- Ethereum address: 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth)
- 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.
Thank you,
+8
View File
@@ -192,8 +192,16 @@ It is usually simpler to run your bot process on the same machine where you run
If you have to run your bot on another machine, you need to secure access to bot CLI via any web proxy that supports WebSockets, e.g. Caddy or Nginx. You must configure TLS termination in the proxy and connect CLI process from bot via a secure TLS connection. If you connect to bot via a public network, you also must configure HTTP basic auth to prevent unauthorized access. You can validate TLS security of your proxy via a free test at [SSLLabs.com](https://www.ssllabs.com/ssltest/). You can also configure firewall on the machine where you run SimpleX CLI to only allow connections from the IP address of your bot.
## Available libraries
#### Libraries with full bot API support
- [The official TypeScript SDK](https://www.npmjs.com/package/simplex-chat)
- [Unofficial Rust SDK](https://crates.io/crates/simploxide-client)
## Useful bots
- [Broadcast bot](../apps/simplex-broadcast-bot/) (Haskell) - we use it to send [status and release updates](https://status.simplex.chat/status/public).
- [Moderation bot](https://github.com/NCalex42/simplex-bot) (Java)
- [Matterbridge bot](https://github.com/UnkwUsr/matterbridge-simplex) (JavaScript)
- [Nodify](https://nodify.ie) (Low-Code)
+109 -53
View File
@@ -60,6 +60,10 @@ This file is generated automatically.
- [APIUpdateProfile](#apiupdateprofile)
- [APISetContactPrefs](#apisetcontactprefs)
[Chat management](#chat-management)
- [StartChat](#startchat)
- [APIStopChat](#apistopchat)
---
@@ -98,7 +102,7 @@ UserContactLinkCreated: User contact address created.
- user: [User](./TYPES.md#user)
- connLinkContact: [CreatedConnLink](./TYPES.md#createdconnlink)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -134,7 +138,7 @@ UserContactLinkDeleted: User contact address deleted.
- type: "userContactLinkDeleted"
- user: [User](./TYPES.md#user)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -171,7 +175,7 @@ UserContactLink: User contact address.
- user: [User](./TYPES.md#user)
- contactLink: [UserContactLink](./TYPES.md#usercontactlink)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -211,7 +215,7 @@ UserProfileUpdated: User profile updated.
- toProfile: [Profile](./TYPES.md#profile)
- updateSummary: [UserProfileUpdateSummary](./TYPES.md#userprofileupdatesummary)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -249,7 +253,7 @@ UserContactLinkUpdated: User contact address updated.
- user: [User](./TYPES.md#user)
- contactLink: [UserContactLink](./TYPES.md#usercontactlink)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -280,7 +284,7 @@ Send messages.
```
```javascript
'/_send ' + sendRef.toString() + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript
'/_send ' + ChatRef.cmdString(sendRef) + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript
```
```python
@@ -294,7 +298,7 @@ NewChatItems: New messages.
- user: [User](./TYPES.md#user)
- chatItems: [[AChatItem](./TYPES.md#achatitem)]
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -320,7 +324,7 @@ Update message.
```
```javascript
'/_update item ' + chatRef.toString() + ' ' + chatItemId + (liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(updatedMessage) // JavaScript
'/_update item ' + ChatRef.cmdString(chatRef) + ' ' + chatItemId + (liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(updatedMessage) // JavaScript
```
```python
@@ -339,7 +343,7 @@ ChatItemNotChanged: Message not changed.
- user: [User](./TYPES.md#user)
- chatItem: [AChatItem](./TYPES.md#achatitem)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -367,7 +371,7 @@ Delete message.
```
```javascript
'/_delete item ' + chatRef.toString() + ' ' + chatItemIds.join(',') + ' ' + deleteMode // JavaScript
'/_delete item ' + ChatRef.cmdString(chatRef) + ' ' + chatItemIds.join(',') + ' ' + deleteMode // JavaScript
```
```python
@@ -383,7 +387,7 @@ ChatItemsDeleted: Messages deleted.
- byUser: bool
- timed: bool
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -423,7 +427,7 @@ ChatItemsDeleted: Messages deleted.
- byUser: bool
- timed: bool
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -449,7 +453,7 @@ Add/remove message reaction.
```
```javascript
'/_reaction ' + chatRef.toString() + ' ' + chatItemId + ' ' + (add ? 'on' : 'off') + ' ' + JSON.stringify(reaction) // JavaScript
'/_reaction ' + ChatRef.cmdString(chatRef) + ' ' + chatItemId + ' ' + (add ? 'on' : 'off') + ' ' + JSON.stringify(reaction) // JavaScript
```
```python
@@ -464,7 +468,7 @@ ChatItemReaction: Message reaction.
- added: bool
- reaction: [ACIReaction](./TYPES.md#acireaction)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -515,7 +519,7 @@ RcvFileAcceptedSndCancelled: File accepted, but no longer sent.
- user: [User](./TYPES.md#user)
- rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -560,7 +564,7 @@ RcvFileCancelled: Cancelled receiving file.
- chatItem_: [AChatItem](./TYPES.md#achatitem)?
- rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -609,7 +613,7 @@ SentGroupInvitation: Group invitation sent.
- contact: [Contact](./TYPES.md#contact)
- member: [GroupMember](./TYPES.md#groupmember)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -647,7 +651,7 @@ UserAcceptedGroupSent: User accepted group invitation.
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
- hostContact: [Contact](./TYPES.md#contact)?
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -687,7 +691,7 @@ MemberAccepted: Member accepted to group.
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
- member: [GroupMember](./TYPES.md#groupmember)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -731,7 +735,7 @@ MembersRoleUser: Members role changed by user.
- members: [[GroupMember](./TYPES.md#groupmember)]
- toRole: [GroupMemberRole](./TYPES.md#groupmemberrole)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -772,7 +776,7 @@ MembersBlockedForAllUser: Members blocked for all by admin.
- members: [[GroupMember](./TYPES.md#groupmember)]
- blocked: bool
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -813,7 +817,7 @@ UserDeletedMembers: Members deleted.
- members: [[GroupMember](./TYPES.md#groupmember)]
- withMessages: bool
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -853,7 +857,7 @@ LeftMemberUser: User left group.
- user: [User](./TYPES.md#user)
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -890,7 +894,7 @@ GroupMembers: Group members.
- user: [User](./TYPES.md#user)
- group: [Group](./TYPES.md#group)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -929,7 +933,7 @@ GroupCreated: Group created.
- user: [User](./TYPES.md#user)
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -969,7 +973,7 @@ GroupUpdated: Group updated.
- toGroup: [GroupInfo](./TYPES.md#groupinfo)
- member_: [GroupMember](./TYPES.md#groupmember)?
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1013,7 +1017,7 @@ GroupLinkCreated: Group link created.
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
- groupLink: [GroupLink](./TYPES.md#grouplink)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1052,7 +1056,7 @@ GroupLink: Group link.
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
- groupLink: [GroupLink](./TYPES.md#grouplink)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1089,7 +1093,7 @@ GroupLinkDeleted: Group link deleted.
- user: [User](./TYPES.md#user)
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1127,7 +1131,7 @@ GroupLink: Group link.
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
- groupLink: [GroupLink](./TYPES.md#grouplink)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1171,7 +1175,7 @@ Invitation: One-time invitation.
- connLinkInvitation: [CreatedConnLink](./TYPES.md#createdconnlink)
- connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1210,7 +1214,7 @@ ConnectionPlan: Connection link information.
- connLink: [CreatedConnLink](./TYPES.md#createdconnlink)
- connectionPlan: [ConnectionPlan](./TYPES.md#connectionplan)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1219,7 +1223,7 @@ ChatCmdError: Command error.
### APIConnect
Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link
Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.
*Network usage*: interactive.
@@ -1235,7 +1239,7 @@ Connect via prepared SimpleX link. The link can be 1-time invitation link, conta
```
```javascript
'/_connect ' + userId + (preparedLink_ ? ' ' + preparedLink_.toString() : '') // JavaScript
'/_connect ' + userId + (preparedLink_ ? ' ' + CreatedConnLink.cmdString(preparedLink_) : '') // JavaScript
```
```python
@@ -1261,7 +1265,7 @@ SentInvitation: Invitation sent to contact address.
- connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection)
- customUserProfile: [Profile](./TYPES.md#profile)?
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1311,7 +1315,7 @@ SentInvitation: Invitation sent to contact address.
- connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection)
- customUserProfile: [Profile](./TYPES.md#profile)?
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1348,7 +1352,7 @@ AcceptingContactRequest: Contact request accepted.
- user: [User](./TYPES.md#user)
- contact: [Contact](./TYPES.md#contact)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1386,7 +1390,7 @@ ContactRequestRejected: Contact request rejected.
- contactRequest: [UserContactRequest](./TYPES.md#usercontactrequest)
- contact_: [Contact](./TYPES.md#contact)?
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1428,7 +1432,7 @@ ContactsList: Contacts.
- user: [User](./TYPES.md#user)
- contacts: [[Contact](./TYPES.md#contact)]
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1467,7 +1471,7 @@ GroupsList: Groups.
- user: [User](./TYPES.md#user)
- groups: [[GroupInfo](./TYPES.md#groupinfo)]
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1491,7 +1495,7 @@ Delete chat.
```
```javascript
'/_delete ' + chatRef.toString() + ' ' + chatDeleteMode.toString() // JavaScript
'/_delete ' + ChatRef.cmdString(chatRef) + ' ' + ChatDeleteMode.cmdString(chatDeleteMode) // JavaScript
```
```python
@@ -1515,7 +1519,7 @@ GroupDeletedUser: User deleted group.
- user: [User](./TYPES.md#user)
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1529,7 +1533,7 @@ Most bots don't need to use these commands, as bot profile can be configured man
### ShowActiveUser
Get active user profile
Get active user profile.
*Network usage*: no.
@@ -1545,7 +1549,7 @@ ActiveUser: Active user profile.
- type: "activeUser"
- user: [User](./TYPES.md#user)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1554,7 +1558,7 @@ ChatCmdError: Command error.
### CreateActiveUser
Create new user profile
Create new user profile.
*Network usage*: no.
@@ -1581,7 +1585,7 @@ ActiveUser: Active user profile.
- type: "activeUser"
- user: [User](./TYPES.md#user)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1594,7 +1598,7 @@ ChatCmdError: Command error.
### ListUsers
Get all user profiles
Get all user profiles.
*Network usage*: no.
@@ -1610,7 +1614,7 @@ UsersList: Users.
- type: "usersList"
- users: [[UserInfo](./TYPES.md#userinfo)]
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1619,7 +1623,7 @@ ChatCmdError: Command error.
### APISetActiveUser
Set active user profile
Set active user profile.
*Network usage*: no.
@@ -1647,7 +1651,7 @@ ActiveUser: Active user profile.
- type: "activeUser"
- user: [User](./TYPES.md#user)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1688,7 +1692,7 @@ CmdOk: Ok.
- type: "cmdOk"
- user_: [User](./TYPES.md#user)?
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1732,7 +1736,7 @@ UserProfileNoChange: User profile was not changed.
- type: "userProfileNoChange"
- user: [User](./TYPES.md#user)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
@@ -1771,8 +1775,60 @@ ContactPrefsUpdated: Contact preferences updated.
- fromContact: [Contact](./TYPES.md#contact)
- toContact: [Contact](./TYPES.md#contact)
ChatCmdError: Command error.
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
- chatError: [ChatError](./TYPES.md#chaterror)
---
## Chat management
These commands should not be used with CLI-based bots
### StartChat
Start chat controller.
*Network usage*: no.
**Parameters**:
- mainApp: bool
- enableSndFiles: bool
**Syntax**:
```
/_start
```
**Responses**:
ChatStarted: Chat started.
- type: "chatStarted"
ChatRunning: Chat running.
- type: "chatRunning"
---
### APIStopChat
Stop chat controller.
*Network usage*: no.
**Syntax**:
```
/_stop
```
**Response**:
ChatStopped: Chat stopped.
- type: "chatStopped"
---
+1 -1
View File
@@ -705,7 +705,7 @@ Message error.
### ChatError
Chat error.
Chat error (only used in WebSockets API).
**Record type**:
- type: "chatError"
+1 -1
View File
@@ -1309,7 +1309,7 @@ Used in API commands. Chat scope can only be passed with groups.
```
```javascript
chatType.toString() + chatId + (chatScope ? chatScope.toString() : '') // JavaScript
ChatType.cmdString(chatType) + chatId + (chatScope ? GroupChatScope.cmdString(chatScope) : '') // JavaScript
```
```python
+13 -8
View File
@@ -72,7 +72,7 @@ instance IsString ErrorTypeDoc where fromString s = TD s ""
-- category name, category description, commands
-- inner: constructor, description, responses, errors (ChatErrorType constructors), network usage, syntax
-- inner: constructor, hidden params, description, responses, errors (ChatErrorType constructors), network usage, syntax
chatCommandsDocsData :: [(String, String, [(ConsName, [String], Text, [ConsName], [ErrorTypeDoc], Maybe UsesNetwork, Expr)])]
chatCommandsDocsData =
[ ( "Address commands",
@@ -132,7 +132,7 @@ chatCommandsDocsData =
"These commands may be used to create connections. Most bots do not need to use them - bot users will connect via bot address with auto-accept enabled.",
[ ("APIAddContact", [], "Create 1-time invitation link.", ["CRInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False)),
("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectionLink"),
("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"),
("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"),
("Connect", [], "Connect via SimpleX link as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connLink_"),
("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest", "CRChatCmdError"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"),
("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected", "CRChatCmdError"], [], Nothing, "/_reject " <> Param "contactReqId")
@@ -163,21 +163,27 @@ chatCommandsDocsData =
),
( "User profile commands",
"Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents).",
[ ("ShowActiveUser", [], "Get active user profile", ["CRActiveUser", "CRChatCmdError"], [], Nothing, "/user"),
[ ("ShowActiveUser", [], "Get active user profile.", ["CRActiveUser", "CRChatCmdError"], [], Nothing, "/user"),
( "CreateActiveUser",
[],
"Create new user profile",
"Create new user profile.",
["CRActiveUser", "CRChatCmdError"],
[TD "CEUserExists" "User or contact with this name already exists", TD "CEInvalidDisplayName" "Invalid user display name"],
Nothing,
"/_create user " <> Json "newUser"
),
("ListUsers", [], "Get all user profiles", ["CRUsersList", "CRChatCmdError"], [], Nothing, "/users"),
("APISetActiveUser", [], "Set active user profile", ["CRActiveUser", "CRChatCmdError"], ["CEChatNotStarted"], Nothing, "/_user " <> Param "userId" <> Optional "" (" " <> Json "$0") "viewPwd"),
("ListUsers", [], "Get all user profiles.", ["CRUsersList", "CRChatCmdError"], [], Nothing, "/users"),
("APISetActiveUser", [], "Set active user profile.", ["CRActiveUser", "CRChatCmdError"], ["CEChatNotStarted"], Nothing, "/_user " <> Param "userId" <> Optional "" (" " <> Json "$0") "viewPwd"),
("APIDeleteUser", [], "Delete user profile.", ["CRCmdOk", "CRChatCmdError"], [], Just UNBackground, "/_delete user " <> Param "userId" <> OnOffParam "del_smp" "delSMPQueues" Nothing <> Optional "" (" " <> Json "$0") "viewPwd"),
("APIUpdateProfile", [], "Update user profile.", ["CRUserProfileUpdated", "CRUserProfileNoChange", "CRChatCmdError"], [], Just UNBackground, "/_profile " <> Param "userId" <> " " <> Json "profile"),
("APISetContactPrefs", [], "Configure chat preference overrides for the contact.", ["CRContactPrefsUpdated", "CRChatCmdError"], [], Just UNBackground, "/_set prefs @" <> Param "contactId" <> " " <> Json "preferences")
]
),
( "Chat management",
"These commands should not be used with CLI-based bots",
[ ("StartChat", [], "Start chat controller.", ["CRChatStarted", "CRChatRunning"], [], Nothing, "/_start"),
("APIStopChat", [], "Stop chat controller.", ["CRChatStopped"], [], Nothing, "/_stop")
]
)
]
@@ -340,6 +346,7 @@ undocumentedCommands =
"APIGetAppSettings",
"APIGetCallInvitations",
"APIGetChat",
"APIGetChatContentTypes",
"APIGetChatItemInfo",
"APIGetChatItems",
"APIGetChatItemTTL",
@@ -395,7 +402,6 @@ undocumentedCommands =
"APISetUserServers",
"APISetUserUIThemes",
"APIStandaloneFileInfo",
"APIStopChat",
"APIStorageEncryption",
"APISuspendChat",
"APISwitchContact",
@@ -452,7 +458,6 @@ undocumentedCommands =
"SetTempFolder",
"SetUserProtoServers",
"SlowSQLQueries",
"StartChat",
"StartRemoteHost",
"StopRemoteCtrl",
"StopRemoteHost",
+1 -1
View File
@@ -143,7 +143,7 @@ chatEventsDocsData =
\or because messages may be delivered to deleted chats for a short period of time \
\(they will be ignored).",
[ ("CEvtMessageError", ""),
("CEvtChatError", ""), -- only used in WebSockets API, Haskell code uses Either, with error in Left
("CEvtChatError", "Chat error (only used in WebSockets API)."), -- Haskell code uses Either, with error in Left
("CEvtChatErrors", "")
],
[]
+1 -1
View File
@@ -73,7 +73,7 @@ syntaxText :: TypeAndFields -> Expr -> Text
syntaxText r syntax =
"\n**Syntax**:\n"
<> "\n```\n" <> docSyntaxText r syntax <> "\n```\n"
<> (if isConst syntax then "" else "\n```javascript\n" <> jsSyntaxText False r syntax <> " // JavaScript\n```\n")
<> (if isConst syntax then "" else "\n```javascript\n" <> jsSyntaxText False "" r syntax <> " // JavaScript\n```\n")
<> (if isConst syntax then "" else "\n```python\n" <> pySyntaxText r syntax <> " # Python\n```\n")
camelToSpace :: String -> String
+6 -6
View File
@@ -49,7 +49,7 @@ commandsCodeText =
<> "}\n\n"
<> ("export namespace " <> T.pack constrName <> " {\n")
<> (" export type Response = " <> constrsCode " " "CR" (("CR." <> ) . T.pack . fstToUpper . memberTag) (map responseType responses))
<> (if syntax == "" then "" else funcCode APITypeDef {typeName' = constrName, typeDef = ATDRecord params} syntax)
<> (if syntax == "" then "" else funcCode APITypeDef {typeName' = constrName, typeDef = ATDRecord params} "T." syntax)
<> "}\n"
where
constrName = fstToUpper tag
@@ -86,7 +86,7 @@ typesCodeText = ("// API Types\n// " <> autoGenerated <> "\n") <> foldMap typeCo
"ConnectionMode" -> T.pack $ map toUpper tag
"FileProtocol" -> T.pack $ map toUpper tag
_ -> T.replace "-" "_" $ T.pack $ fstToUpper tag
namespaceFuncCode = "\nexport namespace " <> name' <> " {" <> funcCode td typeSyntax <> "}\n"
namespaceFuncCode = "\nexport namespace " <> name' <> " {" <> funcCode td "" typeSyntax <> "}\n"
typeDefCode = case typeDef of
ATDRecord fields ->
("\nexport interface " <> name' <> " {\n")
@@ -107,7 +107,7 @@ unionTypeCode unionNamespace typesNamespace td@APITypeDef {typeName' = name} cs
<> (" export type Tag = " <> constrsCode " " name' constrTag (L.toList cs) <> "\n")
<> (" interface Interface {\n type: Tag\n }\n")
<> foldMap constrType cs
<> (if cmdSyntax == "" then "" else funcCode td cmdSyntax)
<> (if cmdSyntax == "" then "" else funcCode td typesNamespace cmdSyntax)
<> "}\n"
where
name' = T.pack name
@@ -128,9 +128,9 @@ constrsCode indent name' constr cs
line = T.intercalate " | " cs'
cs' = map constr cs
funcCode :: APITypeDef -> Expr -> Text
funcCode td@APITypeDef {typeName' = name, typeDef} cmdSyntax =
"\n export function cmdString(" <> param <> ": " <> T.pack name <> "): string {\n return " <> jsSyntaxText True (name, self : typeFields) cmdSyntax <> "\n }\n"
funcCode :: APITypeDef -> String -> Expr -> Text
funcCode td@APITypeDef {typeName' = name, typeDef} typeNamespace cmdSyntax =
"\n export function cmdString(" <> param <> ": " <> T.pack name <> "): string {\n return " <> jsSyntaxText True typeNamespace (name, self : typeFields) cmdSyntax <> "\n }\n"
where
param = if hasParams cmdSyntax then "self" else "_self"
self = APIRecordField "self" (ATDef td)
+5 -4
View File
@@ -51,8 +51,11 @@ chatResponsesDocsData =
("CRChatItemReaction", "Message reaction"),
("CRChatItemUpdated", "Message updated"),
("CRChatItemsDeleted", "Messages deleted"),
("CRChatRunning", ""),
("CRChatStarted", ""),
("CRChatStopped", ""),
("CRCmdOk", "Ok"),
("CRChatCmdError", "Command error"), -- only used in WebSockets API, Haskell code uses Either, with error in Left
("CRChatCmdError", "Command error (only used in WebSockets API)"), -- Haskell code uses Either, with error in Left
("CRConnectionPlan", "Connection link information"),
("CRContactAlreadyExists", ""),
("CRContactConnectionDeleted", "Connection deleted"),
@@ -121,15 +124,13 @@ undocumentedResponses =
"CRBroadcastSent",
"CRCallInvitations",
"CRChatCleared",
"CRChatContentTypes",
"CRChatHelp",
"CRChatItemId",
"CRChatItemInfo",
"CRChatItems",
"CRChatItemTTL",
"CRChatRunning",
"CRChats",
"CRChatStarted",
"CRChatStopped",
"CRConnectionsDiff",
"CRChatTags",
"CRConnectionAliasUpdated",
+3 -3
View File
@@ -99,8 +99,8 @@ withOptBoolParam r param p f =
(ATOptional (ATPrim (PT TBool))) -> f True
_ -> paramError r param p "is not [optional] boolean"
jsSyntaxText :: Bool -> TypeAndFields -> Expr -> Text
jsSyntaxText useSelf r = T.replace "' + '" "" . T.pack . go Nothing True
jsSyntaxText :: Bool -> String -> TypeAndFields -> Expr -> Text
jsSyntaxText useSelf typeNamespace r = T.replace "' + '" "" . T.pack . go Nothing True
where
go param top = \case
Concat exs -> intercalate " + " $ map (go param False) $ L.toList exs
@@ -112,7 +112,7 @@ jsSyntaxText useSelf r = T.replace "' + '" "" . T.pack . go Nothing True
_ -> paramName' useSelf param p
where
toStringSyntax (APITypeDef typeName _)
| typeHasSyntax typeName = paramName' useSelf param p <> ".toString()"
| typeHasSyntax typeName = typeNamespace <> typeName <> ".cmdString(" <> paramName' useSelf param p <> ")"
| otherwise = paramName' useSelf param p
Optional exN exJ p -> open <> n <> " ? " <> go (Just p) False exJ <> " : " <> nothing <> close
where
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 5f73d1e629a8807f1b9d94f8b411d6480a0a59fb
tag: 58212c421aa6abb6ad894b8231d8a380849b704b
source-repository-package
type: git
+1 -1
View File
@@ -17,7 +17,7 @@ Please donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission)
- BTC: [bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u](bitcoin:bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u)
- XMR: [8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt](monero:8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt)
- XMR: [8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c](monero:8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c)
- ETH/USDT (Ethereum, Arbitrum One): [0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a](ethereum:0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a) ([donate.simplexchat.eth](ethereum:0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a))
- [Other cryptocurrencies](https://github.com/simplex-chat/simplex-chat#please-support-us-with-your-donations)
+5 -1
View File
@@ -24,6 +24,8 @@ You can link your mobile device with desktop to use the same profile remotely, b
- Ubuntu 22.04 and Debian-based distros ([x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-aarch64.deb)).
- Ubuntu 24.04 ([x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-24_04-x86_64.deb), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-24_04-aarch64.deb)).
You can [verify and reproduce](./REPRODUCE.md) Linux builds.
**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-aarch64.dmg) (Apple Silicon).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-windows-x86_64.msi).
@@ -32,7 +34,9 @@ You can link your mobile device with desktop to use the same profile remotely, b
**iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu).
**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-armv7a.apk).
**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-armv7a.apk).
You can [verify and reproduce](./REPRODUCE.md) Android APKs.
## Terminal (console) app
+203
View File
@@ -0,0 +1,203 @@
---
title: Verify and reproduce builds
permalink: /reproduce/index.html
revision: 19.12.2025
---
# Verifying and reproducing release builds
- [Obtain release signing key](#obtain-release-signing-key)
- [Verify release signature](#verify-release-signature)
- [How to reproduce builds](#how-to-reproduce-builds)
- [Server binaries](#server-binaries)
- [Linux desktop apps and CLI](#linux-desktop-apps-and-cli)
- [Android apps](#android-apps)
## Obtain release signing key
To verify the signature of `_sha256sums` or apks you need to obtain the signing key. You can do it from keyservers:
```sh
gpg --keyserver hkps://keys.openpgp.org --search build@simplex.chat
gpg --keyserver hkps://keyserver.ubuntu.com --search build@simplex.chat
```
```sh
gpg --list-keys build@simplex.chat
```
Once you obtain the signing key, verify that its fingerprint is:
```
BBDF 7BDA D154 8B16 836A F5B9 D53B DFD1 53C3 66BA
```
Additionally, compare the key fingerprint with:
- [simplexchat.eth](https://app.ens.domains/simplexchat.eth) (release key record)
- [Mastodon](https://mastodon.social/@simplex) (profile)
- [Reddit](https://www.reddit.com/r/SimpleXChat/) (side panel)
You can set the imported key as "ultimately trusted":
```sh
echo -e "trust\n5\ny\nquit" | gpg --command-fd 0 --edit-key build@simplex.chat
```
## Verify release signature
**Linux dekstop apps and CLI**:
Download the file with executable hashes and the signature. For example, to verify the `v6.5.0-beta.3` release:
```sh
curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/_sha256sums.asc'
curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/_sha256sums'
```
Verify the signature:
```sh
gpg --verify _sha256sums.asc _sha256sums
```
**Android APKs**:
Download the APK files and signatures. For example, to verify the `v6.5.0-beta.3` release:
```sh
curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/simplex-aarch64.apk'
curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/_simplex-aarch64.apk.asc'
curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/simplex-armv7a.apk'
curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/_simplex-armv7a.apk.asc'
```
Verify the signatures:
```sh
gpg --verify _simplex-armv7a.apk.asc simplex-armv7a.apk
gpg --verify _simplex-aarch64.apk.asc simplex-aarch64.apk
```
## How to reproduce builds
To reproduce the build you must have:
- Linux machine
- `x86-64` architecture
- Installed `docker`, `curl` and `git`
### Server binaries
1. Download script:
```sh
curl -LO 'https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/master/scripts/simplexmq-reproduce-builds.sh'
```
2. Make it executable:
```sh
chmod +x simplexmq-reproduce-builds.sh
```
3. Execute the script with the required tag:
```sh
./simplexmq-reproduce-builds.sh 'v6.3.1'
```
The script executes these steps (please review the script to confirm):
1) builds all server binaries for the release in docker container.
2) downloads binaries from the same GitHub release and compares them with the built binaries.
3) if they all match, generates _sha256sums file with their checksums.
This will take a while.
4. After compilation, you should see the folder named as the tag and repository name (e.g., `v6.3.1-simplexmq`) with two subfolders:
```sh
ls v6.3.1-simplexmq
```
```sh
from-source prebuilt _sha256sums
```
The file _sha256sums contains the hashes of all builds - you can compare it with the same file in GitHub release.
### Linux desktop apps and CLI
1. Download script:
```sh
curl -LO 'https://raw.githubusercontent.com/simplex-chat/simplex-chat/refs/heads/master/scripts/simplex-chat-reproduce-builds.sh'
```
2. Make it executable:
```sh
chmod +x simplex-chat-reproduce-builds.sh
```
3. Execute the script with the required tag:
```sh
./simplex-chat-reproduce-builds.sh 'v6.4.8'
```
The script executes these steps (please review the script to confirm):
1) builds all Linux CLI and Dekstop binaries for the release in docker container.
2) downloads binaries from the same GitHub release and compares them with the built binaries.
3) if they all match, generates _sha256sums file with their checksums.
This will take a while.
4. After compilation, you should see the folder named as the tag and reprository name (e.g., `v6.4.8-simplex-chat`) with two subfolders:
```sh
ls v6.4.8-simplex-chat
```
```sh
from-source prebuilt _sha256sums
```
The file _sha256sums contains the hashes of all builds - you can compare it with the same file in GitHub release.
### Android apps
In addition to basic requirments, Android build will:
- Take ~150gb of disc space
- Take ~20h to build all the architectures (depends on core count)
- Require at least 16gb of RAM
1. Download script:
```sh
curl -LO 'https://raw.githubusercontent.com/simplex-chat/simplex-chat/refs/heads/master/scripts/simplex-chat-reproduce-builds-android.sh'
```
2. Make it executable:
```sh
chmod +x simplex-chat-reproduce-builds-android.sh
```
3. Execute the script with the required tag:
```sh
./simplex-chat-reproduce-builds-android.sh 'v6.5.0-beta.3'
```
The script executes these steps (please review the script to confirm):
1) Downloads and checks that APKs from GitHub are signed with valid key.
2) Builds Android APKs in a docker container.
3) Compares the releases by copying the signature from downloaded APKs to locally built APKs.
4) If the resulting build is bit-by-bit identical, prints the message that this tag was reproduced.
This will take a while.
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: Contributing translations to SimpleX Chat
title: Contributing SimpleX app translations
revision: 19.03.2023
---
+1 -2
View File
@@ -1,5 +1,5 @@
---
title: Using custom WebRTC ICE servers in SimpleX Chat
title: Using custom WebRTC ICE servers
revision: 31.01.2023
---
@@ -155,4 +155,3 @@ This is it - you now can make audio and video calls via your own server, without
<img src="./stun_3.png">
If results show `srflx` and `relay` candidates, everything is set up correctly!
+8 -8
View File
@@ -18,11 +18,11 @@
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
- 🖲 Chrání vaše zprávy a metadata - s kým a kdy mluvíte.
- 🔐 Koncové šifrování s další vrstvou šifrování.
- 📱 Mobilní aplikace pro Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) a [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 📱 Mobilní aplikace pro Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)) a [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) s novými funkcemi o 1-2 týdny dříve - **omezeno na 10 000 uživatelů**!
- 🖥 K dispozici jako terminálová (konzolová) [aplikace / CLI](#zap-quick-installation-of-a-terminal-app) v systémech Linux, MacOS, Windows.
@@ -277,11 +277,11 @@ Přispět je možné prostřednictvím:
- [GitHub](https://github.com/sponsors/simplex-chat) - je to pro nás bez provize.
- OpenCollective](https://opencollective.com/simplex-chat) - účtuje si provizi a přijímá také dary v kryptoměnách.
- Adresa Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt.
- Bitcoinová adresa: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH adresa: BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum adresa: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Adresa Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
- Adresa Monero: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c.
- Bitcoinová adresa: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
- BCH adresa: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
- ETH/USDT (Ethereum, Arbitrum One) adresa: 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth)
- Adresa Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
- dejte nám prosím vědět prostřednictvím GitHub issue nebo chatu, pokud chcete vytvořit příspěvek v nějaké jiné kryptoměně - přidáme adresu do seznamu.
Děkujeme,
@@ -324,4 +324,4 @@ Jakákoli zjištění možných útoků korelace provozu umožňujících korelo
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
+8 -8
View File
@@ -32,11 +32,11 @@
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
- 🖲 Protégez vos messages et vos métadonnées - avec qui vous parlez et quand.
- 🔐 Chiffrement de bout en bout à double ratchet, avec couche de chiffrement supplémentaire.
- 📱 Apps mobiles pour Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) et [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 📱 Apps mobiles pour Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)) et [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 🚀 [Bêta TestFlight pour iOS](https://testflight.apple.com/join/DWuT2LQu) avec les nouvelles fonctionnalités 1 à 2 semaines plus tôt - **limitée à 10 000 utilisateurs** !
- 🖥 Disponible en tant que [terminal (console) / CLI](#⚡-installation-rapide-dune-application-pour-terminal) sur Linux, MacOS, Windows.
@@ -112,11 +112,11 @@ Il est possible de faire un don via :
- [GitHub](https://github.com/sponsors/simplex-chat) - il n'y a pas de commission à payer.
- [OpenCollective](https://opencollective.com/simplex-chat) - ils prélèvent une commission et acceptent également les dons en crypto-monnaies.
- Adresse Monero : 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Adresse Bitcoin : 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Adresse BCH : 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Adresse Ethereum : 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Adresse Solana : 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
- Adresse Monero : 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
- Adresse Bitcoin : bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
- Adresse BCH : bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
- Adresse ETH/USDT (Ethereum, Arbitrum One) : 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth)
- Adresse Solana : 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
Nous vous remercions,
@@ -351,4 +351,4 @@ Veuillez traiter toute découverte d'une éventuelle attaque par corrélation de
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
+10 -13
View File
@@ -32,11 +32,11 @@
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
- 🖲 Chroni Twoje wiadomości i metadane - z kim rozmawiasz i kiedy.
- 🔐 Szyfrowanie end-to-end double ratchet, z dodatkową warstwą szyfrowania.
- 📱 Aplikacje mobilne dla Androida ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) oraz [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 📱 Aplikacje mobilne dla Androida ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)) oraz [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 🚀 [TestFlight dla iOS](https://testflight.apple.com/join/DWuT2LQu) z nowymi funkcjami na tydzień-dwa wcześniej - **limitowane do 10,000 użytkowników**!
- 🖥 Dostępny jako terminalowa (konsolowa) [aplikacja / CLI](#zap-quick-installation-of-a-terminal-app) na Linuxa, MacOSa, Windowsa.
@@ -164,14 +164,11 @@ Możesz nas wesprzeć za pomocą:
- [GitHuba](https://github.com/sponsors/simplex-chat) - jest to dla nas wolne od prowizji.
- [OpenCollective](https://opencollective.com/simplex-chat) - pobiera prowizję, a także przyjmuje darowizny w kryptowalutach.
- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- USDT:
- BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW
- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
- Monero: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c
- Bitcoin: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u
- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
- ETH/USDT (Ethereum, Arbitrum One): 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth)
- Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
Dziękuję,
@@ -198,9 +195,9 @@ Twórca SimpleX Chat.
## Dlaczego prywatność ma znaczenie
Każdy powinien dbać o prywatność i bezpieczeństwo swojej komunikacji - nieszkodliwe rozmowy mogą narazić Cię na niebezpieczeństwo, nawet jeśli nie masz nic do ukrycia.
Każdy powinien dbać o prywatność i bezpieczeństwo swojej komunikacji - nieszkodliwe rozmowy mogą narazić Cię na niebezpieczeństwo, nawet jeśli nie masz nic do ukrycia.
Jedną z najbardziej wstrząsających historii jest doświadczenie [Mohamedou Ould Salahi](https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi). opisane w jego pamiętniku i pokazane w filmie Mauretańczyk (2021). Został on umieszczony w obozie Guantanamo, bez procesu, i był tam torturowany przez 15 lat po telefonie do swojego krewnego w Afganistanie, pod zarzutem udziału w atakach 9/11, mimo że przez poprzednie 10 lat mieszkał w Niemczech.
Jedną z najbardziej wstrząsających historii jest doświadczenie [Mohamedou Ould Salahi](https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi). opisane w jego pamiętniku i pokazane w filmie Mauretańczyk (2021). Został on umieszczony w obozie Guantanamo, bez procesu, i był tam torturowany przez 15 lat po telefonie do swojego krewnego w Afganistanie, pod zarzutem udziału w atakach 9/11, mimo że przez poprzednie 10 lat mieszkał w Niemczech.
Używanie szyfrowanego komunikatora end-to-end nie jest wystarczające. Powinniśmy używać komunikatorów, które zapewniają prywatność naszym powiązaniom, czyli tym z kim jesteśmy jakkolwiek połączeni.
@@ -432,4 +429,4 @@ Prosimy o traktowanie wszelkich ustaleń dotyczących możliwych ataków korelac
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
&nbsp;
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
+98
View File
@@ -0,0 +1,98 @@
# Community Vouchers contract
This document simplifies [the previous RFC](./2025-10-23-vouchers.md) in two ways:
- only one type of credits (previous design had conversion from community to operator credits).
- vouchers can support any amount and change.
- using a single fixed-size Merkle tree for vouchers.
- proposal for balancing privacy and the size of the tree that the client has to load.
## Objectives
- voucher expiration - fixed period per contract.
- make assignment of voucher to redeemer (a community) private, not allowing further transactions/re-assignments.
- vouchers support any amount.
- allow assigning and redeeming part of the voucher, with change.
## Design proposal
### General ideas
- Voucher is issued by the smart contract in exchange for stable-coin deposit, and recorded as blinded commitment added to Merkle tree.
- Voucher is nominated in infrastructure credits with 18 decimals, as all ETH tokens.
- Voucher assignments and redemptions (partial or full) are done via zero-knowledge proof and recorded as nullifier to prevent double spends, with the new commitment for the remaining amount and transaction count (can be 0).
- Commitment includes:
- the amount.
- the whether the voucher is assigned.
- the expiration time.
- Merkle tree of commitments has a fixed depth of say 20-40 levels (for approx 10^6-10^12 voucher capacity).
- Assignments and redemptions should prove consistency between the old and new commitments, without revealing them, and include the nullifier to prevent "double spends".
- Redemptions also include the blockchain address and ID of the operator.
- Release of funds with revenue sharing can be done via call to another contract.
### Data storage
Smart contract:
- total deposit amounts bucketed by time ranges (to allow the release of funds to network after expiration).
- redemption amounts bucketed by time ranges with homomorphic encryption (to prevent reduction of anonymity set).
- nullifiers bucketed by creation time (to allow removing old nullifiers).
- the path in the Merkle tree on "the front" of the filled part of the tree.
- multiple last Merkle tree roots:
- to accept the proof after the root changes.
- to frustrate timing correlation by computing proofs in advance (for better usability).
Transaction event log:
- Merkle tree of commitments are recorded in event log
### Voucher lifecycle
1. Issuance: blinded commitment recorded as described above in exchange for stablecoin deposit.
The contract should be able to verify that:
- commitment amount is the same as deposit amount.
- expiration time is in 12 months from now (possibly rounded up to 1-3 months).
- not assigned to redeemer.
Initially, the deposit UX could be dApp generating some token to be used in the app to assign voucher to the community via app (to preserve privacy).
2. Computing zero-knowledge proof for assignment or redemption.
To compute the proof the client needs to have a path from commitment to tree root.
As the path to commitment changes when the tree gets filled, we need to find a balance between privacy and usability - to request as little as possible and have as large anonymity set as possible.
3. Assignment.
Includes 2 new commitments (to destination and a "change") and nullifier are submitted.
ZK proof should prove that:
- the old commitment expiration time is greater than transaction submission time (possibly has to be included in parameters to be part of the proof).
- the total amount of 2 new commitments is the same as the amount of nullified old commitment.
- the new commitments expiration time is the same as the old.
- the old commitment is not assigned to redeemer.
- the new commitments is assigned to redeemer.
Questions:
1) how would the redeemer know that it received a voucher? Probably, community owners would be notified by chat relays.
2) do we want to conceal from the relays when and to which group donation was made? If yes, community owners can monitor blockchain themselves.
3) can we have view keys to delegate the detection of incoming transactions to relays?
4) should we round expiration time to a month or even 3 months? (to avoid exposing expiration time to the assignment recipient, as it also leaks purchase time) If so, we should also have overlapping rounding ranges to avoid leaking purchase time in case it is assigned straight after purchase, and straight after range change.
4. Redemption.
Includes one new commitment (with the change) and public record of funds released to operator and network.
The revenues received by the operators are publicly visible.
ZK proof should prove that:
- the old commitment expiration time is greater than transaction submission time.
- the total amount of the new commitment and of released funds is the same as the amount of nullified commitment.
- the new commitment expiration time is the same as the old.
- the old commitment is assigned to the redeemer that redeems the voucher.
- the new commitment has the same assignment.
5. Releasing deposits for expired vouchers to the network.
That would require tracking purchases in buckets (in the clear) and redemptions in homomorphicly encrypted structure, so an observer cannot correlate redemptions to purchases (as the same structure will change, and it won't be possible to establish which bucket).
@@ -1,6 +1,6 @@
{
"name": "@simplex-chat/types",
"version": "0.1.0",
"version": "0.2.0",
"description": "TypeScript types for SimpleX Chat bot libraries",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -96,7 +96,7 @@ export namespace APISendMessages {
export type Response = CR.NewChatItems | CR.ChatCmdError
export function cmdString(self: APISendMessages): string {
return '/_send ' + self.sendRef.toString() + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + ' json ' + JSON.stringify(self.composedMessages)
return '/_send ' + T.ChatRef.cmdString(self.sendRef) + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + ' json ' + JSON.stringify(self.composedMessages)
}
}
@@ -113,7 +113,7 @@ export namespace APIUpdateChatItem {
export type Response = CR.ChatItemUpdated | CR.ChatItemNotChanged | CR.ChatCmdError
export function cmdString(self: APIUpdateChatItem): string {
return '/_update item ' + self.chatRef.toString() + ' ' + self.chatItemId + (self.liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(self.updatedMessage)
return '/_update item ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemId + (self.liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(self.updatedMessage)
}
}
@@ -129,7 +129,7 @@ export namespace APIDeleteChatItem {
export type Response = CR.ChatItemsDeleted | CR.ChatCmdError
export function cmdString(self: APIDeleteChatItem): string {
return '/_delete item ' + self.chatRef.toString() + ' ' + self.chatItemIds.join(',') + ' ' + self.deleteMode
return '/_delete item ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemIds.join(',') + ' ' + self.deleteMode
}
}
@@ -161,7 +161,7 @@ export namespace APIChatItemReaction {
export type Response = CR.ChatItemReaction | CR.ChatCmdError
export function cmdString(self: APIChatItemReaction): string {
return '/_reaction ' + self.chatRef.toString() + ' ' + self.chatItemId + ' ' + (self.add ? 'on' : 'off') + ' ' + JSON.stringify(self.reaction)
return '/_reaction ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemId + ' ' + (self.add ? 'on' : 'off') + ' ' + JSON.stringify(self.reaction)
}
}
@@ -450,7 +450,7 @@ export namespace APIConnectPlan {
}
}
// Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link
// Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.
// Network usage: interactive.
export interface APIConnect {
userId: number // int64
@@ -462,7 +462,7 @@ export namespace APIConnect {
export type Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError
export function cmdString(self: APIConnect): string {
return '/_connect ' + self.userId + (self.preparedLink_ ? ' ' + self.preparedLink_.toString() : '')
return '/_connect ' + self.userId + (self.preparedLink_ ? ' ' + T.CreatedConnLink.cmdString(self.preparedLink_) : '')
}
}
@@ -553,14 +553,14 @@ export namespace APIDeleteChat {
export type Response = CR.ContactDeleted | CR.ContactConnectionDeleted | CR.GroupDeletedUser | CR.ChatCmdError
export function cmdString(self: APIDeleteChat): string {
return '/_delete ' + self.chatRef.toString() + ' ' + self.chatDeleteMode.toString()
return '/_delete ' + T.ChatRef.cmdString(self.chatRef) + ' ' + T.ChatDeleteMode.cmdString(self.chatDeleteMode)
}
}
// User profile commands
// Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents).
// Get active user profile
// Get active user profile.
// Network usage: no.
export interface ShowActiveUser {
}
@@ -573,7 +573,7 @@ export namespace ShowActiveUser {
}
}
// Create new user profile
// Create new user profile.
// Network usage: no.
export interface CreateActiveUser {
newUser: T.NewUser
@@ -587,7 +587,7 @@ export namespace CreateActiveUser {
}
}
// Get all user profiles
// Get all user profiles.
// Network usage: no.
export interface ListUsers {
}
@@ -600,7 +600,7 @@ export namespace ListUsers {
}
}
// Set active user profile
// Set active user profile.
// Network usage: no.
export interface APISetActiveUser {
userId: number // int64
@@ -660,3 +660,34 @@ export namespace APISetContactPrefs {
return '/_set prefs @' + self.contactId + ' ' + JSON.stringify(self.preferences)
}
}
// Chat management
// These commands should not be used with CLI-based bots
// Start chat controller.
// Network usage: no.
export interface StartChat {
mainApp: boolean
enableSndFiles: boolean
}
export namespace StartChat {
export type Response = CR.ChatStarted | CR.ChatRunning
export function cmdString(_self: StartChat): string {
return '/_start'
}
}
// Stop chat controller.
// Network usage: no.
export interface APIStopChat {
}
export namespace APIStopChat {
export type Response = CR.ChatStopped
export function cmdString(_self: APIStopChat): string {
return '/_stop'
}
}
@@ -10,6 +10,9 @@ export type ChatResponse =
| CR.ChatItemReaction
| CR.ChatItemUpdated
| CR.ChatItemsDeleted
| CR.ChatRunning
| CR.ChatStarted
| CR.ChatStopped
| CR.CmdOk
| CR.ChatCmdError
| CR.ConnectionPlan
@@ -58,6 +61,9 @@ export namespace CR {
| "chatItemReaction"
| "chatItemUpdated"
| "chatItemsDeleted"
| "chatRunning"
| "chatStarted"
| "chatStopped"
| "cmdOk"
| "chatCmdError"
| "connectionPlan"
@@ -140,6 +146,18 @@ export namespace CR {
timed: boolean
}
export interface ChatRunning extends Interface {
type: "chatRunning"
}
export interface ChatStarted extends Interface {
type: "chatStarted"
}
export interface ChatStopped extends Interface {
type: "chatStopped"
}
export interface CmdOk extends Interface {
type: "cmdOk"
user_?: T.User
@@ -1552,7 +1552,7 @@ export interface ChatRef {
export namespace ChatRef {
export function cmdString(self: ChatRef): string {
return self.chatType.toString() + self.chatId + (self.chatScope ? self.chatScope.toString() : '')
return ChatType.cmdString(self.chatType) + self.chatId + (self.chatScope ? GroupChatScope.cmdString(self.chatScope) : '')
}
}
+8 -7
View File
@@ -24,18 +24,19 @@ exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/C
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
rm -rf $BUILD_DIR
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN' --ghc-options="-optl-L$(ghc --print-libdir)/rts -optl-Wl,--as-needed,-lHSrts_thr-ghc$GHC_VERSION" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
#rm -rf $BUILD_DIR
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -optl-Wl,-soname,libsimplex.so -flink-rts -threaded' --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
cd $BUILD_DIR/build
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
mv libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so libsimplex.so 2> /dev/null || true
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libsimplex.so
#patchelf --add-rpath '$ORIGIN' libsimplex.so
# GitHub's Ubuntu 20.04 runner started to set libffi.so.7 as a dependency while Ubuntu 20.04 on user's devices may not have it
# but libffi.so.8 is shipped as an external library with other libs
patchelf --replace-needed "libffi.so.7" "libffi.so.8" libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
patchelf --replace-needed "libffi.so.7" "libffi.so.8" libsimplex.so
mkdir deps 2> /dev/null || true
ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
ldd libsimplex.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
cd -
@@ -44,7 +45,7 @@ rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libsimplex.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
scripts/desktop/prepare-vlc-linux.sh
links_dir=apps/multiplatform/build/links
+5 -4
View File
@@ -14,7 +14,7 @@ else
COMPOSE_ARCH=x64
fi
LIB_EXT=dylib
LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT
LIB=libsimplex.$LIB_EXT
GHC_LIBS_DIR=$(ghc --print-libdir)
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
@@ -27,13 +27,14 @@ rm -rf $BUILD_DIR
if [[ "$DATABASE_BACKEND" == "postgres" ]]; then
echo "Building with postgres backend..."
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/rts -optl-lHSrts_thr-ghc8.10.7 -optl-lffi" --constraint 'simplexmq +client_library +client_postgres' --constraint 'simplex-chat +client_library +client_postgres'
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-install_name,@rpath/$LIB -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library +client_postgres' --constraint 'simplex-chat +client_library +client_postgres'
else
echo "Building with sqlite backend..."
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/rts -optl-lHSrts_thr-ghc8.10.7 -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-install_name,@rpath/$LIB -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
fi
cd $BUILD_DIR/build
mv libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT libsimplex.dylib 2> /dev/null || true
mkdir deps 2> /dev/null || true
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
@@ -87,7 +88,7 @@ rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/$LIB apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."5f73d1e629a8807f1b9d94f8b411d6480a0a59fb" = "1w5mxw9rwiiiqphbg2rdyp4cvv9hz2l64f7fpfhncw6gncfx7ggw";
"https://github.com/simplex-chat/simplexmq.git"."58212c421aa6abb6ad894b8231d8a380849b704b" = "1awgvhqfi7gv3xl10h21a6w2hhqc48pq6yq4f83awg1zxkh3hiqn";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+7 -3
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 6.5.0.7
version: 6.5.0.8
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -123,7 +123,9 @@ library
Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync
Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade
Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector
Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2
Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations
Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables
Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices
else
exposed-modules:
Simplex.Chat.Archive
@@ -270,7 +272,9 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync
Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade
Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector
Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2
Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations
Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables
Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices
other-modules:
Paths_simplex_chat
hs-source-dirs:
+1
View File
@@ -110,6 +110,7 @@ defaultChatConfig =
deliveryWorkerDelay = 0,
deliveryBucketSize = 10000,
deviceNameForRemote = "",
remoteCompression = True,
chatHooks = defaultChatHooks
}
+25 -20
View File
@@ -76,6 +76,7 @@ import Simplex.Messaging.Agent.Lock
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority)
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, UpMigration)
import Simplex.Messaging.Agent.Store.DB (SQLError)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..))
import qualified Simplex.Messaging.Crypto as C
@@ -97,7 +98,6 @@ import System.IO (Handle)
import System.Mem.Weak (Weak)
import UnliftIO.STM
#if !defined(dbPostgres)
import Database.SQLite.Simple (SQLError)
import qualified Database.SQLite.Simple as SQL
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
#endif
@@ -159,6 +159,7 @@ data ChatConfig = ChatConfig
deliveryBucketSize :: Int,
highlyAvailable :: Bool,
deviceNameForRemote :: Text,
remoteCompression :: Bool,
chatHooks :: ChatHooks
}
@@ -315,6 +316,7 @@ data ChatCommand
| APIGetChatTags UserId
| APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery}
| APIGetChat {chatRef :: ChatRef, contentTag :: Maybe MsgContentTag, chatPagination :: ChatPagination, search :: Maybe Text}
| APIGetChatContentTypes ChatRef
| APIGetChatItems {chatPagination :: ChatPagination, search :: Maybe Text}
| APIGetChatItemInfo {chatRef :: ChatRef, chatItemId :: ChatItemId}
| APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage}
@@ -636,6 +638,7 @@ data ChatResponse
| CRApiChats {user :: User, chats :: [AChat]}
| CRChats {chats :: [AChat]}
| CRApiChat {user :: User, chat :: AChat, navInfo :: Maybe NavigationInfo}
| CRChatContentTypes {contentTypes :: [MsgContentTag]}
| CRChatTags {user :: User, userTags :: [ChatTag]}
| CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]}
| CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo}
@@ -754,7 +757,7 @@ data ChatResponse
| CRRemoteFileStored {remoteHostId :: RemoteHostId, remoteFileSource :: CryptoFile}
| CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]}
| CRRemoteCtrlConnecting {remoteCtrl_ :: Maybe RemoteCtrlInfo, ctrlAppInfo :: CtrlAppInfo, appVersion :: AppVersion}
| CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo}
| CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo, compression :: Bool}
| CRSQLResult {rows :: [Text]}
#if !defined(dbPostgres)
| CRArchiveExported {archiveErrors :: [ArchiveError]}
@@ -857,7 +860,7 @@ data ChatEvent
| CEvtNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgAckInfo}
| CEvtRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text}
| CEvtNewRemoteHost {remoteHost :: RemoteHostInfo}
| CEvtRemoteHostConnected {remoteHost :: RemoteHostInfo}
| CEvtRemoteHostConnected {remoteHost :: RemoteHostInfo, compression :: Bool}
| CEvtRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId, rhsState :: RemoteHostSessionState, rhStopReason :: RemoteHostStopReason}
| CEvtRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo, ctrlAppInfo_ :: Maybe CtrlAppInfo, appVersion :: AppVersion, compatible :: Bool}
| CEvtRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text}
@@ -897,7 +900,7 @@ allowRemoteEvent = \case
CEvtChatSuspended -> False
CEvtRemoteHostSessionCode {} -> False
CEvtNewRemoteHost _ -> False
CEvtRemoteHostConnected _ -> False
CEvtRemoteHostConnected {} -> False
CEvtRemoteHostStopped {} -> False
CEvtRemoteCtrlFound {} -> False
CEvtRemoteCtrlSessionCode {} -> False
@@ -1397,7 +1400,8 @@ data RemoteCtrlSession
| RCSessionConnecting
{ remoteCtrlId_ :: Maybe RemoteCtrlId,
rcsClient :: RCCtrlClient,
rcsWaitSession :: Async ()
rcsWaitSession :: Async (),
ctrlAppInfo :: CtrlAppInfo
}
| RCSessionPendingConfirmation
{ remoteCtrlId_ :: Maybe RemoteCtrlId,
@@ -1406,7 +1410,8 @@ data RemoteCtrlSession
tls :: TLS 'TClient,
sessionCode :: Text,
rcsWaitSession :: Async (),
rcsWaitConfirmation :: TMVar (Either RCErrorType (RCCtrlSession, RCCtrlPairing))
rcsWaitConfirmation :: TMVar (Either RCErrorType (RCCtrlSession, RCCtrlPairing)),
ctrlAppInfo :: CtrlAppInfo
}
| RCSessionConnected
{ remoteCtrlId :: RemoteCtrlId,
@@ -1414,7 +1419,8 @@ data RemoteCtrlSession
tls :: TLS 'TClient,
rcsSession :: RCCtrlSession,
http2Server :: Async (),
remoteOutputQ :: TBQueue (Either ChatError ChatEvent)
remoteOutputQ :: TBQueue (Either ChatError ChatEvent),
ctrlAppInfo :: CtrlAppInfo
}
data RemoteCtrlSessionState
@@ -1538,25 +1544,24 @@ withFastStore = withStorePriority True
withStorePriority :: Bool -> (DB.Connection -> ExceptT StoreError IO a) -> CM a
withStorePriority priority action = do
ChatController {chatStore} <- ask
liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors
liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catch` handleDBErrors
withStoreBatch :: Traversable t => (DB.Connection -> t (IO (Either ChatError a))) -> CM' (t (Either ChatError a))
withStoreBatch actions = do
ChatController {chatStore} <- ask
liftIO $ withTransaction chatStore $ mapM (`E.catches` handleDBErrors) . actions
liftIO $ withTransaction chatStore $ mapM (`E.catch` handleDBErrors) . actions
-- TODO [postgres] postgres specific error handling
handleDBErrors :: [E.Handler (Either ChatError a)]
handleDBErrors =
#if !defined(dbPostgres)
( E.Handler $ \(e :: SQLError) ->
let se = SQL.sqlError e
busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked
in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e
) :
handleDBErrors :: E.SomeException -> IO (Either ChatError a)
handleDBErrors e = pure $ Left $ ChatErrorStore $ case E.fromException e of
Just (e' :: SQLError) ->
#if defined(dbPostgres)
SEDBException $ show e'
#else
let se = SQL.sqlError e'
busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked
in (if busy then SEDBBusyError else SEDBException) $ show e'
#endif
[ E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e
]
Nothing -> SEDBException $ show e
withStoreBatch' :: Traversable t => (DB.Connection -> t (IO a)) -> CM' (t (Either ChatError a))
withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions
+1 -1
View File
@@ -52,7 +52,7 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@Cha
exitFailure
run db@ChatDatabase {chatStore} = do
u_ <- getSelectActiveUser chatStore
let backgroundMode = not maintenance
let backgroundMode = maintenance
cc <- newChatController db u_ cfg opts backgroundMode
u <- maybe (createActiveUser cc createBot) pure u_
unless testView $ putStrLn $ "Current user: " <> userStr u
+8 -29
View File
@@ -167,9 +167,6 @@ startChatController mainApp enableSndFiles = do
runExceptT (syncConnections' users) >>= \case
Left e -> liftIO $ putStrLn $ "Error synchronizing connections: " <> show e
Right _ -> pure ()
runExceptT migrateMemberRelations >>= \case
Left e -> liftIO $ putStrLn $ "Error migrating member relations: " <> show e
Right _ -> pure ()
restoreCalls
s <- asks agentAsync
readTVarIO s >>= maybe (start s users) (pure . fst)
@@ -181,10 +178,6 @@ startChatController mainApp enableSndFiles = do
(userDiff, connDiff) <- withAgent (\a -> syncConnections a aUserIds connIds)
withFastStore' setConnectionsSyncTs
toView $ CEvtConnectionsDiff (AgentUserId <$> userDiff) (AgentConnId <$> connDiff)
migrateMemberRelations =
when mainApp $
whenM (withStore' hasMembersWithoutVector) $
void $ forkIO runRelationsVectorMigration
start s users = do
a1 <- async agentSubscriber
a2 <-
@@ -269,7 +262,7 @@ stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles,
readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd)
atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd)
disconnectAgentClient smpAgent
readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
readTVarIO s >>= mapM_ (\(a1, a2) -> forkIO $ uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
closeFiles sndFiles
closeFiles rcvFiles
atomically $ do
@@ -542,16 +535,14 @@ processChatCommand vr nm = \case
APIGetChat (ChatRef cType cId scope_) contentFilter pagination search -> withUser $ \user -> case cType of
-- TODO optimize queries calculating ChatStats, currently they're disabled
CTDirect -> do
when (isJust contentFilter) $ throwCmdError "content filter not supported"
(directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId pagination search)
(directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId contentFilter pagination search)
pure $ CRApiChat user (AChat SCTDirect directChat) navInfo
CTGroup -> do
(groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId scope_ contentFilter pagination search)
groupChat' <- checkSupportChatAttention user groupChat
pure $ CRApiChat user (AChat SCTGroup groupChat') navInfo
CTLocal -> do
when (isJust contentFilter) $ throwCmdError "content filter not supported"
(localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search)
(localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId contentFilter pagination search)
pure $ CRApiChat user (AChat SCTLocal localChat) navInfo
CTContactRequest -> throwCmdError "not implemented"
CTContactConnection -> throwCmdError "not supported"
@@ -577,6 +568,8 @@ processChatCommand vr nm = \case
newFromMember (CChatItem _ ChatItem {chatDir = CIGroupRcv m, meta = CIMeta {itemStatus = CISRcvNew}}) =
groupMemberId' m == scopeGMId
newFromMember _ = False
APIGetChatContentTypes chatRef -> withUser $ \user ->
CRChatContentTypes <$> withStore (\db -> getChatContentTypes db user chatRef)
APIGetChatItems pagination search -> withUser $ \user -> do
chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search
pure $ CRChatItems user Nothing chatItems
@@ -1812,7 +1805,7 @@ processChatCommand vr nm = \case
conn <- withFastStore $ \db -> getPendingContactConnection db userId connId
let PendingContactConnection {pccConnStatus, connLinkInv} = conn
case (pccConnStatus, connLinkInv) of
(ConnNew, Just _ссLink) -> do
(ConnNew, Just _ccLink) -> do
newUser <- privateGetUser newUserId
conn' <- recreateConn user conn newUser
pure $ CRConnectionUserChanged user conn conn' newUser
@@ -2966,7 +2959,7 @@ processChatCommand vr nm = \case
ConfirmRemoteCtrl rcId -> withUser_ $ do
(rc, ctrlAppInfo) <- confirmRemoteCtrl rcId
pure CRRemoteCtrlConnecting {remoteCtrl_ = Just rc, ctrlAppInfo, appVersion = currentAppVersion}
VerifyRemoteCtrlSession sessId -> withUser_ $ CRRemoteCtrlConnected <$> verifyRemoteCtrlSession (execChatCommand Nothing) sessId
VerifyRemoteCtrlSession sessId -> withUser_ $ verifyRemoteCtrlSession (execChatCommand Nothing) sessId
StopRemoteCtrl -> withUser_ $ stopRemoteCtrl >> ok_
ListRemoteCtrls -> withUser_ $ CRRemoteCtrlList <$> listRemoteCtrls
DeleteRemoteCtrl rc -> withUser_ $ deleteRemoteCtrl rc >> ok_
@@ -4176,21 +4169,6 @@ agentSubscriber = do
type AgentSubResult = Map ConnId (Either AgentErrorType (Maybe ClientServiceId))
runRelationsVectorMigration :: CM ()
runRelationsVectorMigration = do
liftIO $ threadDelay' 5000000 -- 5 seconds (initial delay)
migrateMembers
where
stepDelay = 1000000 -- 1 second
migrateMembers = flip catchAllErrors eToView $ do
lift waitChatStartedAndActivated
gmIds <- withStore' getGMsWithoutVectorIds
forM_ gmIds $ \gmId -> do
lift waitChatStartedAndActivated
withStore' (`migrateMemberRelationsVector'` gmId) `catchAllErrors` eToView
liftIO $ threadDelay' stepDelay
unless (null gmIds) migrateMembers
cleanupManager :: CM ()
cleanupManager = do
interval <- asks (cleanupManagerInterval . config)
@@ -4390,6 +4368,7 @@ chatCommandP =
<*> (A.space *> jsonP <|> pure clqNoFilters)
),
"/_get chat " *> (APIGetChat <$> chatRefP <*> optional (" content=" *> strP) <* A.space <*> chatPaginationP <*> optional (" search=" *> textP)),
"/_get content types " *> (APIGetChatContentTypes <$> chatRefP),
"/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> textP)),
"/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal),
"/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)),
+42 -54
View File
@@ -707,8 +707,8 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
if
| inline -> do
-- accepting inline
ci <- withStore $ \db -> acceptRcvInlineFT db vr user fileId filePath
sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId
(ci, sharedMsgId) <- withStore $ \db ->
liftM2 (,) (acceptRcvInlineFT db vr user fileId filePath) (getSharedMsgIdByFileId db userId fileId)
send $ XFileAcptInv sharedMsgId Nothing fName
pure ci
| fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName
@@ -925,9 +925,11 @@ acceptGroupJoinRequestAsync
incognitoProfile = do
gVar <- asks random
let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted
(groupMemberId, memberId) <- withStore $ \db ->
createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ welcomeMsgId_ gLinkMemRole initialStatus
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
((groupMemberId, memberId), currentMemCount) <- withStore $ \db ->
liftM2
(,)
(createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ welcomeMsgId_ gLinkMemRole initialStatus)
(liftIO $ getGroupCurrentMembersCount db user gInfo)
let Profile {displayName} = userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile)
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
msg =
@@ -1030,54 +1032,43 @@ introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRol
else XMsgNew $ MCSimple $ extMsgContent (MCText pendingReviewMessage) Nothing
void $ sendDirectMemberMessage mConn msg groupId
modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo
let rcpModMs = filter shouldIntroduce modMs
introduceMember vr user gInfo m rcpModMs (Just $ MSMember $ memberId' m)
let rcpModMs = filter shouldIntroduceToMod modMs
introduceMember user gInfo m rcpModMs (Just $ MSMember $ memberId' m)
where
shouldIntroduce :: GroupMember -> Bool
shouldIntroduce mem =
shouldIntroduceToMod :: GroupMember -> Bool
shouldIntroduceToMod mem =
memberCurrent mem
&& groupMemberId' mem /= groupMemberId' m
&& maxVersion (memberChatVRange mem) >= groupKnockingVersion
introduceToAll :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM ()
introduceToAll vr user gInfo m = do
members <- withStore' $ \db -> getGroupMembers db vr user gInfo
vector_ <- withStore' (`getMemberRelationsVector_` m)
let recipients = filter (shouldIntroduce vector_) members
introduceMember vr user gInfo m recipients Nothing
where
shouldIntroduce :: Maybe ByteString -> GroupMember -> Bool
shouldIntroduce vector_ m' =
memberCurrent m'
&& groupMemberId' m' /= groupMemberId' m
&& maybe True (\v -> getRelation (indexInGroup m') v == MRNew) vector_
(members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db vr user gInfo) (getMemberRelationsVector db m)
let recipients = filter (shouldIntroduce m vector) members
introduceMember user gInfo m recipients Nothing
introduceToRemaining :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM ()
introduceToRemaining vr user gInfo m = do
members <- withStore' $ \db -> getGroupMembers db vr user gInfo
vector_ <- withStore' (`getMemberRelationsVector_` m)
recipients <- filterRecipients vector_ members
introduceMember vr user gInfo m recipients Nothing
where
filterRecipients :: Maybe ByteString -> [GroupMember] -> CM [GroupMember]
filterRecipients vector_ members = do
newRelation <- case vector_ of
Nothing -> do
introducedGMIds <- S.fromList <$> withStore' (`getIntroducedGroupMemberIds` m)
pure $ \m' -> groupMemberId' m' `S.notMember` introducedGMIds
Just vec -> pure $ \m' -> getRelation (indexInGroup m') vec == MRNew
pure $ filter (\m' -> groupMemberId' m' /= groupMemberId' m && memberCurrent m' && newRelation m') members
(members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db vr user gInfo) (getMemberRelationsVector db m)
let recipients = filter (shouldIntroduce m vector) members
introduceMember user gInfo m recipients Nothing
introduceMember :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM ()
introduceMember _ _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active"
introduceMember vr user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do
shouldIntroduce :: GroupMember -> ByteString -> GroupMember -> Bool
shouldIntroduce m vec mem =
memberCurrent mem
&& groupMemberId' mem /= groupMemberId' m
&& getRelation (indexInGroup mem) vec == MRNew
introduceMember :: User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM ()
introduceMember _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active"
introduceMember user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do
void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo gInfo toMember) msgScope
sendIntroductions introduceToMembers
where
sendIntroductions reMembers = do
updateToMemberVector reMembers
reMembers' <- withStore' $ \db -> createIntrosOrUpdateVectors db vr reMembers toMember
shuffledReMembers <- liftIO $ shuffleMembers reMembers'
updateReMembersVectors reMembers
shuffledReMembers <- liftIO $ shuffleMembers reMembers
if toMember `supportsVersion` batchSendVersion
then do
let events = map memberIntro shuffledReMembers
@@ -1089,6 +1080,10 @@ introduceMember vr user gInfo@GroupInfo {groupId} toMember@GroupMember {activeCo
updateToMemberVector reMembers = do
let relations = map (\GroupMember {indexInGroup} -> (indexInGroup, (IDReferencedIntroduced, MRIntroduced))) reMembers
withStore' $ \db -> setMemberVectorNewRelations db toMember relations
updateReMembersVectors :: [GroupMember] -> CM ()
updateReMembersVectors reMembers = do
let GroupMember {indexInGroup} = toMember
withStore' $ \db -> setMembersVectorsNewRelation db reMembers indexInGroup IDSubjectIntroduced MRIntroduced
memberIntro :: GroupMember -> ChatMsgEvent 'Json
memberIntro reMember =
let mInfo = memberInfo gInfo reMember
@@ -2020,7 +2015,7 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do
pendingReq SndMessage {msgId} = (groupMemberId, msgId)
createPendingMsg :: DB.Connection -> (GroupMemberId, MessageId) -> IO (Either ChatError ())
createPendingMsg db (groupMemberId, msgId) =
createPendingGroupMessage db groupMemberId msgId Nothing $> Right ()
createPendingGroupMessage db groupMemberId msgId $> Right ()
data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded
@@ -2083,32 +2078,25 @@ readyMemberConn GroupMember {groupMemberId, activeConn = Just conn@Connection {c
| otherwise = Nothing
readyMemberConn GroupMember {activeConn = Nothing} = Nothing
sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe GroupMemberIntro -> CM () -> CM ()
sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent intro_ postDeliver = do
sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> CM ()
sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent = do
msg <- createSndMessage chatMsgEvent (GroupId groupId)
messageMember msg `catchAllErrors` eToView
where
messageMember :: SndMessage -> CM ()
messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case
MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
MSASendBatched conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId (introId <$> intro_)
MSASend conn -> void $ deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
MSASendBatched conn -> void $ deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId
MSAForwarded -> pure ()
-- TODO ensure order - pending messages interleave with user input messages
sendPendingGroupMessages :: User -> GroupMember -> Connection -> CM ()
sendPendingGroupMessages user GroupMember {groupMemberId} conn = do
pgms <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
forM_ (L.nonEmpty pgms) $ \pgms' -> do
let msgs = L.map (\(sndMsg, _, _) -> sndMsg) pgms'
void $ batchSendConnMessages user conn MsgFlags {notification = True} msgs
lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs
lift . void . withStoreBatch' $ \db -> L.map (\(_, tag, introId_) -> updateIntro_ db tag introId_) pgms'
where
updateIntro_ :: DB.Connection -> ACMEventTag -> Maybe Int64 -> IO ()
updateIntro_ db tag introId_ = case (tag, introId_) of
(ACMEventTag _ XGrpMemFwd_, Just introId) -> updateIntroStatus db introId GMIntroInvForwarded
_ -> pure ()
msgs <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
forM_ (L.nonEmpty msgs) $ \msgs' -> do
void $ batchSendConnMessages user conn MsgFlags {notification = True} msgs'
lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs'
saveDirectRcvMSG :: MsgEncodingI e => Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (Connection, RcvMessage)
saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do
+48 -48
View File
@@ -691,9 +691,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- TODO REMOVE LEGACY vvv
-- [async agent commands] group link auto-accept continuation on receiving INV
CFCreateConnGrpInv -> do
ct <- withStore $ \db -> getContactViaMember db vr user m
withStore' $ \db -> setNewContactMemberConnRequest db user m cReq
groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo
(ct, groupLinkId) <- withStore $ \db -> do
ct <- getContactViaMember db vr user m
liftIO $ setNewContactMemberConnRequest db user m cReq
liftIO $ (ct,) <$> getGroupLinkId db user gInfo
sendGrpInvitation ct m groupLinkId
toView $ CEvtSentGroupInvitation user gInfo ct m
where
@@ -1814,8 +1815,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ts@(_, ft_) = msgContentTexts mc
live = fromMaybe False live_
updateRcvChatItem = do
cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user gInfo groupMemberId sharedMsgId
scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci)
(cci, scopeInfo) <- withStore $ \db -> do
cci <- getGroupChatItemBySharedMsgId db user gInfo groupMemberId sharedMsgId
(cci,) <$> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci)
case cci of
CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} ->
if sameMemberId memberId m'
@@ -1948,8 +1950,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xFileCancel :: Contact -> SharedMsgId -> CM ()
xFileCancel Contact {contactId} sharedMsgId = do
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
ft <- withStore (\db -> getRcvFileTransfer db user fileId)
(fileId, ft) <- withStore $ \db -> do
fileId <- getFileIdBySharedMsgId db userId contactId sharedMsgId
(fileId,) <$> getRcvFileTransfer db user fileId
unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft
ci <- withStore $ \db -> getChatItemByFileId db vr user fileId
@@ -1957,8 +1960,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> CM ()
xFileAcptInv ct sharedMsgId fileConnReq_ fName = do
fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId
(AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId
(fileId, AChatItem _ _ _ ci) <- withStore $ \db -> do
fileId <- getDirectFileIdBySharedMsgId db user ct sharedMsgId
(fileId,) <$> getChatItemByFileId db vr user fileId
assertSMPAcceptNotProhibited ci
ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId)
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
@@ -2033,8 +2037,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xFileCancelGroup g@GroupInfo {groupId} GroupMember {memberId} sharedMsgId = do
(fileId, aci) <- withStore $ \db -> do
fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
aci <- getChatItemByFileId db vr user fileId
pure (fileId, aci)
(fileId,) <$> getChatItemByFileId db vr user fileId
case aci of
AChatItem SCTGroup SMDRcv (GroupChat _g scopeInfo) ChatItem {chatDir = CIGroupRcv m} -> do
if sameMemberId memberId m
@@ -2051,8 +2054,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe ConnReqInvitation -> String -> CM ()
xFileAcptInvGroup GroupInfo {groupId} m@GroupMember {activeConn} sharedMsgId fileConnReq_ fName = do
fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
(AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId
(fileId, AChatItem _ _ _ ci) <- withStore $ \db -> do
fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
(fileId,) <$> getChatItemByFileId db vr user fileId
assertSMPAcceptNotProhibited ci
-- TODO check that it's not already accepted
ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId)
@@ -2123,8 +2127,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xDirectDel c msg msgMeta =
if directOrUsed c
then do
ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted
contactConns <- withStore' $ \db -> getContactConnections db vr userId ct'
(ct', contactConns) <- withStore' $ \db -> do
ct' <- updateContactStatus db user c CSDeleted
(ct',) <$> getContactConnections db vr userId ct'
deleteAgentConnectionsAsync $ map aConnId contactConns
forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted}
@@ -2496,15 +2501,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
associateMemberWithContact :: Contact -> GroupMember -> CM Contact
associateMemberWithContact c1 m2@GroupMember {groupId} = do
withStore' $ \db -> associateMemberWithContactRecord db user c1 m2
g <- withStore $ \db -> getGroupInfo db vr user groupId
g <- withStore $ \db -> do
liftIO $ associateMemberWithContactRecord db user c1 m2
getGroupInfo db vr user groupId
toView $ CEvtContactAndMemberAssociated user c1 g m2 c1
pure c1
associateContactWithMember :: GroupMember -> Contact -> CM Contact
associateContactWithMember m1@GroupMember {groupId} c2 = do
c2' <- withStore $ \db -> associateContactWithMemberRecord db vr user m1 c2
g <- withStore $ \db -> getGroupInfo db vr user groupId
(c2', g) <- withStore $ \db ->
liftM2 (,) (associateContactWithMemberRecord db vr user m1 c2) (getGroupInfo db vr user groupId)
toView $ CEvtContactAndMemberAssociated user c2 g m1 c2'
pure c2'
@@ -2615,33 +2621,28 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
GCInviteeMember ->
withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case
Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist"
Right reMember -> do
intro_ <- withStore' $ \db -> getIntroduction db reMember m
update intro_ GMIntroInvReceived
sendGroupMemberMessage gInfo reMember (XGrpMemFwd (memberInfo gInfo m) introInv) intro_ $
update intro_ GMIntroInvForwarded
where
update (Just GroupMemberIntro {introId}) status = withStore' $ \db -> updateIntroStatus db introId status
update Nothing _ = pure ()
Right reMember -> sendGroupMemberMessage gInfo reMember $ XGrpMemFwd (memberInfo gInfo m) introInv
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> CM ()
xGrpMemFwd gInfo@GroupInfo {membership, chatSettings} m memInfo@(MemberInfo memId memRole memChatVRange _) IntroInvitation {groupConnReq, directConnReq} = do
let GroupMember {memberId = membershipMemId} = membership
checkHostRole m memRole
toMember <-
withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case
toMember <- withStore $ \db -> do
toMember <- getGroupMemberByMemberId db vr user gInfo memId
-- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent
-- the situation when member does not exist is an error
-- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that.
-- For now, this branch compensates for the lack of delayed message delivery.
Left _ -> withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced
Right m' -> pure m'
-- TODO [knocking] separate pending statuses from GroupMemberStatus?
-- TODO add GSMemIntroInvitedPending, GSMemConnectedPending, etc.?
-- TODO keep as is? (GSMemIntroInvited has no purpose)
let newMemberStatus = if memberPending toMember then memberStatus toMember else GSMemIntroInvited
withStore' $ \db -> updateGroupMemberStatus db userId toMember newMemberStatus
`catchError` \case
SEGroupMemberNotFoundByMemberId _ -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced
e -> throwError e
-- TODO [knocking] separate pending statuses from GroupMemberStatus?
-- TODO add GSMemIntroInvitedPending, GSMemConnectedPending, etc.?
-- TODO keep as is? (GSMemIntroInvited has no purpose)
let newMemberStatus = if memberPending toMember then memberStatus toMember else GSMemIntroInvited
liftIO $ updateGroupMemberStatus db userId toMember newMemberStatus
pure toMember
subMode <- chatReadVar subscriptionMode
-- [incognito] send membership incognito profile, create direct connection as incognito
let membershipProfile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership
@@ -2718,8 +2719,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xGrpMemCon :: GroupInfo -> GroupMember -> MemberId -> CM ()
xGrpMemCon gInfo sendingMem memId = do
refMem <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo memId
withStore' (`migrateMemberRelationsVector` sendingMem)
withStore' (`migrateMemberRelationsVector` refMem)
-- Updating vectors in separate transactions to avoid deadlocks.
withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected
withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected
@@ -2783,7 +2782,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
let GroupMember {memberId} = m
memberName = Just $ memberShortenedName m
event = XGrpMsgForward memberId memberName chatMsg brokerTs
sendGroupMemberMessage gInfo member event Nothing (pure ())
sendGroupMemberMessage gInfo member event
-- TODO [channels fwd] base on differentiation between groups and channels
isUserGrpFwdRelay :: GroupInfo -> Bool
@@ -3030,14 +3029,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
updateGroupItemsStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> GroupSndStatus -> Maybe Bool -> CM ()
updateGroupItemsStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus viaProxy_ = do
items <- withStore' (\db -> getGroupChatItemsByAgentMsgId db user groupId connId msgId)
cis <- catMaybes <$> withStore (\db -> mapM (updateItem db) items)
-- SENT and RCVD events are received for messages that may be batched in single scope,
-- so we can look up scope of first item
scopeInfo <- case cis of
(ci : _) -> withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (chatItemId' ci)
_ -> pure Nothing
let acis = map (gItem scopeInfo) cis
acis <- withStore $ \db -> do
items <- liftIO $ getGroupChatItemsByAgentMsgId db user groupId connId msgId
cis <- catMaybes <$> mapM (updateItem db) items
-- SENT and RCVD events are received for messages that may be batched in single scope,
-- so we can look up scope of first item
scopeInfo <- case cis of
(ci : _) -> getGroupChatScopeInfoForItem db vr user gInfo (chatItemId' ci)
_ -> pure Nothing
pure $ map (gItem scopeInfo) cis
unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis
where
gItem scopeInfo ci = AChatItem SCTGroup SMDSnd (GroupChat gInfo scopeInfo) ci
@@ -3228,7 +3228,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
unless (null ms) $ deliver body ms
where
buildMemberList sender = do
vec <- withStore $ \db -> migrateGetMemberRelationsVector db sender
vec <- withStore (`getMemberRelationsVector` sender)
-- this excludes the sender
let introducedMemsIdxs = getRelationsIndexes MRIntroduced vec
case jobScope of
+6 -6
View File
@@ -22,7 +22,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Lazy as LB
import Data.Int (Int64)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Text.Encoding (encodeUtf8)
import Data.Type.Equality
import Data.Word (Word32)
import Simplex.Chat.Messages.CIContent.Events
@@ -323,7 +323,7 @@ e2eInfoPQText =
ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text
ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role =
"invitation to join group " <> displayName <> optionalFullName displayName fullName Nothing <> " as " <> (decodeLatin1 . strEncode $ role)
"invitation to join group " <> displayName <> optionalFullName displayName fullName Nothing <> " as " <> textEncode role
rcvDirectEventToText :: RcvDirectEvent -> Text
rcvDirectEventToText = \case
@@ -338,9 +338,9 @@ rcvGroupEventToText = \case
RGEMemberAccepted _ p -> "accepted " <> profileToText p
RGEUserAccepted -> "accepted you"
RGEMemberLeft -> "left"
RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r)
RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> textEncode r
RGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p
RGEUserRole r -> "changed your role to " <> safeDecodeUtf8 (strEncode r)
RGEUserRole r -> "changed your role to " <> textEncode r
RGEMemberDeleted _ p -> "removed " <> profileToText p
RGEUserDeleted -> "removed you"
RGEGroupDeleted -> "deleted group"
@@ -352,9 +352,9 @@ rcvGroupEventToText = \case
sndGroupEventToText :: SndGroupEvent -> Text
sndGroupEventToText = \case
SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r)
SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> textEncode r
SGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p
SGEUserRole r -> "changed role for yourself to " <> safeDecodeUtf8 (strEncode r)
SGEUserRole r -> "changed role for yourself to " <> textEncode r
SGEMemberDeleted _ p -> "removed " <> profileToText p
SGEUserLeft -> "left"
SGEGroupUpdated _ -> "group profile updated"
+2 -2
View File
@@ -42,7 +42,7 @@ chatDbOptsP _appDir defaultDbName = do
( long "pool-size"
<> metavar "DB_POOL_SIZE"
<> help "Database connection pool size"
<> value 10
<> value 1
<> showDefault
)
dbCreateSchema <-
@@ -84,7 +84,7 @@ mobileDbOpts schemaPrefix connstr = do
ChatDbOpts
{ dbConnstr,
dbSchemaPrefix,
dbPoolSize = 10,
dbPoolSize = 1,
dbCreateSchema = True
}
+2
View File
@@ -518,6 +518,8 @@ instance ToJSON MsgContentTag where
toJSON = strToJSON
toEncoding = strToJEncoding
instance FromField MsgContentTag where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8
instance ToField MsgContentTag where toField = toField . safeDecodeUtf8 . strEncode
data MsgContainer
+31 -14
View File
@@ -165,7 +165,8 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do
where
mkCtrlAppInfo = do
deviceName <- chatReadVar localDeviceName
pure CtrlAppInfo {appVersionRange = ctrlAppVersionRange, deviceName}
useCompression <- asks $ remoteCompression . config
pure CtrlAppInfo {appVersionRange = ctrlAppVersionRange, deviceName, compression = BoolDef useCompression}
parseHostAppInfo :: RCHostHello -> ExceptT RemoteHostError IO HostAppInfo
parseHostAppInfo RCHostHello {app = hostAppInfo} = do
hostInfo@HostAppInfo {appVersion, encoding} <-
@@ -213,7 +214,9 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do
RHSessionConfirmed _ RHPendingSession {rchClient} -> Right ((), RHSessionConnected {rchClient, tls, rhClient, pollAction, storePath})
_ -> Left $ ChatErrorRemoteHost rhKey RHEBadState
chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host
toView $ CEvtRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}}
let RemoteHostClient {encryption = RemoteCrypto {compression}} = rhClient
remoteHost = rhi {sessionState = Just RHSConnected {sessionCode}} :: RemoteHostInfo
toView $ CEvtRemoteHostConnected {remoteHost, compression}
upsertRemoteHost :: RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> CM RemoteHostInfo
upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ rcAddr_ hostDeviceName sseq state = do
KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_
@@ -459,7 +462,7 @@ startRemoteCtrlSession = do
connectRemoteCtrl :: RCVerifiedInvitation -> SessionSeq -> CM (Maybe RemoteCtrlInfo, CtrlAppInfo)
connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) sseq = handleCtrlError sseq RCSRConnectionFailed "connectRemoteCtrl" $ do
ctrlInfo@CtrlAppInfo {deviceName = ctrlDeviceName} <- parseCtrlAppInfo app
ctrlInfo <- parseCtrlAppInfo app
v <- checkAppVersion ctrlInfo
rc_ <- withStore' $ \db -> getRemoteCtrlByFingerprint db ca
mapM_ (validateRemoteCtrl inv) rc_
@@ -469,23 +472,23 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app})
cmdOk <- newEmptyTMVarIO
rcsWaitSession <- async $ do
atomically $ takeTMVar cmdOk
handleCtrlError sseq RCSRConnectionFailed "waitForCtrlSession" $ waitForCtrlSession rc_ ctrlDeviceName rcsClient vars
handleCtrlError sseq RCSRConnectionFailed "waitForCtrlSession" $ waitForCtrlSession rc_ ctrlInfo rcsClient vars
updateRemoteCtrlSession sseq $ \case
RCSessionStarting -> Right RCSessionConnecting {remoteCtrlId_ = remoteCtrlId' <$> rc_, rcsClient, rcsWaitSession}
RCSessionStarting -> Right RCSessionConnecting {remoteCtrlId_ = remoteCtrlId' <$> rc_, rcsClient, rcsWaitSession, ctrlAppInfo = ctrlInfo}
_ -> Left $ ChatErrorRemoteCtrl RCEBadState
atomically $ putTMVar cmdOk ()
pure ((`remoteCtrlInfo` Just RCSConnecting) <$> rc_, ctrlInfo)
where
validateRemoteCtrl RCInvitation {idkey} RemoteCtrl {ctrlPairing = RCCtrlPairing {idPubKey}} =
unless (idkey == idPubKey) $ throwError $ ChatErrorRemoteCtrl $ RCEProtocolError $ PRERemoteControl RCEIdentity
waitForCtrlSession :: Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, TLS 'TClient, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> CM ()
waitForCtrlSession rc_ ctrlName rcsClient vars = do
waitForCtrlSession :: Maybe RemoteCtrl -> CtrlAppInfo -> RCCtrlClient -> RCStepTMVar (ByteString, TLS 'TClient, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> CM ()
waitForCtrlSession rc_ ctrlAppInfo@CtrlAppInfo {deviceName = ctrlName} rcsClient vars = do
(uniq, tls, rcsWaitConfirmation) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) networkIOTimeout $ takeRCStep vars
let sessionCode = verificationCode uniq
updateRemoteCtrlSession sseq $ \case
RCSessionConnecting {rcsWaitSession} ->
let remoteCtrlId_ = remoteCtrlId' <$> rc_
in Right RCSessionPendingConfirmation {remoteCtrlId_, ctrlDeviceName = ctrlName, rcsClient, tls, sessionCode, rcsWaitSession, rcsWaitConfirmation}
in Right RCSessionPendingConfirmation {remoteCtrlId_, ctrlDeviceName = ctrlName, rcsClient, tls, sessionCode, rcsWaitSession, rcsWaitConfirmation, ctrlAppInfo}
_ -> Left $ ChatErrorRemoteCtrl RCEBadState
toView CEvtRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode}
checkAppVersion CtrlAppInfo {appVersionRange} =
@@ -495,7 +498,8 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app})
getHostAppInfo appVersion = do
hostDeviceName <- chatReadVar localDeviceName
encryptFiles <- chatReadVar encryptLocalFiles
pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles}
useCompression <- asks $ remoteCompression . config
pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles, compression = BoolDef useCompression}
parseCtrlAppInfo :: JT.Value -> CM CtrlAppInfo
parseCtrlAppInfo ctrlAppInfo = do
@@ -514,7 +518,7 @@ handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBo
parseRequest :: ExceptT RemoteProtocolError IO (C.SbKeyNonce, GetChunk, RemoteCommand)
parseRequest = do
(rfKN, header, getNext) <- parseDecryptHTTP2Body encryption request reqBody
(rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecode header)
(rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecodeStrict header)
replyError = reply . RRChatResponse . RRError
processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM ()
processCommand user rfKN getNext = \case
@@ -611,7 +615,7 @@ remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlDeviceName} sessionState =
RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState}
-- | Take a look at emoji of tlsunique, commit pairing, and start session server
verifyRemoteCtrlSession :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo
verifyRemoteCtrlSession :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> CM ChatResponse
verifyRemoteCtrlSession execCC sessCode' = do
(sseq, client, ctrlName, sessionCode, vars) <-
chatReadVar remoteCtrlSession >>= \case
@@ -625,14 +629,15 @@ verifyRemoteCtrlSession execCC sessCode' = do
(rcsSession@RCCtrlSession {tls, sessionKeys}, rcCtrlPairing) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) networkIOTimeout $ takeRCStep vars
rc@RemoteCtrl {remoteCtrlId} <- upsertRemoteCtrl ctrlName rcCtrlPairing
remoteOutputQ <- asks (tbqSize . config) >>= newTBQueueIO
encryption <- mkCtrlRemoteCrypto sessionKeys $ tlsUniq tls
encryption@RemoteCrypto {compression} <- mkCtrlRemoteCrypto sessionKeys (tlsUniq tls) =<< getRemoteCtrlAppInfo sseq
cc <- ask
http2Server <- liftIO . async $ attachHTTP2Server tls $ \req -> handleRemoteCommand execCC encryption remoteOutputQ req `runReaderT` cc
void . forkIO $ monitor sseq http2Server
updateRemoteCtrlSession sseq $ \case
RCSessionPendingConfirmation {} -> Right RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, tls, http2Server, remoteOutputQ}
RCSessionPendingConfirmation {ctrlAppInfo} -> Right RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, tls, http2Server, remoteOutputQ, ctrlAppInfo}
_ -> Left $ ChatErrorRemoteCtrl RCEBadState
pure $ remoteCtrlInfo rc $ Just RCSConnected {sessionCode = tlsSessionCode tls}
let remoteCtrl = remoteCtrlInfo rc $ Just RCSConnected {sessionCode = tlsSessionCode tls}
pure CRRemoteCtrlConnected {remoteCtrl, compression}
where
upsertRemoteCtrl :: Text -> RCCtrlPairing -> CM RemoteCtrl
upsertRemoteCtrl ctrlName rcCtrlPairing = withStore $ \db -> do
@@ -717,6 +722,18 @@ updateRemoteCtrlSession sseq state = do
Right st' -> Right () <$ writeTVar session (Just (sseq, st'))
liftEither r
getRemoteCtrlAppInfo :: SessionSeq -> CM (Maybe CtrlAppInfo)
getRemoteCtrlAppInfo sseq = chatReadVar remoteCtrlSession $>>= pure . appInfo
where
appInfo (currSseq, sess)
| sseq == currSseq = case sess of
RCSessionStarting -> Nothing
RCSessionSearching {} -> Nothing
RCSessionConnecting {ctrlAppInfo} -> Just ctrlAppInfo
RCSessionPendingConfirmation {ctrlAppInfo} -> Just ctrlAppInfo
RCSessionConnected {ctrlAppInfo} -> Just ctrlAppInfo
| otherwise = Nothing
utf8String :: [Char] -> ByteString
utf8String = encodeUtf8 . T.pack
{-# INLINE utf8String #-}
+29 -14
View File
@@ -11,6 +11,7 @@
module Simplex.Chat.Remote.Protocol where
import qualified Codec.Compression.Zstd as Z1
import Control.Monad
import Control.Monad.Except
import Control.Monad.Reader
@@ -27,6 +28,7 @@ import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.ByteString.Builder (Builder, byteString, lazyByteString)
import qualified Data.ByteString.Lazy as LB
import qualified Data.ByteString.Lazy.Internal as LB
import Data.String (fromString)
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
@@ -37,6 +39,7 @@ import Network.Transport.Internal (decodeWord32, encodeWord32)
import Simplex.Chat.Controller
import Simplex.Chat.Remote.Transport
import Simplex.Chat.Remote.Types
import Simplex.Chat.Types (BoolDef (..))
import Simplex.FileTransfer.Description (FileDigest (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..))
@@ -102,10 +105,10 @@ $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse)
-- * Client side / desktop
mkRemoteHostClient :: HTTP2Client -> HostSessKeys -> SessionCode -> FilePath -> HostAppInfo -> CM RemoteHostClient
mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {encoding, deviceName, encryptFiles} = do
mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {encoding, deviceName, encryptFiles, compression} = do
let HostSessKeys {chainKeys, idPrivKey, sessPrivKey} = sessionKeys
signatures = RSSign {idPrivKey, sessPrivKey}
encryption <- liftIO $ mkRemoteCrypto sessionCode chainKeys signatures
encryption <- mkRemoteCrypto sessionCode chainKeys signatures $ isTrue compression
pure
RemoteHostClient
{ hostEncoding = encoding,
@@ -116,17 +119,19 @@ mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {enc
storePath
}
mkCtrlRemoteCrypto :: CtrlSessKeys -> SessionCode -> CM RemoteCrypto
mkCtrlRemoteCrypto CtrlSessKeys {chainKeys, idPubKey, sessPubKey} sessionCode =
mkCtrlRemoteCrypto :: CtrlSessKeys -> SessionCode -> Maybe CtrlAppInfo -> CM RemoteCrypto
mkCtrlRemoteCrypto CtrlSessKeys {chainKeys, idPubKey, sessPubKey} sessionCode ctrlAppInfo_ = do
let signatures = RSVerify {idPubKey, sessPubKey}
in liftIO $ mkRemoteCrypto sessionCode chainKeys signatures
peerCompression = maybe False (\CtrlAppInfo {compression} -> isTrue compression) ctrlAppInfo_
mkRemoteCrypto sessionCode chainKeys signatures peerCompression
mkRemoteCrypto :: SessionCode -> TSbChainKeys -> RemoteSignatures -> IO RemoteCrypto
mkRemoteCrypto sessionCode chainKeys signatures = do
mkRemoteCrypto :: SessionCode -> TSbChainKeys -> RemoteSignatures -> Bool -> CM RemoteCrypto
mkRemoteCrypto sessionCode chainKeys signatures peerCompression = do
sndCounter <- newTVarIO 0
rcvCounter <- newTVarIO 0
skippedKeys <- liftIO TM.emptyIO
pure RemoteCrypto {sessionCode, sndCounter, rcvCounter, chainKeys, skippedKeys, signatures}
useCompression <- asks $ remoteCompression . config
pure RemoteCrypto {sessionCode, sndCounter, rcvCounter, chainKeys, skippedKeys, signatures, compression = peerCompression && useCompression}
closeRemoteHostClient :: RemoteHostClient -> IO ()
closeRemoteHostClient RemoteHostClient {httpClient} = closeHTTP2Client httpClient
@@ -176,7 +181,7 @@ sendRemoteCommand RemoteHostClient {httpClient, hostEncoding, encryption} file_
let req = httpRequest encFile_ encCmd
HTTP2Response {response, respBody} <- liftError' (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing
(rfKN, header, getNext) <- parseDecryptHTTP2Body encryption response respBody
rr <- liftEitherWith (RPEInvalidJSON . fromString) $ J.eitherDecode header >>= JT.parseEither J.parseJSON . convertJSON hostEncoding localEncoding
rr <- liftEitherWith (RPEInvalidJSON . fromString) $ J.eitherDecodeStrict header >>= JT.parseEither J.parseJSON . convertJSON hostEncoding localEncoding
pure (rfKN, getNext, rr)
where
httpRequest encFile_ cmdBld = H.requestStreaming N.methodPost "/" mempty $ \send flush -> do
@@ -247,8 +252,11 @@ pattern OwsfTag = (SingleFieldJSONTag, J.Bool True)
-- See https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2023-10-25-remote-control.md for encoding
encryptEncodeHTTP2Body :: Word32 -> C.SbKeyNonce -> RemoteCrypto -> LazyByteString -> ExceptT RemoteProtocolError IO Builder
encryptEncodeHTTP2Body corrId cmdKN RemoteCrypto {sessionCode, signatures} s = do
ct <- liftError PRERemoteControl $ RC.rcEncryptBody cmdKN $ LB.fromStrict (smpEncode sessionCode) <> s
encryptEncodeHTTP2Body corrId cmdKN RemoteCrypto {sessionCode, signatures, compression} s = do
let s'
| compression = LB.fromStrict $ Z1.compress 3 $ LB.toStrict s
| otherwise = s
ct <- liftError PRERemoteControl $ RC.rcEncryptBody cmdKN $ LB.Chunk (smpEncode sessionCode) s'
let ctLen = encodeWord32 (fromIntegral $ LB.length ct)
signed = LB.fromStrict (encodeWord32 corrId <> ctLen) <> ct
sigs <- bodySignatures signed
@@ -266,12 +274,12 @@ encryptEncodeHTTP2Body corrId cmdKN RemoteCrypto {sessionCode, signatures} s = d
sign k = C.signatureBytes . C.sign' k . BA.convert . CH.hashFinalize
-- | Parse and decrypt HTTP2 request/response
parseDecryptHTTP2Body :: HTTP2BodyChunk a => RemoteCrypto -> a -> HTTP2Body -> ExceptT RemoteProtocolError IO (C.SbKeyNonce, LazyByteString, Int -> IO ByteString)
parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures} hr HTTP2Body {bodyBuffer} = do
parseDecryptHTTP2Body :: HTTP2BodyChunk a => RemoteCrypto -> a -> HTTP2Body -> ExceptT RemoteProtocolError IO (C.SbKeyNonce, ByteString, Int -> IO ByteString)
parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures, compression} hr HTTP2Body {bodyBuffer} = do
(corrId, ct) <- getBody
(cmdKN, rfKN) <- ExceptT $ atomically $ getRemoteRcvKeys rc corrId
s <- liftError PRERemoteControl $ RC.rcDecryptBody cmdKN ct
s' <- parseBody s
s' <- decompress =<< parseBody s
pure (rfKN, s', getNext)
where
getBody :: ExceptT RemoteProtocolError IO (Word32, LazyByteString)
@@ -320,3 +328,10 @@ parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures} hr HTTP2Body {bo
unless (LB.length bs == n) $ throwError PRESessionCode
pure (LB.toStrict bs, rest)
getNext sz = getBuffered bodyBuffer sz Nothing $ getBodyChunk hr
decompress :: LazyByteString -> ExceptT RemoteProtocolError IO ByteString
decompress s
| compression = case Z1.decompress $ LB.toStrict s of
Z1.Error e -> throwError $ RPEInvalidBody e
Z1.Skip -> pure B.empty
Z1.Decompress s' -> pure s'
| otherwise = pure $ LB.toStrict s
+7 -4
View File
@@ -21,7 +21,7 @@ import Data.Int (Int64)
import Data.Text (Text)
import Data.Word (Word16, Word32)
import Simplex.Chat.Remote.AppVersion
import Simplex.Chat.Types (verificationCode)
import Simplex.Chat.Types (BoolDef, verificationCode)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON)
@@ -47,7 +47,8 @@ data RemoteCrypto = RemoteCrypto
rcvCounter :: TVar Word32,
chainKeys :: TSbChainKeys,
skippedKeys :: TM.TMap Word32 (C.SbKeyNonce, C.SbKeyNonce),
signatures :: RemoteSignatures
signatures :: RemoteSignatures,
compression :: Bool
}
getRemoteSndKeys :: RemoteCrypto -> STM (Word32, C.SbKeyNonce, C.SbKeyNonce)
@@ -220,7 +221,8 @@ data RemoteFile = RemoteFile
data CtrlAppInfo = CtrlAppInfo
{ appVersionRange :: AppVersionRange,
deviceName :: Text
deviceName :: Text,
compression :: BoolDef
}
deriving (Show)
@@ -228,7 +230,8 @@ data HostAppInfo = HostAppInfo
{ appVersion :: AppVersion,
deviceName :: Text,
encoding :: PlatformEncoding,
encryptFiles :: Bool -- if the host encrypts files in app storage
encryptFiles :: Bool, -- if the host encrypts files in app storage
compression :: BoolDef
}
$(J.deriveJSON defaultJSON ''RemoteFile)
+4 -3
View File
@@ -5,11 +5,12 @@ module Simplex.Chat.Store.AppSettings where
import Control.Monad (join)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Aeson as J
import Data.Maybe (fromMaybe)
import Simplex.Chat.AppSettings (AppSettings (..), combineAppSettings, defaultAppSettings, defaultParseAppSettings)
import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
#if defined(dbPostgres)
import Database.PostgreSQL.Simple (Only (..))
#else
@@ -19,9 +20,9 @@ import Database.SQLite.Simple (Only (..))
saveAppSettings :: DB.Connection -> AppSettings -> IO ()
saveAppSettings db appSettings = do
DB.execute_ db "DELETE FROM app_settings"
DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ J.encode appSettings)
DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ encodeJSON appSettings)
getAppSettings :: DB.Connection -> Maybe AppSettings -> IO AppSettings
getAppSettings db platformDefaults = do
stored_ <- join <$> liftIO (maybeFirstRow (J.decodeStrict . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings")
stored_ <- join <$> liftIO (maybeFirstRow (decodeJSON . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings")
pure $ combineAppSettings (fromMaybe defaultAppSettings platformDefaults) (fromMaybe defaultParseAppSettings stored_)
+9 -195
View File
@@ -99,18 +99,10 @@ module Simplex.Chat.Store.Groups
deleteGroupMember,
deleteGroupMemberConnection,
updateGroupMemberRole,
createIntroductions,
createIntrosOrUpdateVectors,
setMemberVectorNewRelations,
setMembersVectorsNewRelation,
setMemberVectorRelationConnected,
migrateGetMemberRelationsVector,
migrateMemberRelationsVector,
migrateMemberRelationsVector',
getMemberRelationsVector_,
updateIntroStatus,
getIntroduction,
getIntroducedGroupMemberIds,
getMemberRelationsVector,
createIntroReMember,
createIntroToMemberContact,
getMatchingContacts,
@@ -151,8 +143,6 @@ module Simplex.Chat.Store.Groups
setGroupChatTTL,
getGroupChatTTL,
getUserGroupsToExpire,
hasMembersWithoutVector,
getGMsWithoutVectorIds,
updateGroupAlias,
)
where
@@ -166,7 +156,6 @@ import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Char (toLower)
import Data.Either (rights)
import Data.Foldable (foldrM)
import Data.Int (Int64)
import Data.List (partition, sortOn)
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
@@ -195,7 +184,6 @@ import Simplex.Messaging.Util (eitherToMaybe, firstRow', safeDecodeUtf8, ($>>=),
import Simplex.Messaging.Version
import UnliftIO.STM
#if defined(dbPostgres)
import qualified Data.Set as S
import Database.PostgreSQL.Simple (In (..), Only (..), Query, (:.) (..))
import Database.PostgreSQL.Simple.SqlQQ (sql)
#else
@@ -1602,85 +1590,16 @@ updateGroupMemberRole :: DB.Connection -> User -> GroupMember -> GroupMemberRole
updateGroupMemberRole db User {userId} GroupMember {groupMemberId} memRole =
DB.execute db "UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_member_id = ?" (memRole, userId, groupMemberId)
createIntroductions :: DB.Connection -> VersionChat -> [GroupMember] -> GroupMember -> IO [GroupMember]
createIntroductions db chatV reMembers toMember
| null reMembers = pure []
| otherwise = do
currentTs <- getCurrentTime
catMaybes <$> mapM (createIntro_ currentTs) reMembers
where
createIntro_ :: UTCTime -> GroupMember -> IO (Maybe GroupMember)
createIntro_ ts reMember =
-- when members connect concurrently, host would try to create introductions between them in both directions;
-- this check avoids creating second (redundant) introduction
checkInverseIntro >>= \case
Just _ -> pure Nothing
Nothing -> do
DB.execute
db
[sql|
INSERT INTO group_member_intros
(re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at)
VALUES (?,?,?,?,?,?)
|]
(groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts)
pure $ Just reMember
where
checkInverseIntro :: IO (Maybe Int64)
checkInverseIntro =
maybeFirstRow fromOnly $
DB.query
db
"SELECT 1 FROM group_member_intros WHERE re_group_member_id = ? AND to_group_member_id = ? LIMIT 1"
(groupMemberId' toMember, groupMemberId' reMember)
-- Create introductions for members without vectors and update vectors for members with vectors.
-- Partitioning and updates happen in same transaction to avoid race conditions.
createIntrosOrUpdateVectors :: DB.Connection -> VersionRangeChat -> [GroupMember] -> GroupMember -> IO [GroupMember]
createIntrosOrUpdateVectors db vr reMembers toMember
| null reMembers = pure []
| otherwise = do
(memsWithVec, memsWithoutVec) <- partitionByVector reMembers
let GroupMember {indexInGroup} = toMember
setMembersVectorsNewRelation db memsWithVec indexInGroup IDSubjectIntroduced MRIntroduced
memsWithoutVec' <- createIntroductions db (maxVersion vr) memsWithoutVec toMember
pure $ memsWithoutVec' <> memsWithVec
where
partitionByVector :: [GroupMember] -> IO ([GroupMember], [GroupMember])
#if defined(dbPostgres)
partitionByVector members = do
let memberIds = map groupMemberId' members
-- Lock rows first to ensure partitioning doesn't change in case of concurrent updates
_ :: [Only Int] <-
DB.query
db
"SELECT 1 FROM group_members WHERE group_member_id IN ? FOR UPDATE"
(Only $ In memberIds)
memberIdsWithVec <- S.fromList . map fromOnly <$>
DB.query
db
"SELECT group_member_id FROM group_members WHERE group_member_id IN ? AND member_relations_vector IS NOT NULL"
(Only $ In memberIds)
pure $ partition (\m -> groupMemberId' m `S.member` memberIdsWithVec) members
#else
partitionByVector = foldrM checkMember ([], [])
where
checkMember m (withVec, withoutVec) = do
hasVec <- isJust <$> maybeFirstRow fromOnly
(DB.query db "SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL" (Only $ groupMemberId' m) :: IO [Only Int64])
pure $ if hasVec then (m : withVec, withoutVec) else (withVec, m : withoutVec)
#endif
setMemberVectorNewRelations :: DB.Connection -> GroupMember -> [(Int64, (IntroductionDirection, MemberRelation))] -> IO ()
setMemberVectorNewRelations db GroupMember {groupMemberId} relations = do
v_ <- maybeFirstRow fromOnly $
DB.query
db
( "SELECT member_relations_vector FROM group_members WHERE group_member_id = ?"
#if defined(dbPostgres)
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ? FOR UPDATE"
#else
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ?"
<> " FOR UPDATE"
#endif
)
(Only groupMemberId)
let v' = setNewRelations relations $ fromMaybe B.empty v_
currentTs <- getCurrentTime
@@ -1718,11 +1637,11 @@ setMemberVectorRelationConnected db GroupMember {groupMemberId} GroupMember {ind
firstRow fromOnly (SEMemberRelationsVectorNotFound groupMemberId) $
DB.query
db
( "SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL"
#if defined(dbPostgres)
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL FOR UPDATE"
#else
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL"
<> " FOR UPDATE"
#endif
)
(Only groupMemberId)
let v' = setRelationConnected indexInGroup newStatus v
currentTs <- liftIO getCurrentTime
@@ -1735,100 +1654,14 @@ setMemberVectorRelationConnected db GroupMember {groupMemberId} GroupMember {ind
|]
(Binary v', currentTs, groupMemberId)
migrateGetMemberRelationsVector :: DB.Connection -> GroupMember -> ExceptT StoreError IO ByteString
migrateGetMemberRelationsVector db m@GroupMember {groupMemberId} = do
liftIO $ migrateMemberRelationsVector db m
getMemberRelationsVector :: DB.Connection -> GroupMember -> ExceptT StoreError IO ByteString
getMemberRelationsVector db GroupMember {groupMemberId} =
ExceptT . firstRow fromOnly (SEGroupMemberNotFound groupMemberId) $
DB.query
db
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ?"
(Only groupMemberId)
migrateMemberRelationsVector :: DB.Connection -> GroupMember -> IO ()
migrateMemberRelationsVector db GroupMember {groupMemberId} =
migrateMemberRelationsVector' db groupMemberId
migrateMemberRelationsVector' :: DB.Connection -> GroupMemberId -> IO ()
migrateMemberRelationsVector' db groupMemberId = do
currentTs <- liftIO getCurrentTime
liftIO $ do
#if defined(dbPostgres)
-- Lock the row first to ensure computation runs only after lock is acquired
_ :: [Only Int] <-
DB.query
db
"SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NULL FOR UPDATE"
(Only groupMemberId)
#endif
DB.execute
db
[sql|
UPDATE group_members
SET
member_relations_vector = (
SELECT migrate_relations_vector(idx, direction, intro_status)
FROM (
SELECT m.index_in_group AS idx, 0 AS direction, i.intro_status
FROM group_member_intros i
JOIN group_members m ON m.group_member_id = i.to_group_member_id
WHERE i.re_group_member_id = group_members.group_member_id
UNION ALL
SELECT m.index_in_group AS idx, 1 AS direction, i.intro_status
FROM group_member_intros i
JOIN group_members m ON m.group_member_id = i.re_group_member_id
WHERE i.to_group_member_id = group_members.group_member_id
) AS relations
),
updated_at = ?
WHERE group_member_id = ?
AND member_relations_vector IS NULL
|]
(currentTs, groupMemberId)
getMemberRelationsVector_ :: DB.Connection -> GroupMember -> IO (Maybe ByteString)
getMemberRelationsVector_ db GroupMember {groupMemberId} =
maybeFirstRow fromOnly $
DB.query
db
"SELECT member_relations_vector FROM group_members WHERE group_member_id = ?"
(Only groupMemberId)
updateIntroStatus :: DB.Connection -> Int64 -> GroupMemberIntroStatus -> IO ()
updateIntroStatus db introId introStatus = do
currentTs <- getCurrentTime
DB.execute
db
[sql|
UPDATE group_member_intros
SET intro_status = ?, updated_at = ?
WHERE group_member_intro_id = ?
|]
(introStatus, currentTs, introId)
getIntroduction :: DB.Connection -> GroupMember -> GroupMember -> IO (Maybe GroupMemberIntro)
getIntroduction db reMember toMember =
maybeFirstRow toIntro $
DB.query
db
[sql|
SELECT group_member_intro_id, intro_status
FROM group_member_intros
WHERE re_group_member_id = ? AND to_group_member_id = ?
|]
(groupMemberId' reMember, groupMemberId' toMember)
where
toIntro :: (Int64, GroupMemberIntroStatus) -> GroupMemberIntro
toIntro (introId, introStatus) =
GroupMemberIntro {introId, reMember, toMember, introStatus}
getIntroducedGroupMemberIds :: DB.Connection -> GroupMember -> IO [GroupMemberId]
getIntroducedGroupMemberIds db invitee =
map fromOnly <$>
DB.query
db
"SELECT re_group_member_id FROM group_member_intros WHERE to_group_member_id = ?"
(Only $ groupMemberId' invitee)
createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> VersionChat -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember
createIntroReMember
db
@@ -2718,25 +2551,6 @@ getUserGroupsToExpire db User {userId} globalTTL =
where
cond = if globalTTL == 0 then "" else " OR chat_item_ttl IS NULL"
hasMembersWithoutVector :: DB.Connection -> IO Bool
hasMembersWithoutVector db =
fromOnly . head
<$> DB.query_
db
"SELECT EXISTS (SELECT 1 FROM group_members WHERE member_relations_vector IS NULL LIMIT 1)"
getGMsWithoutVectorIds :: DB.Connection -> IO [GroupMemberId]
getGMsWithoutVectorIds db =
map fromOnly <$>
DB.query_
db
[sql|
SELECT group_member_id
FROM group_members
WHERE member_relations_vector IS NULL
LIMIT 1000
|]
updateGroupAlias :: DB.Connection -> UserId -> GroupInfo -> LocalAlias -> IO GroupInfo
updateGroupAlias db userId g@GroupInfo {groupId} localAlias = do
updatedAt <- getCurrentTime
+205 -250
View File
@@ -45,6 +45,7 @@ module Simplex.Chat.Store.Messages
createNewChatItem_,
getChatPreviews,
checkContactHasItems,
getChatContentTypes,
getDirectChat,
getGroupChat,
getGroupChatScopeInfoForItem,
@@ -52,7 +53,6 @@ module Simplex.Chat.Store.Messages
getDirectChatItemLast,
getAllChatItems,
getAChatItem,
getAChatItemBySharedMsgId,
updateDirectChatItem,
updateDirectChatItem',
addInitialAndNewCIVersions,
@@ -335,24 +335,24 @@ updateSndMsgDeliveryStatus db connId agentMsgId sndMsgDeliveryStatus = do
|]
(sndMsgDeliveryStatus, currentTs, connId, agentMsgId)
createPendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> Maybe Int64 -> IO ()
createPendingGroupMessage db groupMemberId messageId introId_ = do
createPendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO ()
createPendingGroupMessage db groupMemberId messageId = do
currentTs <- getCurrentTime
DB.execute
db
[sql|
INSERT INTO pending_group_messages
(group_member_id, message_id, group_member_intro_id, created_at, updated_at) VALUES (?,?,?,?,?)
(group_member_id, message_id, created_at, updated_at) VALUES (?,?,?,?)
|]
(groupMemberId, messageId, introId_, currentTs, currentTs)
(groupMemberId, messageId, currentTs, currentTs)
getPendingGroupMessages :: DB.Connection -> Int64 -> IO [(SndMessage, ACMEventTag, Maybe Int64)]
getPendingGroupMessages :: DB.Connection -> Int64 -> IO [SndMessage]
getPendingGroupMessages db groupMemberId =
map pendingGroupMessage
<$> DB.query
db
[sql|
SELECT pgm.message_id, m.shared_msg_id, m.msg_body, m.chat_msg_event, pgm.group_member_intro_id
SELECT pgm.message_id, m.shared_msg_id, m.msg_body
FROM pending_group_messages pgm
JOIN messages m USING (message_id)
WHERE pgm.group_member_id = ?
@@ -360,8 +360,8 @@ getPendingGroupMessages db groupMemberId =
|]
(Only groupMemberId)
where
pendingGroupMessage (msgId, sharedMsgId, msgBody, cmEventTag, introId_) =
(SndMessage {msgId, sharedMsgId, msgBody}, cmEventTag, introId_)
pendingGroupMessage (msgId, sharedMsgId, msgBody) =
SndMessage {msgId, sharedMsgId, msgBody}
deletePendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO ()
deletePendingGroupMessage db groupMemberId messageId =
@@ -1167,40 +1167,44 @@ checkContactHasItems db User {userId} Contact {contactId} =
"SELECT EXISTS (SELECT 1 FROM chat_items WHERE user_id = ? AND contact_id = ?)"
(userId, contactId)
getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChat db vr user contactId pagination search_ = do
getChatContentTypes :: DB.Connection -> User -> ChatRef -> ExceptT StoreError IO [MsgContentTag]
getChatContentTypes db User {userId} (ChatRef cType chatId chatScope_) = case cType of
CTDirect -> getTypes " contact_id = ? " ()
CTLocal -> getTypes " note_folder_id = ? " ()
CTGroup -> case chatScope_ of
Nothing -> getTypes " group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL " ()
Just (GCSMemberSupport mId_) -> getTypes " group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? " (GCSTMemberSupport_, mId_)
_ -> throwError $ SEInternalError "unsupported chat type"
where
getTypes :: ToRow p => Query -> p -> ExceptT StoreError IO [MsgContentTag]
getTypes cond params =
liftIO $ map fromOnly
<$> DB.query
db
("SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND " <> cond <> " AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag")
((userId, chatId) :. params)
getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChat db vr user contactId contentFilter pagination search_ = do
let search = fromMaybe "" search_
ct <- getContact db vr user contactId
case pagination of
CPLast count -> liftIO $ (,Nothing) <$> getDirectChatLast_ db user ct count search
CPAfter afterId count -> (,Nothing) <$> getDirectChatAfter_ db user ct afterId count search
CPBefore beforeId count -> (,Nothing) <$> getDirectChatBefore_ db user ct beforeId count search
CPAround aroundId count -> getDirectChatAround_ db user ct aroundId count search
CPLast count -> (,Nothing) <$> getDirectChatLast_ db user ct contentFilter count search
CPAfter afterId count -> (,Nothing) <$> getDirectChatAfter_ db user ct contentFilter afterId count search
CPBefore beforeId count -> (,Nothing) <$> getDirectChatBefore_ db user ct contentFilter beforeId count search
CPAround aroundId count -> getDirectChatAround_ db user ct contentFilter aroundId count search
CPInitial count -> do
unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
getDirectChatInitial_ db user ct count
getDirectChatInitial_ db user ct contentFilter count
-- the last items in reverse order (the last item in the conversation is the first in the returned list)
getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> Text -> IO (Chat 'CTDirect)
getDirectChatLast_ db user ct count search = do
ciIds <- getDirectChatItemIdsLast_ db user ct count search
ts <- getCurrentTime
cis <- mapM (safeGetDirectItem db user ct ts) ciIds
pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats
getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> Text -> IO [ChatItemId]
getDirectChatItemIdsLast_ db User {userId} Contact {contactId} count search =
map fromOnly
<$> DB.query
db
[sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
ORDER BY created_at DESC, chat_item_id DESC
LIMIT ?
|]
(userId, contactId, search, count)
getDirectChatLast_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatLast_ db user ct contentFilter count search = do
let cInfo = DirectChat ct
ciIds <- getChatItemIDs db user cInfo contentFilter CRLast count search
ts <- liftIO getCurrentTime
cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds
pure $ Chat cInfo (reverse cis) emptyChatStats
safeGetDirectItem :: DB.Connection -> User -> Contact -> UTCTime -> ChatItemId -> IO (CChatItem 'CTDirect)
safeGetDirectItem db user ct currentTs itemId =
@@ -1235,91 +1239,71 @@ getDirectChatItemLast db user@User {userId} contactId = do
ExceptT . firstRow fromOnly (SEChatItemNotFoundByContactId contactId) $
DB.query
db
[sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ?
ORDER BY created_at DESC, chat_item_id DESC
LIMIT 1
|]
( [sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ?
ORDER BY created_at DESC, chat_item_id DESC
LIMIT 1
|]
#if defined(dbPostgres)
<> " FOR UPDATE"
#endif
)
(userId, contactId)
getDirectChatItem db user contactId chatItemId
getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatAfter_ db user ct@Contact {contactId} afterId count search = do
getDirectChatAfter_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatAfter_ db user ct@Contact {contactId} contentFilter afterId count search = do
afterCI <- getDirectChatItem db user contactId afterId
ciIds <- liftIO $ getDirectCIsAfter_ db user ct afterCI count search
let cInfo = DirectChat ct
range = CRAfter (ciCreatedAt afterCI) (cChatItemId afterCI)
ciIds <- getChatItemIDs db user cInfo contentFilter range count search
ts <- liftIO getCurrentTime
cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds
pure $ Chat (DirectChat ct) cis emptyChatStats
pure $ Chat cInfo cis emptyChatStats
getDirectCIsAfter_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> Text -> IO [ChatItemId]
getDirectCIsAfter_ db User {userId} Contact {contactId} afterCI count search =
map fromOnly
<$> DB.query
db
[sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (created_at > ? OR (created_at = ? AND chat_item_id > ?))
ORDER BY created_at ASC, chat_item_id ASC
LIMIT ?
|]
(userId, contactId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count)
getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatBefore_ db user ct@Contact {contactId} beforeId count search = do
getDirectChatBefore_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect)
getDirectChatBefore_ db user ct@Contact {contactId} contentFilter beforeId count search = do
beforeCI <- getDirectChatItem db user contactId beforeId
ciIds <- liftIO $ getDirectCIsBefore_ db user ct beforeCI count search
let cInfo = DirectChat ct
range = CRBefore (ciCreatedAt beforeCI) (cChatItemId beforeCI)
ciIds <- getChatItemIDs db user cInfo contentFilter range count search
ts <- liftIO getCurrentTime
cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds
pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats
pure $ Chat cInfo (reverse cis) emptyChatStats
getDirectCIsBefore_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> Text -> IO [ChatItemId]
getDirectCIsBefore_ db User {userId} Contact {contactId} beforeCI count search =
map fromOnly
<$> DB.query
db
[sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (created_at < ? OR (created_at = ? AND chat_item_id < ?))
ORDER BY created_at DESC, chat_item_id DESC
LIMIT ?
|]
(userId, contactId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count)
getDirectChatAround_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatAround_ db user ct aroundId count search = do
getDirectChatAround_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatAround_ db user ct contentFilter aroundId count search = do
stats <- liftIO $ getContactStats_ db user ct
getDirectChatAround' db user ct aroundId count search stats
getDirectChatAround' db user ct contentFilter aroundId count search stats
getDirectChatAround' :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatAround' db user ct@Contact {contactId} aroundId count search stats = do
getDirectChatAround' :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatAround' db user ct@Contact {contactId} contentFilter aroundId count search stats = do
aroundCI <- getDirectChatItem db user contactId aroundId
beforeIds <- liftIO $ getDirectCIsBefore_ db user ct aroundCI count search
afterIds <- liftIO $ getDirectCIsAfter_ db user ct aroundCI count search
let cInfo = DirectChat ct
range r = r (ciCreatedAt aroundCI) (cChatItemId aroundCI)
beforeIds <- getChatItemIDs db user cInfo contentFilter (range CRBefore) count search
afterIds <- getChatItemIDs db user cInfo contentFilter (range CRAfter) count search
ts <- liftIO getCurrentTime
beforeCIs <- liftIO $ mapM (safeGetDirectItem db user ct ts) beforeIds
afterCIs <- liftIO $ mapM (safeGetDirectItem db user ct ts) afterIds
let cis = reverse beforeCIs <> [aroundCI] <> afterCIs
navInfo <- liftIO $ getNavInfo cis
pure (Chat (DirectChat ct) cis stats, Just navInfo)
pure (Chat cInfo cis stats, Just navInfo)
where
getNavInfo cis_ = case cis_ of
[] -> pure $ NavigationInfo 0 0
cis -> getContactNavInfo_ db user ct (last cis)
getDirectChatInitial_ :: DB.Connection -> User -> Contact -> Int -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatInitial_ db user ct count = do
getDirectChatInitial_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> Int -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo)
getDirectChatInitial_ db user ct contentFilter count = do
liftIO (getContactMinUnreadId_ db user ct) >>= \case
Just minUnreadItemId -> do
unreadCount <- liftIO $ getContactUnreadCount_ db user ct
let stats = emptyChatStats {unreadCount, minUnreadItemId}
getDirectChatAround' db user ct minUnreadItemId count "" stats
Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getDirectChatLast_ db user ct count ""
getDirectChatAround' db user ct contentFilter minUnreadItemId count "" stats
Nothing -> (,Just $ NavigationInfo 0 0) <$> getDirectChatLast_ db user ct contentFilter count ""
getContactStats_ :: DB.Connection -> User -> Contact -> IO ChatStats
getContactStats_ db user ct = do
@@ -1468,64 +1452,81 @@ getGroupChatScopeForItem_ db itemId =
getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatLast_ db user g scopeInfo_ contentFilter count search stats = do
ciIds <- getGroupChatItemIDs db user g scopeInfo_ contentFilter GRLast count search
let cInfo = GroupChat g scopeInfo_
ciIds <- getChatItemIDs db user cInfo contentFilter CRLast count search
ts <- liftIO getCurrentTime
cis <- mapM (liftIO . safeGetGroupItem db user g ts) ciIds
pure $ Chat (GroupChat g scopeInfo_) (reverse cis) stats
pure $ Chat cInfo (reverse cis) stats
data GroupItemIDsRange = GRLast | GRAfter UTCTime ChatItemId | GRBefore UTCTime ChatItemId
data ChatItemIDsRange = CRLast | CRAfter UTCTime ChatItemId | CRBefore UTCTime ChatItemId
getGroupChatItemIDs :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> GroupItemIDsRange -> Int -> Text -> ExceptT StoreError IO [ChatItemId]
getGroupChatItemIDs db User {userId} GroupInfo {groupId} scopeInfo_ contentFilter range count search = case (scopeInfo_, contentFilter) of
(Nothing, Nothing) ->
liftIO $
idsQuery
(baseCond <> " AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL ")
(userId, groupId)
(Nothing, Just mcTag) ->
liftIO $
idsQuery
(baseCond <> " AND msg_content_tag = ? ")
(userId, groupId, mcTag)
(Just GCSIMemberSupport {groupMember_ = Just m}, Nothing) ->
liftIO $
idsQuery
(baseCond <> " AND group_scope_tag = ? AND group_scope_group_member_id = ? ")
(userId, groupId, GCSTMemberSupport_, groupMemberId' m)
(Just GCSIMemberSupport {groupMember_ = Nothing}, Nothing) ->
liftIO $
idsQuery
(baseCond <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NULL ")
(userId, groupId, GCSTMemberSupport_)
(Just _scope, Just _mcTag) ->
throwError $ SEInternalError "group scope and content filter are not supported together"
getChatItemIDs :: DB.Connection -> User -> ChatInfo c -> Maybe MsgContentTag -> ChatItemIDsRange -> Int -> Text -> ExceptT StoreError IO [ChatItemId]
getChatItemIDs db User {userId} cInfo contentFilter range count search = case cInfo of
GroupChat GroupInfo {groupId} scopeInfo_ -> case (scopeInfo_, contentFilter) of
(Nothing, Nothing) ->
liftIO $
idsQuery
(grCond <> " AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL ")
(userId, groupId)
"item_ts"
(Nothing, Just mcTag) ->
liftIO $
idsQuery
(grCond <> " AND msg_content_tag = ? ")
(userId, groupId, mcTag)
"item_ts"
(Just GCSIMemberSupport {groupMember_ = m}, Nothing) ->
liftIO $
idsQuery
(grCond <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? ")
(userId, groupId, GCSTMemberSupport_, groupMemberId' <$> m)
"item_ts"
(Just _scope, Just _mcTag) ->
throwError $ SEInternalError "group scope and content filter are not supported together"
where
grCond = " user_id = ? AND group_id = ? "
DirectChat Contact {contactId} -> liftIO $ case contentFilter of
Nothing -> idsQuery ctCond (userId, contactId) "created_at"
Just mcTag -> idsQuery (ctCond <> " AND msg_content_tag = ? ") (userId, contactId, mcTag) "created_at"
where
ctCond = " user_id = ? AND contact_id = ? "
LocalChat NoteFolder {noteFolderId} -> liftIO $ case contentFilter of
Nothing -> idsQuery nfCond (userId, noteFolderId) "created_at"
Just mcTag -> idsQuery (nfCond <> " AND msg_content_tag = ? ") (userId, noteFolderId, mcTag) "created_at"
where
nfCond = " user_id = ? AND note_folder_id = ? "
_ -> throwError $ SEInternalError "unsupported chat type"
where
baseQuery = " SELECT chat_item_id FROM chat_items WHERE "
baseCond = " user_id = ? AND group_id = ? "
idsQuery :: ToRow p => Query -> p -> IO [ChatItemId]
idsQuery c p = case range of
GRLast -> rangeQuery c p " ORDER BY item_ts DESC, chat_item_id DESC "
GRAfter ts itemId ->
-- parameterized by timestamp field `f` used to order chat items:
-- `item_ts` for groups, `created_at` for direct chats and notes.
idsQuery :: ToRow p => Query -> p -> Query -> IO [ChatItemId]
idsQuery c p f = case range of
CRLast -> rangeQuery c p (" ORDER BY " <> f <> " DESC, chat_item_id DESC ")
CRAfter ts itemId ->
rangeQuery
(" item_ts > ? " `orCond` " item_ts = ? AND chat_item_id > ? ")
((f <> " > ?") `orCond` (f <> " = ? AND chat_item_id > ?"))
(orParams ts itemId)
" ORDER BY item_ts ASC, chat_item_id ASC "
GRBefore ts itemId ->
(" ORDER BY " <> f <> " ASC, chat_item_id ASC ")
CRBefore ts itemId ->
rangeQuery
(" item_ts < ? " `orCond` " item_ts = ? AND chat_item_id < ? ")
((f <> " < ?") `orCond` (f <> " = ? AND chat_item_id < ?"))
(orParams ts itemId)
" ORDER BY item_ts DESC, chat_item_id DESC "
(" ORDER BY " <> f <> " DESC, chat_item_id DESC ")
where
-- `orCond` creates this query: `(c AND c1) OR (c AND c2)`,
-- that is equivalent to `c AND (c1 OR c2)`.
-- OR has to be used on the top level for query planner to use indices
-- that include fields in c1 and c2.
orCond c1 c2 = " ((" <> c <> " AND " <> c1 <> ") OR (" <> c <> " AND " <> c2 <> ")) "
orParams ts itemId = (p :. (Only ts) :. p :. (ts, itemId))
rangeQuery :: ToRow p => Query -> p -> Query -> IO [ChatItemId]
rangeQuery c p ob
| T.null search = searchQuery "" ()
| otherwise = searchQuery " AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' " (Only search)
where
searchQuery :: ToRow p' => Query -> p' -> IO [ChatItemId]
searchQuery c' p' =
map fromOnly <$> DB.query db (baseQuery <> c <> c' <> ob <> " LIMIT ?") (p :. p' :. Only count)
rangeQuery c p ob =
map fromOnly
<$> if T.null search
then DB.query db (baseQuery <> c <> ob <> " LIMIT ?") (p :. Only count)
else DB.query db (baseQuery <> c <> searchCond <> ob <> " LIMIT ?") (p :. (search, count))
searchCond = " AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' "
safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup)
safeGetGroupItem db user g currentTs itemId =
@@ -1560,33 +1561,39 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do
ExceptT . firstRow fromOnly (SEChatItemNotFoundByGroupId groupId) $
DB.query
db
[sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND group_id = ? AND group_member_id = ?
ORDER BY item_ts DESC, chat_item_id DESC
LIMIT 1
|]
( [sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND group_id = ? AND group_member_id = ?
ORDER BY item_ts DESC, chat_item_id DESC
LIMIT 1
|]
#if defined(dbPostgres)
<> " FOR UPDATE"
#endif
)
(userId, groupId, groupMemberId)
getGroupChatItem db user groupId chatItemId
getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatAfter_ db user g@GroupInfo {groupId} scopeInfo contentFilter afterId count search = do
afterCI <- getGroupChatItem db user groupId afterId
let range = GRAfter (chatItemTs afterCI) (cChatItemId afterCI)
ciIds <- getGroupChatItemIDs db user g scopeInfo contentFilter range count search
let cInfo = GroupChat g scopeInfo
range = CRAfter (chatItemTs afterCI) (cChatItemId afterCI)
ciIds <- getChatItemIDs db user cInfo contentFilter range count search
ts <- liftIO getCurrentTime
cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds
pure $ Chat (GroupChat g scopeInfo) cis emptyChatStats
pure $ Chat cInfo cis emptyChatStats
getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup)
getGroupChatBefore_ db user g@GroupInfo {groupId} scopeInfo contentFilter beforeId count search = do
beforeCI <- getGroupChatItem db user groupId beforeId
let range = GRBefore (chatItemTs beforeCI) (cChatItemId beforeCI)
ciIds <- getGroupChatItemIDs db user g scopeInfo contentFilter range count search
let cInfo = GroupChat g scopeInfo
range = CRBefore (chatItemTs beforeCI) (cChatItemId beforeCI)
ciIds <- getChatItemIDs db user cInfo contentFilter range count search
ts <- liftIO getCurrentTime
cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds
pure $ Chat (GroupChat g scopeInfo) (reverse cis) emptyChatStats
pure $ Chat cInfo (reverse cis) emptyChatStats
getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo)
getGroupChatAround_ db user g scopeInfo contentFilter aroundId count search = do
@@ -1596,16 +1603,16 @@ getGroupChatAround_ db user g scopeInfo contentFilter aroundId count search = do
getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo)
getGroupChatAround' db user g scopeInfo contentFilter aroundId count search stats = do
aroundCI <- getGroupCIWithReactions db user g aroundId
let beforeRange = GRBefore (chatItemTs aroundCI) (cChatItemId aroundCI)
afterRange = GRAfter (chatItemTs aroundCI) (cChatItemId aroundCI)
beforeIds <- getGroupChatItemIDs db user g scopeInfo contentFilter beforeRange count search
afterIds <- getGroupChatItemIDs db user g scopeInfo contentFilter afterRange count search
let cInfo = GroupChat g scopeInfo
range r = r (chatItemTs aroundCI) (cChatItemId aroundCI)
beforeIds <- getChatItemIDs db user cInfo contentFilter (range CRBefore) count search
afterIds <- getChatItemIDs db user cInfo contentFilter (range CRAfter) count search
ts <- liftIO getCurrentTime
beforeCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) beforeIds
afterCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) afterIds
let cis = reverse beforeCIs <> [aroundCI] <> afterCIs
navInfo <- liftIO $ getNavInfo cis
pure (Chat (GroupChat g scopeInfo) cis stats, Just navInfo)
pure (Chat cInfo cis stats, Just navInfo)
where
getNavInfo cis_ = case cis_ of
[] -> pure $ NavigationInfo 0 0
@@ -1670,18 +1677,12 @@ queryUnreadGroupItems db User {userId} GroupInfo {groupId} scopeInfo_ contentFil
db
(baseQuery <> " AND msg_content_tag = ? AND item_status = ? " <> orderLimit)
(userId, groupId, mcTag, CISRcvNew)
(Just GCSIMemberSupport {groupMember_ = Just m}, Nothing) ->
(Just GCSIMemberSupport {groupMember_ = m}, Nothing) ->
liftIO $
DB.query
db
(baseQuery <> " AND group_scope_tag = ? AND group_scope_group_member_id = ? AND item_status = ? " <> orderLimit)
(userId, groupId, GCSTMemberSupport_, groupMemberId' m, CISRcvNew)
(Just GCSIMemberSupport {groupMember_ = Nothing}, Nothing) ->
liftIO $
DB.query
db
(baseQuery <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NULL AND item_status = ? " <> orderLimit)
(userId, groupId, GCSTMemberSupport_, CISRcvNew)
(baseQuery <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? AND item_status = ? " <> orderLimit)
(userId, groupId, GCSTMemberSupport_, groupMemberId' <$> m, CISRcvNew)
(Just _scope, Just _mcTag) ->
throwError $ SEInternalError "group scope and content filter are not supported together"
@@ -1736,39 +1737,26 @@ getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do
:. (userId, groupId, chatItemTs afterCI, cChatItemId afterCI)
)
getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChat db user folderId pagination search_ = do
getLocalChat :: DB.Connection -> User -> Int64 -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChat db user folderId contentFilter pagination search_ = do
let search = fromMaybe "" search_
nf <- getNoteFolder db user folderId
case pagination of
CPLast count -> liftIO $ (,Nothing) <$> getLocalChatLast_ db user nf count search
CPAfter afterId count -> (,Nothing) <$> getLocalChatAfter_ db user nf afterId count search
CPBefore beforeId count -> (,Nothing) <$> getLocalChatBefore_ db user nf beforeId count search
CPAround aroundId count -> getLocalChatAround_ db user nf aroundId count search
CPLast count -> (,Nothing) <$> getLocalChatLast_ db user nf contentFilter count search
CPAfter afterId count -> (,Nothing) <$> getLocalChatAfter_ db user nf contentFilter afterId count search
CPBefore beforeId count -> (,Nothing) <$> getLocalChatBefore_ db user nf contentFilter beforeId count search
CPAround aroundId count -> getLocalChatAround_ db user nf contentFilter aroundId count search
CPInitial count -> do
unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search"
getLocalChatInitial_ db user nf count
getLocalChatInitial_ db user nf contentFilter count
getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> Text -> IO (Chat 'CTLocal)
getLocalChatLast_ db user nf count search = do
ciIds <- getLocalChatItemIdsLast_ db user nf count search
ts <- getCurrentTime
cis <- mapM (safeGetLocalItem db user nf ts) ciIds
pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats
getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> Text -> IO [ChatItemId]
getLocalChatItemIdsLast_ db User {userId} NoteFolder {noteFolderId} count search =
map fromOnly
<$> DB.query
db
[sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
ORDER BY created_at DESC, chat_item_id DESC
LIMIT ?
|]
(userId, noteFolderId, search, count)
getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatLast_ db user nf contentFilter count search = do
let cInfo = LocalChat nf
ciIds <- getChatItemIDs db user cInfo contentFilter CRLast count search
ts <- liftIO getCurrentTime
cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds
pure $ Chat cInfo (reverse cis) emptyChatStats
safeGetLocalItem :: DB.Connection -> User -> NoteFolder -> UTCTime -> ChatItemId -> IO (CChatItem 'CTLocal)
safeGetLocalItem db user NoteFolder {noteFolderId} currentTs itemId =
@@ -1797,81 +1785,57 @@ safeToLocalItem currentTs itemId = \case
file = Nothing
}
getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterId count search = do
getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} contentFilter afterId count search = do
afterCI <- getLocalChatItem db user noteFolderId afterId
ciIds <- liftIO $ getLocalCIsAfter_ db user nf afterCI count search
let cInfo = LocalChat nf
range = CRAfter (ciCreatedAt afterCI) (cChatItemId afterCI)
ciIds <- getChatItemIDs db user cInfo contentFilter range count search
ts <- liftIO getCurrentTime
cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds
pure $ Chat (LocalChat nf) cis emptyChatStats
pure $ Chat cInfo cis emptyChatStats
getLocalCIsAfter_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> Text -> IO [ChatItemId]
getLocalCIsAfter_ db User {userId} NoteFolder {noteFolderId} afterCI count search =
map fromOnly
<$> DB.query
db
[sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (created_at > ? OR (created_at = ? AND chat_item_id > ?))
ORDER BY created_at ASC, chat_item_id ASC
LIMIT ?
|]
(userId, noteFolderId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count)
getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeId count search = do
getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal)
getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} contentFilter beforeId count search = do
beforeCI <- getLocalChatItem db user noteFolderId beforeId
ciIds <- liftIO $ getLocalCIsBefore_ db user nf beforeCI count search
let cInfo = LocalChat nf
range = CRBefore (ciCreatedAt beforeCI) (cChatItemId beforeCI)
ciIds <- getChatItemIDs db user cInfo contentFilter range count search
ts <- liftIO getCurrentTime
cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds
pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats
pure $ Chat cInfo (reverse cis) emptyChatStats
getLocalCIsBefore_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> Text -> IO [ChatItemId]
getLocalCIsBefore_ db User {userId} NoteFolder {noteFolderId} beforeCI count search =
map fromOnly
<$> DB.query
db
[sql|
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (created_at < ? OR (created_at = ? AND chat_item_id < ?))
ORDER BY created_at DESC, chat_item_id DESC
LIMIT ?
|]
(userId, noteFolderId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count)
getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatAround_ db user nf aroundId count search = do
getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatAround_ db user nf contentFilter aroundId count search = do
stats <- liftIO $ getLocalStats_ db user nf
getLocalChatAround' db user nf aroundId count search stats
getLocalChatAround' db user nf contentFilter aroundId count search stats
getLocalChatAround' :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatAround' db user nf@NoteFolder {noteFolderId} aroundId count search stats = do
getLocalChatAround' :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatAround' db user nf@NoteFolder {noteFolderId} contentFilter aroundId count search stats = do
aroundCI <- getLocalChatItem db user noteFolderId aroundId
beforeIds <- liftIO $ getLocalCIsBefore_ db user nf aroundCI count search
afterIds <- liftIO $ getLocalCIsAfter_ db user nf aroundCI count search
let cInfo = LocalChat nf
range r = r (ciCreatedAt aroundCI) (cChatItemId aroundCI)
beforeIds <- getChatItemIDs db user cInfo contentFilter (range CRBefore) count search
afterIds <- getChatItemIDs db user cInfo contentFilter (range CRAfter) count search
ts <- liftIO getCurrentTime
beforeCIs <- liftIO $ mapM (safeGetLocalItem db user nf ts) beforeIds
afterCIs <- liftIO $ mapM (safeGetLocalItem db user nf ts) afterIds
let cis = reverse beforeCIs <> [aroundCI] <> afterCIs
navInfo <- liftIO $ getNavInfo cis
pure (Chat (LocalChat nf) cis stats, Just navInfo)
pure (Chat cInfo cis stats, Just navInfo)
where
getNavInfo cis_ = case cis_ of
[] -> pure $ NavigationInfo 0 0
cis -> getLocalNavInfo_ db user nf (last cis)
getLocalChatInitial_ :: DB.Connection -> User -> NoteFolder -> Int -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatInitial_ db user nf count = do
getLocalChatInitial_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> Int -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo)
getLocalChatInitial_ db user nf contentFilter count = do
liftIO (getLocalMinUnreadId_ db user nf) >>= \case
Just minUnreadItemId -> do
unreadCount <- liftIO $ getLocalUnreadCount_ db user nf
let stats = emptyChatStats {unreadCount, minUnreadItemId}
getLocalChatAround' db user nf minUnreadItemId count "" stats
Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getLocalChatLast_ db user nf count ""
getLocalChatAround' db user nf contentFilter minUnreadItemId count "" stats
Nothing -> (,Just $ NavigationInfo 0 0) <$> getLocalChatLast_ db user nf contentFilter count ""
getLocalStats_ :: DB.Connection -> User -> NoteFolder -> IO ChatStats
getLocalStats_ db user nf = do
@@ -3243,15 +3207,6 @@ getAChatItem db vr user (ChatRef cType chatId scope) itemId = do
_ -> throwError $ SEChatItemNotFound itemId
liftIO $ getACIReactions db aci
getAChatItemBySharedMsgId :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> SharedMsgId -> ExceptT StoreError IO AChatItem
getAChatItemBySharedMsgId db user cd sharedMsgId = case cd of
CDDirectRcv ct@Contact {contactId} -> do
(CChatItem msgDir ci) <- getDirectChatItemBySharedMsgId db user contactId sharedMsgId
pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci
CDGroupRcv g scopeInfo GroupMember {groupMemberId} -> do
(CChatItem msgDir ci) <- getGroupChatItemBySharedMsgId db user g groupMemberId sharedMsgId
pure $ AChatItem SCTGroup msgDir (GroupChat g scopeInfo) ci
getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion]
getChatItemVersions db itemId = do
map toChatItemVersion
@@ -22,7 +22,9 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250922_remove_unused_connection
import Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync
import Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade
import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector
-- import Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2
import Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations
import Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables
import Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -44,8 +46,10 @@ schemaMigrations =
("20250922_remove_unused_connections", m20250922_remove_unused_connections, Just down_m20250922_remove_unused_connections),
("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync),
("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade),
("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector)
-- ("20251128_member_relations_vector_stage_2", m20251128_member_relations_vector_stage_2, Just down_m20251128_member_relations_vector_stage_2)
("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector),
("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations),
("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables),
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices)
]
-- | The list of migrations in ascending order by date
@@ -7,7 +7,7 @@ import qualified Data.Text as T
import Text.RawString.QQ (r)
-- This migration creates custom aggregate function migrate_relations_vector(idx, direction, intro_status).
-- Used in live migration and stage 2 migration (M20251128_member_relations_vector_stage_2).
-- Used in live migration and stage 2 migration (M20251128_migrate_member_relations).
--
-- Vector byte encoding: 4 reserved | 1 direction | 3 status
-- Direction: 0 = IDSubjectIntroduced, 1 = IDReferencedIntroduced
@@ -1,9 +1,9 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2 where
module Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations where
import Data.Text (Text)
import qualified Data.Text as T
import Text.RawString.QQ (r)
-- Build member_relations_vector for all members that don't have it yet.
@@ -13,11 +13,9 @@ import Text.RawString.QQ (r)
-- - direction 0 (IDSubjectIntroduced): current member (subject) is re_group_member_id, was introduced to referenced member
-- - direction 1 (IDReferencedIntroduced): current member (subject) is to_group_member_id, referenced member was introduced to it
-- TODO [relations vector] drop group_member_intros in the end of migration
m20251128_member_relations_vector_stage_2 :: Text
m20251128_member_relations_vector_stage_2 =
T.pack
[r|
m20251128_migrate_member_relations :: Text
m20251128_migrate_member_relations =
[r|
UPDATE group_members
SET member_relations_vector = (
SELECT migrate_relations_vector(idx, direction, intro_status)
@@ -34,12 +32,14 @@ SET member_relations_vector = (
) AS relations
)
WHERE member_relations_vector IS NULL;
DROP INDEX idx_pending_group_messages_group_member_intro_id;
ALTER TABLE pending_group_messages DROP COLUMN group_member_intro_id;
|]
-- TODO [relations vector] re-create group_member_intros
down_m20251128_member_relations_vector_stage_2 :: Text
down_m20251128_member_relations_vector_stage_2 =
T.pack
[r|
down_m20251128_migrate_member_relations :: Text
down_m20251128_migrate_member_relations =
[r|
ALTER TABLE pending_group_messages ADD COLUMN group_member_intro_id BIGINT REFERENCES group_member_intros ON DELETE CASCADE;
CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages(group_member_intro_id);
|]
@@ -0,0 +1,26 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables where
import Data.Text (Text)
import Text.RawString.QQ (r)
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251230_strict_tables (isValidText)
m20251230_strict_tables :: Text
m20251230_strict_tables =
isValidText
<> [r|
DELETE FROM calls
WHERE NOT simplex_is_valid_text(call_state);
ALTER TABLE calls ALTER COLUMN call_state TYPE TEXT USING call_state::TEXT;
DROP FUNCTION simplex_is_valid_text(BYTEA);
|]
down_m20251230_strict_tables :: Text
down_m20251230_strict_tables =
[r|
ALTER TABLE calls ALTER COLUMN call_state TYPE BYTEA USING call_state::BYTEA;
|]
@@ -0,0 +1,33 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260108_chat_indices :: Text
m20260108_chat_indices =
[r|
CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON chat_items(
user_id,
contact_id,
msg_content_tag,
created_at
);
CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items(
user_id,
note_folder_id,
msg_content_tag,
created_at
);
|]
down_m20260108_chat_indices :: Text
down_m20260108_chat_indices =
[r|
DROP INDEX idx_chat_items_contacts_msg_content_tag_created_at;
DROP INDEX idx_chat_items_note_folder_msg_content_tag_created_at;
|]
@@ -173,7 +173,7 @@ CREATE TABLE test_chat_schema.calls (
contact_id bigint NOT NULL,
shared_call_id bytea NOT NULL,
chat_item_id bigint NOT NULL,
call_state bytea NOT NULL,
call_state text NOT NULL,
call_ts timestamp with time zone NOT NULL,
user_id bigint NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
@@ -1033,7 +1033,6 @@ CREATE TABLE test_chat_schema.pending_group_messages (
pending_group_message_id bigint NOT NULL,
group_member_id bigint NOT NULL,
message_id bigint NOT NULL,
group_member_intro_id bigint,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
);
@@ -1814,6 +1813,10 @@ CREATE INDEX idx_chat_items_contacts_created_at ON test_chat_schema.chat_items U
CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON test_chat_schema.chat_items USING btree (user_id, contact_id, msg_content_tag, created_at);
CREATE UNIQUE INDEX idx_chat_items_direct_shared_msg_id ON test_chat_schema.chat_items USING btree (user_id, contact_id, shared_msg_id);
@@ -1898,6 +1901,10 @@ CREATE INDEX idx_chat_items_item_status ON test_chat_schema.chat_items USING btr
CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON test_chat_schema.chat_items USING btree (user_id, note_folder_id, msg_content_tag, created_at);
CREATE INDEX idx_chat_items_notes ON test_chat_schema.chat_items USING btree (user_id, note_folder_id, item_status, created_at);
@@ -2274,10 +2281,6 @@ CREATE INDEX idx_pending_group_messages_group_member_id ON test_chat_schema.pend
CREATE INDEX idx_pending_group_messages_group_member_intro_id ON test_chat_schema.pending_group_messages USING btree (group_member_intro_id);
CREATE INDEX idx_pending_group_messages_message_id ON test_chat_schema.pending_group_messages USING btree (message_id);
@@ -2939,11 +2942,6 @@ ALTER TABLE ONLY test_chat_schema.pending_group_messages
ALTER TABLE ONLY test_chat_schema.pending_group_messages
ADD CONSTRAINT pending_group_messages_group_member_intro_id_fkey FOREIGN KEY (group_member_intro_id) REFERENCES test_chat_schema.group_member_intros(group_member_intro_id) ON DELETE CASCADE;
ALTER TABLE ONLY test_chat_schema.pending_group_messages
ADD CONSTRAINT pending_group_messages_message_id_fkey FOREIGN KEY (message_id) REFERENCES test_chat_schema.messages(message_id) ON DELETE CASCADE;
+7 -3
View File
@@ -145,7 +145,9 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250922_remove_unused_connections
import Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync
import Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade
import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector
-- import Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2
import Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations
import Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables
import Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -290,8 +292,10 @@ schemaMigrations =
("20250922_remove_unused_connections", m20250922_remove_unused_connections, Just down_m20250922_remove_unused_connections),
("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync),
("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade),
("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector)
-- ("20251128_member_relations_vector_stage_2", m20251128_member_relations_vector_stage_2, Just down_m20251128_member_relations_vector_stage_2)
("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector),
("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations),
("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables),
("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices)
]
-- | The list of migrations in ascending order by date
@@ -14,7 +14,7 @@ UPDATE sqlite_master
SET sql = replace(sql, 'ON DELETE CASCADE', 'ON DELETE RESTRICT')
WHERE name = 'users' AND type = 'table';
PRAGMA writable_schema=0;
PRAGMA writable_schema=RESET;
|]
down_m20240226_users_restrict :: Query
@@ -26,5 +26,5 @@ UPDATE sqlite_master
SET sql = replace(sql, 'ON DELETE RESTRICT', 'ON DELETE CASCADE')
WHERE name = 'users' AND type = 'table';
PRAGMA writable_schema=0;
PRAGMA writable_schema=RESET;
|]
@@ -16,7 +16,7 @@ PRAGMA writable_schema=1;
UPDATE sqlite_master SET sql = replace(sql, 'INTEGER PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT')
WHERE name = 'chat_items' AND type = 'table';
PRAGMA writable_schema=0;
PRAGMA writable_schema=RESET;
|]
down_m20241023_chat_item_autoincrement_id :: Query
@@ -30,5 +30,5 @@ UPDATE sqlite_master
SET sql = replace(sql, 'INTEGER PRIMARY KEY AUTOINCREMENT', 'INTEGER PRIMARY KEY')
WHERE name = 'chat_items' AND type = 'table';
PRAGMA writable_schema=0;
PRAGMA writable_schema=RESET;
|]
@@ -22,7 +22,7 @@ SET sql = replace(
)
WHERE name = 'contact_requests' AND type = 'table';
PRAGMA writable_schema=0;
PRAGMA writable_schema=RESET;
|]
down_m20250702_contact_requests_remove_cascade_delete :: Query
@@ -42,5 +42,5 @@ SET sql = replace(
)
WHERE name = 'contact_requests' AND type = 'table';
PRAGMA writable_schema=0;
PRAGMA writable_schema=RESET;
|]
@@ -14,7 +14,7 @@ UPDATE sqlite_master
SET sql = replace(sql, 'user_id INTEGER REFERENCES users', 'user_id INTEGER REFERENCES users ON DELETE CASCADE')
WHERE name = 'chat_tags' AND type = 'table';
PRAGMA writable_schema=0;
PRAGMA writable_schema=RESET;
|]
down_m20251017_chat_tags_cascade :: Query
@@ -26,5 +26,5 @@ UPDATE sqlite_master
SET sql = replace(sql, 'user_id INTEGER REFERENCES users ON DELETE CASCADE', 'user_id INTEGER REFERENCES users')
WHERE name = 'chat_tags' AND type = 'table';
PRAGMA writable_schema=0;
PRAGMA writable_schema=RESET;
|]
@@ -15,7 +15,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Util (SQLiteFunc, SQLiteFuncFinal, m
-- This module defines custom aggregate function migrate_relations_vector(idx, direction, intro_status).
-- It is passed via DBOpts and registered on DB open.
-- Used in live migration and stage 2 migration (M20251128_member_relations_vector_stage_2).
-- Used in live migration and stage 2 migration (M20251128_migrate_member_relations).
--
-- Vector byte encoding: 4 reserved | 1 direction | 3 status
-- Direction: 0 = IDSubjectIntroduced, 1 = IDReferencedIntroduced
@@ -1,6 +1,6 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2 where
module Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
@@ -12,9 +12,8 @@ import Database.SQLite.Simple.QQ (sql)
-- - direction 0 (IDSubjectIntroduced): current member (subject) is re_group_member_id, was introduced to referenced member
-- - direction 1 (IDReferencedIntroduced): current member (subject) is to_group_member_id, referenced member was introduced to it
-- TODO [relations vector] drop group_member_intros in the end of migration
m20251128_member_relations_vector_stage_2 :: Query
m20251128_member_relations_vector_stage_2 =
m20251128_migrate_member_relations :: Query
m20251128_migrate_member_relations =
[sql|
UPDATE group_members
SET member_relations_vector = (
@@ -32,11 +31,14 @@ SET member_relations_vector = (
)
)
WHERE member_relations_vector IS NULL;
DROP INDEX idx_pending_group_messages_group_member_intro_id;
ALTER TABLE pending_group_messages DROP COLUMN group_member_intro_id;
|]
-- TODO [relations vector] re-create group_member_intros
down_m20251128_member_relations_vector_stage_2 :: Query
down_m20251128_member_relations_vector_stage_2 =
down_m20251128_migrate_member_relations :: Query
down_m20251128_migrate_member_relations =
[sql|
ALTER TABLE pending_group_messages ADD COLUMN group_member_intro_id INTEGER REFERENCES group_member_intros ON DELETE CASCADE;
CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages(group_member_intro_id);
|]
@@ -0,0 +1,70 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20251230_strict_tables :: Query
m20251230_strict_tables =
[sql|
UPDATE group_members
SET member_role = CAST(member_role as TEXT),
member_restriction = CAST(member_restriction as TEXT);
UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role as TEXT);
UPDATE app_settings SET app_settings = CAST(app_settings as TEXT);
PRAGMA writable_schema=1;
UPDATE sqlite_master
SET sql = replace(sql, 'call_state BLOB NOT NULL', 'call_state TEXT NOT NULL')
WHERE type = 'table' AND name = 'calls';
UPDATE sqlite_master
SET sql = replace(sql, 'local_alias DEFAULT', 'local_alias TEXT DEFAULT')
WHERE type = 'table' AND name = 'connections';
UPDATE sqlite_master
SET sql = CASE
WHEN LOWER(SUBSTR(sql, -15)) = ') without rowid' THEN sql || ', STRICT'
WHEN SUBSTR(sql, -1) = ')' THEN sql || ' STRICT'
ELSE sql
END
WHERE type = 'table' AND name != 'sqlite_sequence';
PRAGMA writable_schema=RESET;
|]
down_m20251230_strict_tables :: Query
down_m20251230_strict_tables =
[sql|
PRAGMA writable_schema=1;
UPDATE sqlite_master
SET sql = CASE
WHEN LOWER(SUBSTR(sql, -8)) = ', strict' THEN SUBSTR(sql, 1, LENGTH(sql) - 8)
WHEN LOWER(SUBSTR(sql, -7)) = ' strict' THEN SUBSTR(sql, 1, LENGTH(sql) - 7)
ELSE sql
END
WHERE type = 'table' AND name != 'sqlite_sequence';
UPDATE sqlite_master
SET sql = replace(sql, 'call_state TEXT NOT NULL', 'call_state BLOB NOT NULL')
WHERE type = 'table' AND name = 'calls';
UPDATE sqlite_master
SET sql = replace(sql, 'local_alias TEXT DEFAULT', 'local_alias DEFAULT')
WHERE type = 'table' AND name = 'connections';
PRAGMA writable_schema=RESET;
UPDATE group_members
SET member_role = CAST(member_role as BLOB),
member_restriction = CAST(member_restriction as BLOB);
UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role as BLOB);
UPDATE app_settings SET app_settings = CAST(app_settings as BLOB);
|]
@@ -0,0 +1,32 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260108_chat_indices :: Query
m20260108_chat_indices =
[sql|
CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON chat_items(
user_id,
contact_id,
msg_content_tag,
created_at
);
CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items(
user_id,
note_folder_id,
msg_content_tag,
created_at
);
|]
down_m20260108_chat_indices :: Query
down_m20260108_chat_indices =
[sql|
DROP INDEX idx_chat_items_contacts_msg_content_tag_created_at;
DROP INDEX idx_chat_items_note_folder_msg_content_tag_created_at;
|]
@@ -59,6 +59,16 @@ Query:
Plan:
SEARCH commands USING INDEX idx_commands_server_commands (host=? AND port=?)
Query:
SELECT rcpt_status, snd_message_body_id FROM snd_messages
WHERE NOT EXISTS (SELECT 1 FROM snd_message_deliveries WHERE conn_id = ? AND internal_id = ? AND failed = 0)
AND conn_id = ? AND internal_id = ?
Plan:
SEARCH snd_messages USING PRIMARY KEY (conn_id=?)
SCALAR SUBQUERY 1
SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=?)
Query:
SELECT rcv_file_chunk_id, chunk_no, chunk_size, digest, tmp_path
FROM rcv_file_chunks
@@ -69,6 +79,14 @@ Plan:
SEARCH rcv_file_chunks USING INDEX idx_rcv_file_chunks_rcv_file_id (rcv_file_id=?)
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, deleted, redirect_id, redirect_entity_id, redirect_size, redirect_digest
FROM rcv_files
WHERE rcv_file_id = ?
Plan:
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT snd_file_chunk_id, chunk_no, chunk_offset, chunk_size, digest
FROM snd_file_chunks
@@ -77,6 +95,14 @@ Query:
Plan:
SEARCH snd_file_chunks USING INDEX idx_snd_file_chunks_snd_file_id (snd_file_id=?)
Query:
SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted, redirect_size, redirect_digest
FROM snd_files
WHERE snd_file_id = ?
Plan:
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)
Query:
DELETE FROM snd_message_bodies
WHERE NOT EXISTS (SELECT 1 FROM snd_messages WHERE snd_message_body_id = ?)
@@ -201,24 +227,6 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
SEARCH f USING INTEGER PRIMARY KEY (rowid=?)
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT rcpt_status, snd_message_body_id FROM snd_messages
WHERE NOT EXISTS (SELECT 1 FROM snd_message_deliveries WHERE conn_id = ? AND internal_id = ? AND failed = 0)
AND conn_id = ? AND internal_id = ?
Plan:
SEARCH snd_messages USING PRIMARY KEY (conn_id=?)
SCALAR SUBQUERY 1
SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=?)
Query:
SELECT rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, deleted, redirect_id, redirect_entity_id, redirect_size, redirect_digest
FROM rcv_files
WHERE rcv_file_id = ?
Plan:
SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT rcv_file_id
FROM rcv_files
@@ -230,14 +238,6 @@ Plan:
SEARCH rcv_files USING INDEX idx_rcv_files_status_created_at (status=? AND created_at>?)
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted, redirect_size, redirect_digest
FROM snd_files
WHERE snd_file_id = ?
Plan:
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT snd_file_id
FROM snd_files
@@ -257,6 +257,22 @@ Query:
Plan:
SEARCH messages USING PRIMARY KEY (conn_id=?)
Query:
SELECT last_internal_msg_id, last_internal_rcv_msg_id, last_external_snd_msg_id, last_rcv_msg_hash
FROM connections
WHERE conn_id = ?
Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?)
Query:
SELECT last_internal_msg_id, last_internal_snd_msg_id, last_snd_msg_hash
FROM connections
WHERE conn_id = ?
Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?)
Query:
SELECT user_id FROM users u
WHERE u.user_id = ?
@@ -268,6 +284,15 @@ SEARCH u USING INTEGER PRIMARY KEY (rowid=?)
CORRELATED SCALAR SUBQUERY 1
SEARCH c USING COVERING INDEX idx_connections_user (user_id=?)
Query:
SELECT user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs,
last_external_snd_msg_id, deleted, ratchet_sync_state, pq_support
FROM connections
WHERE conn_id = ? AND deleted = ?
Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?)
Query:
INSERT INTO conn_confirmations
(confirmation_id, conn_id, sender_key, e2e_snd_pub_key, ratchet_state, sender_conn_info, smp_reply_queues, smp_client_version, accepted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0);
@@ -287,6 +312,18 @@ Query:
Plan:
Query:
INSERT INTO xftp_servers (xftp_host, xftp_port, xftp_key_hash)
VALUES (?, ?, ?)
ON CONFLICT (xftp_host, xftp_port, xftp_key_hash)
DO UPDATE SET xftp_host = EXCLUDED.xftp_host
RETURNING xftp_server_id
Plan:
SEARCH deleted_snd_chunk_replicas USING COVERING INDEX idx_deleted_snd_chunk_replicas_xftp_server_id (xftp_server_id=?)
SEARCH snd_file_chunk_replicas USING COVERING INDEX idx_snd_file_chunk_replicas_xftp_server_id (xftp_server_id=?)
SEARCH rcv_file_chunk_replicas USING COVERING INDEX idx_rcv_file_chunk_replicas_xftp_server_id (xftp_server_id=?)
Query:
SELECT
r.internal_id, m.internal_ts, r.broker_id, r.broker_ts, r.external_snd_id, r.integrity, r.internal_hash,
@@ -445,22 +482,6 @@ Query:
Plan:
SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?)
Query:
SELECT last_internal_msg_id, last_internal_rcv_msg_id, last_external_snd_msg_id, last_rcv_msg_hash
FROM connections
WHERE conn_id = ?
Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?)
Query:
SELECT last_internal_msg_id, last_internal_snd_msg_id, last_snd_msg_hash
FROM connections
WHERE conn_id = ?
Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?)
Query:
SELECT link_id, snd_private_key
FROM inv_short_links
@@ -497,15 +518,6 @@ Plan:
SEARCH s USING PRIMARY KEY (conn_id=? AND internal_snd_id=?)
SEARCH m USING PRIMARY KEY (conn_id=? AND internal_id=?)
Query:
SELECT user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs,
last_external_snd_msg_id, deleted, ratchet_sync_state, pq_support
FROM connections
WHERE conn_id = ? AND deleted = ?
Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?)
Query:
DELETE FROM conn_confirmations
WHERE conn_id = ?
@@ -1027,8 +1039,13 @@ Plan:
Query: INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size, approved_relays) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan:
Query: INSERT INTO servers (host, port, key_hash) VALUES (?,?,?)
Query: INSERT INTO servers (host, port, key_hash) VALUES (?,?,?) ON CONFLICT (host, port) DO NOTHING RETURNING 1
Plan:
SEARCH inv_short_links USING COVERING INDEX idx_inv_short_links_link_id (host=? AND port=?)
SEARCH commands USING COVERING INDEX idx_commands_server_commands (host=? AND port=?)
SEARCH ntf_subscriptions USING COVERING INDEX idx_ntf_subscriptions_smp_host_smp_port (smp_host=? AND smp_port=?)
SEARCH snd_queues USING COVERING INDEX idx_snd_queues_host_port (host=? AND port=?)
SEARCH rcv_queues USING COVERING INDEX idx_rcv_queues_link_id (host=? AND port=?)
Query: INSERT INTO skipped_messages (conn_id, header_key, msg_n, msg_key) VALUES (?, ?, ?, ?)
Plan:
@@ -1052,9 +1069,6 @@ Plan:
Query: INSERT INTO users DEFAULT VALUES
Plan:
Query: INSERT INTO xftp_servers (xftp_host, xftp_port, xftp_key_hash) VALUES (?,?,?)
Plan:
Query: SELECT 1 FROM encrypted_rcv_message_hashes WHERE conn_id = ? AND hash = ? LIMIT 1
Plan:
SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=? AND hash=?)
@@ -1159,10 +1173,6 @@ Query: SELECT x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem FROM ratchets WHERE
Plan:
SEARCH ratchets USING PRIMARY KEY (conn_id=?)
Query: SELECT xftp_server_id FROM xftp_servers WHERE xftp_host = ? AND xftp_port = ? AND xftp_key_hash = ?
Plan:
SEARCH xftp_servers USING COVERING INDEX sqlite_autoindex_xftp_servers_1 (xftp_host=? AND xftp_port=? AND xftp_key_hash=?)
Query: UPDATE connections SET deleted = ? WHERE conn_id = ?
Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?)
@@ -1255,10 +1265,6 @@ Query: UPDATE snd_file_chunk_replicas SET replica_status = ?, updated_at = ? WHE
Plan:
SEARCH snd_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE snd_files SET deleted = 1, updated_at = ? WHERE snd_file_id = ?
Plan:
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE snd_files SET prefix_path = NULL, status = ?, updated_at = ? WHERE snd_file_id = ?
Plan:
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)
@@ -736,6 +736,26 @@ Query:
Plan:
SEARCH messages USING INDEX idx_messages_group_id_shared_msg_id (group_id=? AND shared_msg_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ?
ORDER BY created_at DESC, chat_item_id DESC
LIMIT 1
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND group_id = ? AND group_member_id = ?
ORDER BY item_ts DESC, chat_item_id DESC
LIMIT 1
Plan:
SEARCH chat_items USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id
FROM chat_items
@@ -745,7 +765,7 @@ Query:
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?)
SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
USE TEMP B-TREE FOR ORDER BY
Query:
@@ -756,7 +776,7 @@ Query:
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?)
SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
USE TEMP B-TREE FOR ORDER BY
Query:
@@ -1225,26 +1245,6 @@ Plan:
SEARCH c USING INDEX idx_connections_contact_id (contact_id=?)
SEARCH ct USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ?
ORDER BY created_at DESC, chat_item_id DESC
LIMIT 1
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND group_id = ? AND group_member_id = ?
ORDER BY item_ts DESC, chat_item_id DESC
LIMIT 1
Plan:
SEARCH chat_items USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?)
Query:
SELECT chat_item_id
FROM chat_items
@@ -1292,7 +1292,7 @@ Query:
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?)
SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
USE TEMP B-TREE FOR ORDER BY
Query:
@@ -3131,38 +3131,6 @@ SEARCH chat_item_messages USING INDEX sqlite_autoindex_chat_item_messages_1 (mes
LIST SUBQUERY 1
SEARCH msg_deliveries USING INDEX idx_msg_deliveries_agent_msg_id (connection_id=? AND agent_msg_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (created_at < ? OR (created_at = ? AND chat_item_id < ?))
ORDER BY created_at DESC, chat_item_id DESC
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (created_at > ? OR (created_at = ? AND chat_item_id > ?))
ORDER BY created_at ASC, chat_item_id ASC
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
ORDER BY created_at DESC, chat_item_id DESC
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
Query:
SELECT chat_item_id
FROM chat_items
@@ -3213,38 +3181,6 @@ Query:
Plan:
SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (created_at < ? OR (created_at = ? AND chat_item_id < ?))
ORDER BY created_at DESC, chat_item_id DESC
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
AND (created_at > ? OR (created_at = ? AND chat_item_id > ?))
ORDER BY created_at ASC, chat_item_id ASC
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
Query:
SELECT chat_item_id
FROM chat_items
WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%'
ORDER BY created_at DESC, chat_item_id DESC
LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
Query:
SELECT chat_item_id
FROM chat_items
@@ -3427,14 +3363,6 @@ Query:
Plan:
SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_group (group_id=? AND shared_msg_id=?)
Query:
SELECT group_member_intro_id, intro_status
FROM group_member_intros
WHERE re_group_member_id = ? AND to_group_member_id = ?
Plan:
SEARCH group_member_intros USING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?)
Query:
SELECT group_scope_tag, group_scope_group_member_id
FROM chat_items
@@ -3484,7 +3412,7 @@ SEARCH m USING INDEX idx_group_members_user_id (user_id=?)
SEARCH p USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT pgm.message_id, m.shared_msg_id, m.msg_body, m.chat_msg_event, pgm.group_member_intro_id
SELECT pgm.message_id, m.shared_msg_id, m.msg_body
FROM pending_group_messages pgm
JOIN messages m USING (message_id)
WHERE pgm.group_member_id = ?
@@ -3715,52 +3643,6 @@ SEARCH connections USING INDEX idx_connections_group_member_id (group_member_id=
LIST SUBQUERY 1
SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name
Query:
UPDATE group_member_intros SET intro_status='fwd'
WHERE re_group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?)
AND to_group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?)
Plan:
SEARCH group_member_intros USING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?)
LIST SUBQUERY 1
SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name
LIST SUBQUERY 2
SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name
Query:
UPDATE group_members
SET
member_relations_vector = (
SELECT migrate_relations_vector(idx, direction, intro_status)
FROM (
SELECT m.index_in_group AS idx, 0 AS direction, i.intro_status
FROM group_member_intros i
JOIN group_members m ON m.group_member_id = i.to_group_member_id
WHERE i.re_group_member_id = group_members.group_member_id
UNION ALL
SELECT m.index_in_group AS idx, 1 AS direction, i.intro_status
FROM group_member_intros i
JOIN group_members m ON m.group_member_id = i.re_group_member_id
WHERE i.to_group_member_id = group_members.group_member_id
) AS relations
),
updated_at = ?
WHERE group_member_id = ?
AND member_relations_vector IS NULL
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
CORRELATED SCALAR SUBQUERY 3
CO-ROUTINE relations
COMPOUND QUERY
LEFT-MOST SUBQUERY
SEARCH i USING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?)
SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
UNION ALL
SEARCH i USING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?)
SEARCH m USING INTEGER PRIMARY KEY (rowid=?)
SCAN relations
Query:
UPDATE group_members
SET contact_id = ?, local_display_name = ?, contact_profile_id = ?, updated_at = ?
@@ -4432,7 +4314,7 @@ Plan:
Query:
INSERT INTO pending_group_messages
(group_member_id, message_id, group_member_intro_id, created_at, updated_at) VALUES (?,?,?,?,?)
(group_member_id, message_id, created_at, updated_at) VALUES (?,?,?,?)
Plan:
@@ -5259,7 +5141,7 @@ Query:
JOIN files f ON f.chat_item_id = i.chat_item_id
WHERE i.user_id = ?
Plan:
SEARCH i USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=?)
SEARCH i USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?)
Query:
@@ -5529,7 +5411,25 @@ Query:
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts < ? ) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id < ? )) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ?
Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND contact_id = ? AND created_at < ?) OR ( user_id = ? AND contact_id = ? AND created_at = ? AND chat_item_id < ?)) ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
Plan:
MULTI-INDEX OR
INDEX 1
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at<?)
INDEX 2
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at=? AND rowid<?)
USE TEMP B-TREE FOR ORDER BY
Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND contact_id = ? AND created_at > ?) OR ( user_id = ? AND contact_id = ? AND created_at = ? AND chat_item_id > ?)) ORDER BY created_at ASC, chat_item_id ASC LIMIT ?
Plan:
MULTI-INDEX OR
INDEX 1
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at>?)
INDEX 2
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at=? AND rowid>?)
USE TEMP B-TREE FOR ORDER BY
Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts < ?) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id < ?)) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ?
Plan:
MULTI-INDEX OR
INDEX 1
@@ -5538,7 +5438,7 @@ INDEX 2
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_ts=? AND rowid<?)
USE TEMP B-TREE FOR ORDER BY
Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts > ? ) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id > ? )) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ?
Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts > ?) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id > ?)) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ?
Plan:
MULTI-INDEX OR
INDEX 1
@@ -5547,11 +5447,37 @@ INDEX 2
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_ts=? AND rowid>?)
USE TEMP B-TREE FOR ORDER BY
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ?
Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND note_folder_id = ? AND created_at < ?) OR ( user_id = ? AND note_folder_id = ? AND created_at = ? AND chat_item_id < ?)) ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?)
MULTI-INDEX OR
INDEX 1
SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at<?)
INDEX 2
SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at=? AND rowid<?)
USE TEMP B-TREE FOR ORDER BY
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id IS NULL ORDER BY item_ts DESC, chat_item_id DESC LIMIT ?
Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND note_folder_id = ? AND created_at > ?) OR ( user_id = ? AND note_folder_id = ? AND created_at = ? AND chat_item_id > ?)) ORDER BY created_at ASC, chat_item_id ASC LIMIT ?
Plan:
MULTI-INDEX OR
INDEX 1
SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at>?)
INDEX 2
SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at=? AND rowid>?)
USE TEMP B-TREE FOR ORDER BY
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND msg_content_tag = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_msg_content_tag_created_at (user_id=? AND contact_id=? AND msg_content_tag=?)
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ?
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?)
@@ -5567,6 +5493,18 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ?
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_item_ts (user_id=? AND group_id=? AND msg_content_tag=?)
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND msg_content_tag = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=? AND note_folder_id=? AND msg_content_tag=?)
Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ?
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?)
Query: CREATE TABLE temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT)
Error: SQLite3 returned ErrorError while attempting to perform prepare "explain query plan CREATE TABLE temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT)": table temp_delete_members already exists
@@ -5965,7 +5903,7 @@ SEARCH protocol_servers USING COVERING INDEX idx_smp_servers_user_id (user_id=?)
SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?)
SEARCH commands USING COVERING INDEX idx_commands_user_id (user_id=?)
SEARCH calls USING COVERING INDEX idx_calls_user_id (user_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=?)
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?)
SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_2 (user_id=?)
SEARCH user_contact_links USING COVERING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
SEARCH connections USING COVERING INDEX idx_connections_to_subscribe (user_id=?)
@@ -6089,10 +6027,6 @@ Plan:
Query: INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?)
Plan:
Query: SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query: SELECT 1 FROM settings WHERE user_id = ? LIMIT 1
Plan:
SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?)
@@ -6117,18 +6051,24 @@ Query: SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE use
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_status=?)
Query: SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND contact_id = ? AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_msg_content_tag_created_at (user_id=? AND contact_id=? AND msg_content_tag>?)
Query: SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag
Plan:
SEARCH chat_items USING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id=? AND msg_content_tag>?)
Query: SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag
Plan:
SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=? AND note_folder_id=? AND msg_content_tag>?)
Query: SELECT EXISTS (SELECT 1 FROM chat_items WHERE user_id = ? AND contact_id = ?)
Plan:
SCAN CONSTANT ROW
SCALAR SUBQUERY 1
SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?)
Query: SELECT EXISTS (SELECT 1 FROM group_members WHERE member_relations_vector IS NULL LIMIT 1)
Plan:
SCAN CONSTANT ROW
SCALAR SUBQUERY 1
SCAN group_members
Query: SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit = ?
Plan:
SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_conditions_commit (conditions_commit=? AND server_operator_id=?)
@@ -6509,14 +6449,6 @@ Query: UPDATE files SET private_snd_file_descr = ?, updated_at = ? WHERE user_id
Plan:
SEARCH files USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE group_member_intros SET intro_status='con'
Plan:
SCAN group_member_intros
Query: UPDATE group_member_intros SET intro_status='fwd'
Plan:
SCAN group_member_intros
Query: UPDATE group_members SET contact_id = ?, updated_at = ? WHERE contact_profile_id = ?
Plan:
SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?)
@@ -2,7 +2,7 @@ CREATE TABLE migrations(
name TEXT NOT NULL PRIMARY KEY,
ts TEXT NOT NULL,
down TEXT
);
) STRICT;
CREATE TABLE contact_profiles(
-- remote user profile
contact_profile_id INTEGER PRIMARY KEY,
@@ -20,7 +20,7 @@ CREATE TABLE contact_profiles(
contact_link BLOB,
short_descr TEXT,
chat_peer_type TEXT
);
) STRICT;
CREATE TABLE users(
user_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE RESTRICT
@@ -44,7 +44,7 @@ CREATE TABLE users(
ON DELETE RESTRICT
ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED
);
) STRICT;
CREATE TABLE display_names(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
local_display_name TEXT NOT NULL,
@@ -54,7 +54,7 @@ CREATE TABLE display_names(
updated_at TEXT CHECK(updated_at NOT NULL),
PRIMARY KEY(user_id, local_display_name) ON CONFLICT FAIL,
UNIQUE(user_id, ldn_base, ldn_suffix) ON CONFLICT FAIL
) WITHOUT ROWID;
) WITHOUT ROWID, STRICT;
CREATE TABLE contacts(
contact_id INTEGER PRIMARY KEY,
contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL,
@@ -96,7 +96,7 @@ CREATE TABLE contacts(
ON UPDATE CASCADE,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, contact_profile_id)
);
) STRICT;
CREATE TABLE known_servers(
server_id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
@@ -106,7 +106,7 @@ CREATE TABLE known_servers(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
UNIQUE(user_id, host, port)
) WITHOUT ROWID;
) WITHOUT ROWID, STRICT;
CREATE TABLE group_profiles(
-- shared group profiles
group_profile_id INTEGER PRIMARY KEY,
@@ -122,7 +122,7 @@ CREATE TABLE group_profiles(
description TEXT NULL,
member_admission TEXT,
short_descr TEXT
);
) STRICT;
CREATE TABLE groups(
group_id INTEGER PRIMARY KEY, -- local group ID
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
@@ -163,7 +163,7 @@ CREATE TABLE groups(
ON UPDATE CASCADE,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, group_profile_id)
);
) STRICT;
CREATE TABLE group_members(
-- group members, excluding the local user
group_member_id INTEGER PRIMARY KEY,
@@ -203,7 +203,7 @@ CREATE TABLE group_members(
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE(group_id, member_id)
);
) STRICT;
CREATE TABLE group_member_intros(
group_member_intro_id INTEGER PRIMARY KEY,
re_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE,
@@ -215,7 +215,7 @@ CREATE TABLE group_member_intros(
updated_at TEXT CHECK(updated_at NOT NULL),
intro_chat_protocol_version INTEGER NOT NULL DEFAULT 3, -- see GroupMemberIntroStatus
UNIQUE(re_group_member_id, to_group_member_id)
);
) STRICT;
CREATE TABLE files(
file_id INTEGER PRIMARY KEY,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
@@ -240,7 +240,7 @@ CREATE TABLE files(
file_crypto_nonce BLOB,
note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE,
redirect_file_id INTEGER REFERENCES files ON DELETE CASCADE
);
) STRICT;
CREATE TABLE snd_files(
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE,
@@ -253,7 +253,7 @@ CREATE TABLE snd_files(
file_descr_id INTEGER NULL
REFERENCES xftp_file_descriptions ON DELETE SET NULL,
PRIMARY KEY(file_id, connection_id)
) WITHOUT ROWID;
) WITHOUT ROWID, STRICT;
CREATE TABLE rcv_files(
file_id INTEGER PRIMARY KEY REFERENCES files ON DELETE CASCADE,
file_status TEXT NOT NULL, -- new, accepted, connected, completed
@@ -270,7 +270,7 @@ CREATE TABLE rcv_files(
agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL),
to_receive INTEGER,
user_approved_relays INTEGER NOT NULL DEFAULT 0
);
) STRICT;
CREATE TABLE rcv_file_chunks(
file_id INTEGER NOT NULL REFERENCES rcv_files ON DELETE CASCADE,
chunk_number INTEGER NOT NULL,
@@ -279,7 +279,7 @@ CREATE TABLE rcv_file_chunks(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL), -- 0(received), 1(appended to file)
PRIMARY KEY(file_id, chunk_number)
) WITHOUT ROWID;
) WITHOUT ROWID, STRICT;
CREATE TABLE connections(
-- all SMP agent connections
connection_id INTEGER PRIMARY KEY,
@@ -302,7 +302,7 @@ CREATE TABLE connections(
REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL,
custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL,
conn_req_inv BLOB,
local_alias DEFAULT '' CHECK(local_alias NOT NULL),
local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL),
via_group_link INTEGER DEFAULT 0 CHECK(via_group_link NOT NULL),
group_link_id BLOB,
security_code TEXT NULL,
@@ -325,7 +325,7 @@ CREATE TABLE connections(
REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED
);
) STRICT;
CREATE TABLE user_contact_links(
user_contact_link_id INTEGER PRIMARY KEY,
conn_req_contact BLOB NOT NULL,
@@ -344,7 +344,7 @@ CREATE TABLE user_contact_links(
short_link_data_set INTEGER NOT NULL DEFAULT 0,
short_link_large_data_set INTEGER NOT NULL DEFAULT 0,
UNIQUE(user_id, local_display_name)
);
) STRICT;
CREATE TABLE contact_requests(
contact_request_id INTEGER PRIMARY KEY,
user_contact_link_id INTEGER REFERENCES user_contact_links
@@ -372,7 +372,7 @@ CREATE TABLE contact_requests(
DEFERRABLE INITIALLY DEFERRED,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, contact_profile_id)
);
) STRICT;
CREATE TABLE messages(
message_id INTEGER PRIMARY KEY,
msg_sent INTEGER NOT NULL, -- 0 for received, 1 for sent
@@ -388,15 +388,14 @@ CREATE TABLE messages(
author_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL,
forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL,
broker_ts TEXT
);
) STRICT;
CREATE TABLE pending_group_messages(
pending_group_message_id INTEGER PRIMARY KEY,
group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE,
message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE,
group_member_intro_id INTEGER REFERENCES group_member_intros ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE chat_items(
chat_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
@@ -442,7 +441,7 @@ CREATE TABLE chat_items(
group_scope_tag TEXT,
group_scope_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE,
show_group_as_sender INTEGER NOT NULL DEFAULT 0
);
) STRICT;
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE chat_item_messages(
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
@@ -450,21 +449,21 @@ CREATE TABLE chat_item_messages(
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
UNIQUE(chat_item_id, message_id)
);
) STRICT;
CREATE TABLE calls(
-- stores call invitations state for communicating state between NSE and app when call notification comes
call_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE,
shared_call_id BLOB NOT NULL,
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
call_state BLOB NOT NULL,
call_state TEXT NOT NULL,
call_ts TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
call_uuid TEXT NOT NULL DEFAULT ""
);
) STRICT;
CREATE TABLE commands(
command_id INTEGER PRIMARY KEY AUTOINCREMENT, -- used as ACorrId
connection_id INTEGER REFERENCES connections ON DELETE CASCADE,
@@ -473,14 +472,14 @@ CREATE TABLE commands(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE settings(
settings_id INTEGER PRIMARY KEY,
chat_item_ttl INTEGER,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE IF NOT EXISTS "protocol_servers"(
smp_server_id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
@@ -495,7 +494,7 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"(
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
protocol TEXT NOT NULL DEFAULT 'smp',
UNIQUE(user_id, host, port)
);
) STRICT;
CREATE TABLE xftp_file_descriptions(
file_descr_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
@@ -504,7 +503,7 @@ CREATE TABLE xftp_file_descriptions(
file_descr_complete INTEGER NOT NULL DEFAULT(0),
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE extra_xftp_file_descriptions(
extra_file_descr_id INTEGER PRIMARY KEY,
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
@@ -512,7 +511,7 @@ CREATE TABLE extra_xftp_file_descriptions(
file_descr_text TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE chat_item_versions(
-- contains versions only for edited chat items, including current version
chat_item_version_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -521,7 +520,7 @@ CREATE TABLE chat_item_versions(
item_version_ts TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE chat_item_reactions(
chat_item_reaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_member_id BLOB, -- member that created item, NULL for items in direct chats
@@ -535,7 +534,7 @@ CREATE TABLE chat_item_reactions(
reaction_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE chat_item_moderations(
chat_item_moderation_id INTEGER PRIMARY KEY,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
@@ -546,7 +545,7 @@ CREATE TABLE chat_item_moderations(
moderated_at TEXT NOT NULL, -- broker_ts of creating message
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE group_snd_item_statuses(
group_snd_item_status_id INTEGER PRIMARY KEY,
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
@@ -556,7 +555,7 @@ CREATE TABLE group_snd_item_statuses(
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
via_proxy INTEGER
);
) STRICT;
CREATE TABLE IF NOT EXISTS "sent_probes"(
sent_probe_id INTEGER PRIMARY KEY,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
@@ -566,7 +565,7 @@ CREATE TABLE IF NOT EXISTS "sent_probes"(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
UNIQUE(user_id, probe)
);
) STRICT;
CREATE TABLE IF NOT EXISTS "sent_probe_hashes"(
sent_probe_hash_id INTEGER PRIMARY KEY,
sent_probe_id INTEGER NOT NULL REFERENCES "sent_probes" ON DELETE CASCADE,
@@ -575,7 +574,7 @@ CREATE TABLE IF NOT EXISTS "sent_probe_hashes"(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL)
);
) STRICT;
CREATE TABLE IF NOT EXISTS "received_probes"(
received_probe_id INTEGER PRIMARY KEY,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
@@ -585,7 +584,7 @@ CREATE TABLE IF NOT EXISTS "received_probes"(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL)
);
) STRICT;
CREATE TABLE remote_hosts(
-- e.g., mobiles known to a desktop app
remote_host_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -600,7 +599,7 @@ CREATE TABLE remote_hosts(
bind_addr TEXT,
bind_iface TEXT,
bind_port INTEGER
);
) STRICT;
CREATE TABLE remote_controllers(
-- e.g., desktops known to a mobile app
remote_ctrl_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -611,7 +610,7 @@ CREATE TABLE remote_controllers(
id_pub BLOB NOT NULL, -- remote controller long-term/identity key to verify signatures
dh_priv_key BLOB NOT NULL, -- last session DH key
prev_dh_priv_key BLOB -- previous session DH key
);
) STRICT;
CREATE TABLE IF NOT EXISTS "msg_deliveries"(
msg_delivery_id INTEGER PRIMARY KEY,
message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, -- non UNIQUE for group messages and for batched messages
@@ -622,7 +621,7 @@ CREATE TABLE IF NOT EXISTS "msg_deliveries"(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
delivery_status TEXT -- MsgDeliveryStatus
);
) STRICT;
CREATE TABLE note_folders(
note_folder_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
@@ -631,8 +630,8 @@ CREATE TABLE note_folders(
chat_ts TEXT NOT NULL DEFAULT(datetime('now')),
favorite INTEGER NOT NULL DEFAULT 0,
unread_chat INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE app_settings(app_settings TEXT NOT NULL);
) STRICT;
CREATE TABLE app_settings(app_settings TEXT NOT NULL) STRICT;
CREATE TABLE server_operators(
server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT,
server_operator_tag TEXT,
@@ -646,14 +645,14 @@ CREATE TABLE server_operators(
xftp_role_proxy INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE usage_conditions(
usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
conditions_commit TEXT NOT NULL UNIQUE,
notified_at TEXT,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE operator_usage_conditions(
operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
server_operator_id INTEGER REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE,
@@ -663,26 +662,26 @@ CREATE TABLE operator_usage_conditions(
created_at TEXT NOT NULL DEFAULT(datetime('now'))
,
auto_accepted INTEGER DEFAULT 0
);
) STRICT;
CREATE TABLE chat_tags(
chat_tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users ON DELETE CASCADE,
chat_tag_text TEXT NOT NULL,
chat_tag_emoji TEXT,
tag_order INTEGER NOT NULL
);
) STRICT;
CREATE TABLE chat_tags_chats(
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
chat_tag_id INTEGER NOT NULL REFERENCES chat_tags ON DELETE CASCADE
);
) STRICT;
CREATE TABLE chat_item_mentions(
chat_item_mention_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
member_id BLOB NOT NULL,
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
display_name TEXT NOT NULL
);
) STRICT;
CREATE TABLE delivery_tasks(
delivery_task_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
@@ -698,7 +697,7 @@ CREATE TABLE delivery_tasks(
failed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE delivery_jobs(
delivery_job_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
@@ -714,16 +713,16 @@ CREATE TABLE delivery_jobs(
failed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE group_member_status_predicates(
member_status TEXT NOT NULL PRIMARY KEY,
current_member INTEGER NOT NULL DEFAULT 0
);
) STRICT;
CREATE TABLE connections_sync(
connections_sync_id INTEGER PRIMARY KEY AUTOINCREMENT,
should_sync INTEGER NOT NULL DEFAULT 0,
last_sync_ts TEXT
);
) STRICT;
CREATE INDEX contact_profiles_index ON contact_profiles(
display_name,
full_name
@@ -804,9 +803,6 @@ CREATE INDEX idx_group_profiles_user_id ON group_profiles(user_id);
CREATE INDEX idx_groups_chat_item_id ON groups(chat_item_id);
CREATE INDEX idx_groups_group_profile_id ON groups(group_profile_id);
CREATE INDEX idx_messages_group_id ON messages(group_id);
CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages(
group_member_intro_id
);
CREATE INDEX idx_pending_group_messages_message_id ON pending_group_messages(
message_id
);
@@ -1191,6 +1187,18 @@ CREATE UNIQUE INDEX idx_group_members_group_id_index_in_group ON group_members(
group_id,
index_in_group
);
CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON chat_items(
user_id,
contact_id,
msg_content_tag,
created_at
);
CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items(
user_id,
note_folder_id,
msg_content_tag,
created_at
);
CREATE TRIGGER on_group_members_insert_update_summary
AFTER INSERT ON group_members
FOR EACH ROW
+11 -55
View File
@@ -58,7 +58,7 @@ import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON)
import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8)
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
#if defined(dbPostgres)
@@ -871,27 +871,26 @@ data MemberRestrictionStatus
| MRSUnknown Text
deriving (Eq, Show)
instance FromField MemberRestrictionStatus where fromField = blobFieldDecoder strDecode
instance FromField MemberRestrictionStatus where fromField = fromTextField_ textDecode
instance ToField MemberRestrictionStatus where toField = toField . strEncode
instance ToField MemberRestrictionStatus where toField = toField . textEncode
instance StrEncoding MemberRestrictionStatus where
strEncode = \case
instance TextEncoding MemberRestrictionStatus where
textEncode = \case
MRSBlocked -> "blocked"
MRSUnrestricted -> "unrestricted"
MRSUnknown tag -> encodeUtf8 tag
strDecode s = Right $ case s of
MRSUnknown tag -> tag
textDecode s = Just $ case s of
"blocked" -> MRSBlocked
"unrestricted" -> MRSUnrestricted
tag -> MRSUnknown $ safeDecodeUtf8 tag
strP = strDecode <$?> A.takeByteString
tag -> MRSUnknown tag
instance FromJSON MemberRestrictionStatus where
parseJSON = strParseJSON "MemberRestrictionStatus"
parseJSON = textParseJSON "MemberRestrictionStatus"
instance ToJSON MemberRestrictionStatus where
toJSON = strToJSON
toEncoding = strToJEncoding
toJSON = textToJSON
toEncoding = textToEncoding
mrsBlocked :: MemberRestrictionStatus -> Bool
mrsBlocked = \case
@@ -1747,49 +1746,6 @@ instance TextEncoding ConnType where
ConnMember -> "member"
ConnUserContact -> "user_contact"
data GroupMemberIntro = GroupMemberIntro
{ introId :: Int64,
reMember :: GroupMember,
toMember :: GroupMember,
introStatus :: GroupMemberIntroStatus
}
deriving (Show)
data GroupMemberIntroStatus
= GMIntroPending
| GMIntroSent
| GMIntroInvReceived
| GMIntroInvForwarded
| GMIntroReConnected
| GMIntroToConnected
| GMIntroConnected
deriving (Eq, Show)
instance FromField GroupMemberIntroStatus where fromField = fromTextField_ introStatusT
instance ToField GroupMemberIntroStatus where toField = toField . serializeIntroStatus
introStatusT :: Text -> Maybe GroupMemberIntroStatus
introStatusT = \case
"new" -> Just GMIntroPending
"sent" -> Just GMIntroSent
"rcv" -> Just GMIntroInvReceived
"fwd" -> Just GMIntroInvForwarded
"re-con" -> Just GMIntroReConnected
"to-con" -> Just GMIntroToConnected
"con" -> Just GMIntroConnected
_ -> Nothing
serializeIntroStatus :: GroupMemberIntroStatus -> Text
serializeIntroStatus = \case
GMIntroPending -> "new"
GMIntroSent -> "sent"
GMIntroInvReceived -> "rcv"
GMIntroInvForwarded -> "fwd"
GMIntroReConnected -> "re-con"
GMIntroToConnected -> "to-con"
GMIntroConnected -> "con"
type CommandId = Int64
aCorrId :: CommandId -> ACorrId
+1 -1
View File
@@ -782,7 +782,7 @@ groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature ->
groupPrefStateText feature pref param role =
let enabled = getField @"enable" pref
paramText = if enabled == FEOn then groupParamText_ feature param else ""
roleText = maybe "" (\r -> " for " <> safeDecodeUtf8 (strEncode r) <> "s") role
roleText = maybe "" (\r -> " for " <> textEncode r <> "s") role
in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText <> roleText
groupParamText_ :: GroupFeature -> Maybe Int -> Text
+16 -17
View File
@@ -7,7 +7,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder)
import Simplex.Messaging.Agent.Store.DB (fromTextField_)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$?>))
@@ -20,34 +20,33 @@ data GroupMemberRole
| GROwner -- + delete and change group information, add/remove/change roles for Owners
deriving (Eq, Show, Ord)
instance FromField GroupMemberRole where fromField = blobFieldDecoder strDecode
instance FromField GroupMemberRole where fromField = fromTextField_ textDecode
instance ToField GroupMemberRole where toField = toField . strEncode
instance ToField GroupMemberRole where toField = toField . textEncode
instance StrEncoding GroupMemberRole where
strEncode = \case
instance TextEncoding GroupMemberRole where
textEncode = \case
GROwner -> "owner"
GRAdmin -> "admin"
GRModerator -> "moderator"
GRMember -> "member"
GRAuthor -> "author"
GRObserver -> "observer"
strDecode = \case
"owner" -> Right GROwner
"admin" -> Right GRAdmin
"moderator" -> Right GRModerator
"member" -> Right GRMember
"author" -> Right GRAuthor
"observer" -> Right GRObserver
r -> Left $ "bad GroupMemberRole " <> B.unpack r
strP = strDecode <$?> A.takeByteString
textDecode = \case
"owner" -> Just GROwner
"admin" -> Just GRAdmin
"moderator" -> Just GRModerator
"member" -> Just GRMember
"author" -> Just GRAuthor
"observer" -> Just GRObserver
r -> Nothing
instance FromJSON GroupMemberRole where
parseJSON = strParseJSON "GroupMemberRole"
parseJSON = textParseJSON "GroupMemberRole"
instance ToJSON GroupMemberRole where
toJSON = strToJSON
toEncoding = strToJEncoding
toJSON = textToJSON
toEncoding = textToEncoding
data GroupAcceptance = GAAccepted | GAPendingApproval | GAPendingReview deriving (Eq, Show)
+15 -10
View File
@@ -122,6 +122,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats]
CRChats chats -> viewChats ts tz chats
CRApiChat u chat _ -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat]
CRChatContentTypes cts -> [plain $ "Chat content types: " <> T.intercalate ", " (map (safeDecodeUtf8 . strEncode) cts)]
CRChatTags u tags -> ttyUser u $ [viewJSON tags]
CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure
CRServerOperatorConditions (ServerOperatorConditions ops _ ca) -> viewServerOperators ops ca
@@ -273,8 +274,10 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
[ (maybe "connecting new remote controller" (\RemoteCtrlInfo {remoteCtrlId} -> "connecting remote controller " <> sShow remoteCtrlId) remoteCtrl_ <> ": ")
<> viewRemoteCtrl ctrlAppInfo appVersion True
]
CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} ->
["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName]
CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} compression ->
["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName <> " (" <> compressStr <> " compression)"]
where
compressStr = if compression then "with" else "no"
CRSQLResult rows -> map plain rows
#if !defined(dbPostgres)
CRArchiveExported archiveErrs -> if null archiveErrs then ["ok"] else ["archive export errors: " <> plain (show archiveErrs)]
@@ -486,7 +489,9 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
plain sessionCode
]
CEvtNewRemoteHost RemoteHostInfo {remoteHostId = rhId, hostDeviceName} -> ["new remote host " <> sShow rhId <> " added: " <> plain hostDeviceName]
CEvtRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} -> ["remote host " <> sShow rhId <> " connected"]
CEvtRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} compression -> ["remote host " <> sShow rhId <> " connected (" <> compressStr <> " compression)"]
where
compressStr = if compression then "with" else "no"
CEvtRemoteHostStopped {remoteHostId_} ->
[ maybe "new remote host" (mappend "remote host " . sShow) remoteHostId_ <> " stopped"
]
@@ -1227,7 +1232,7 @@ viewConnectedToGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, m
viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString]
viewReceivedGroupInvitation g c role =
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role)
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> showRole role
: case incognitoMembershipProfile g of
Just mp -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)]
Nothing -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to accept"]
@@ -1266,15 +1271,15 @@ viewMembersBlockedForAllUser g members blocked = case members of
mems' -> [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> sShow (length mems') <> " members"]
showRole :: GroupMemberRole -> StyledString
showRole = plain . strEncode
showRole = plain . textEncode
viewGroupMembers :: Group -> [StyledString]
viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filter (not . removedOrLeft) $ membership : members
where
removedOrLeft m = let s = memberStatus m in s == GSMemRejected || s == GSMemRemoved || s == GSMemLeft
groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (intercalate ", " $ [role m] <> category m <> status m <> muted m)
role :: GroupMember -> String
role GroupMember {memberRole} = B.unpack $ strEncode memberRole
groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (T.intercalate ", " $ [role m] <> category m <> status m <> muted m)
role :: GroupMember -> Text
role GroupMember {memberRole} = textEncode memberRole
category m = case memberCategory m of
GCUserMember -> ["you"]
GCInviteeMember -> ["invited"]
@@ -2451,8 +2456,8 @@ viewChatError isCmd logLevel testView = \case
CEGroupUserRole g role ->
(: []) . (ttyGroup' g <>) $ case role of
GRAuthor -> ": you don't have permission to send messages"
_ -> ": you have insufficient permissions for this action, the required role is " <> plain (strEncode role)
CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> plain (strEncode role) <> ", use member or observer"]
_ -> ": you have insufficient permissions for this action, the required role is " <> showRole role
CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> showRole role <> ", use member or observer"]
CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"]
CEGroupIncognitoCantInvite -> ["you are using an incognito profile for this group - prohibited to invite contacts"]
CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"]
+6 -6
View File
@@ -1167,8 +1167,7 @@ testCapthaScreening ps =
bob <## "/'filter 1 off' - disable filter"
-- connect with captcha screen
_ <- join cath groupLink
cath ##> "/_send #1(_support) text 123" -- sending incorrect captcha
cath <# "#privacy (support) 123"
cath #> "#privacy (support) 123" -- sending incorrect captcha
cath <# "#privacy (support) 'SimpleX Directory'!> > cath 123"
cath <## " Incorrect text, please try again."
captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
@@ -1220,8 +1219,7 @@ testCapthaScreening ps =
cath <## "Send captcha text to join the group privacy."
dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath
sendCaptcha cath captcha = do
cath ##> ("/_send #1(_support) text " <> captcha)
cath <# ("#privacy (support) " <> captcha)
cath #> ("#privacy (support) " <> captcha)
cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha)
cath <## " Correct, you joined the group privacy"
cath <## "#privacy: you joined the group"
@@ -1411,8 +1409,10 @@ submitGroup u n fn = do
groupAccepted :: TestCC -> String -> IO String
groupAccepted u n = do
u <# ("'SimpleX Directory'> Joining the group " <> n <> "…")
u <## ("#" <> viewName n <> ": 'SimpleX Directory' joined the group")
u <###
[ WithTime ("'SimpleX Directory'> Joining the group " <> n <> "…"),
ConsoleString ("#" <> viewName n <> ": 'SimpleX Directory' joined the group")
]
u <# ("'SimpleX Directory'> Joined the group " <> n <> ", creating the link…")
u <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online."
u <## ""
+17 -17
View File
@@ -84,7 +84,7 @@ schemaDumpDBOpts =
DBOpts
{ connstr = B.pack testDBConnstr,
schema = "test_chat_schema",
poolSize = 3,
poolSize = 10,
createSchema = True
}
@@ -131,7 +131,7 @@ testCoreOpts =
-- dbSchemaPrefix is not used in tests (except bot tests where it's redefined),
-- instead different schema prefix is passed per client so that single test database is used
dbSchemaPrefix = "",
dbPoolSize = 3,
dbPoolSize = 10,
dbCreateSchema = True
#else
{ dbFilePrefix = "./simplex_v1", -- dbFilePrefix is not used in tests (except bot tests where it's redefined)
@@ -184,16 +184,11 @@ aCfg = (agentConfig defaultChatConfig) {tbqSize = 16}
testAgentCfg :: AgentConfig
testAgentCfg =
aCfg
{ reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}
}
testAgentCfgSlow :: AgentConfig
testAgentCfgSlow =
testAgentCfg
{ smpClientVRange = mkVersionRange (Version 1) srvHostnamesSMPClientVersion, -- v2
smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion pqdrSMPAgentVersion, -- v5
smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange minClientSMPRelayVersion sendingProxySMPVersion} -- v8
{ reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000},
messageRetryInterval = RetryInterval2 {riFast = riFast {initialInterval = 50000}, riSlow = riSlow {initialInterval = 50000}}
}
where
RetryInterval2 {riFast, riSlow} = messageRetryInterval aCfg
testAgentCfgNoShortLinks :: AgentConfig
testAgentCfgNoShortLinks =
@@ -213,9 +208,6 @@ testCfg =
confirmMigrations = MCYesUp
}
testCfgSlow :: ChatConfig
testCfgSlow = testCfg {agentConfig = testAgentCfgSlow}
testCfgNoShortLinks :: ChatConfig
testCfgNoShortLinks = testCfg {agentConfig = testAgentCfgNoShortLinks}
@@ -432,7 +424,10 @@ testChatN cfg opts ps test params =
(<//) cc t = timeout t (getTermLine cc) `shouldReturn` Nothing
getTermLine :: HasCallStack => TestCC -> IO String
getTermLine cc@TestCC {printOutput} =
getTermLine = getTermLine' Nothing
getTermLine' :: HasCallStack => Maybe String -> TestCC -> IO String
getTermLine' expected cc@TestCC {printOutput} =
5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case
Just s -> do
-- remove condition to always echo virtual terminal
@@ -441,7 +436,12 @@ getTermLine cc@TestCC {printOutput} =
name <- userName cc
putStrLn $ name <> ": " <> s
pure s
_ -> error "no output for 5 seconds"
Nothing -> do
name <- userName cc
let expectedMsg = case expected of
Just e -> ", expected: " <> show e
Nothing -> ""
error $ name <> ": no output for 5 seconds" <> expectedMsg
userName :: TestCC -> IO [Char]
userName (TestCC ChatController {currentUser} _ _ _ _ _) =
@@ -522,7 +522,7 @@ smpServerCfg :: ServerConfig STMMsgStore
smpServerCfg =
ServerConfig
{ transports = [(serverPort, transport @TLS, False)],
tbqSize = 1,
tbqSize = 4,
msgQueueQuota = 16,
maxJournalMsgCount = 24,
maxJournalStateLines = 4,
+28 -71
View File
@@ -94,22 +94,9 @@ chatDirectTests = do
describe "operators and usage conditions" $ do
it "get and enable operators, accept conditions" testOperators
describe "async connection handshake" $ do
describe "connect when initiating client goes offline" $ do
it "curr" $ testAsyncInitiatingOffline True testCfg testCfg
it "v5" $ testAsyncInitiatingOffline False testCfgSlow testCfgSlow
it "v5/curr" $ testAsyncInitiatingOffline False testCfgSlow testCfg
it "curr/v5" $ testAsyncInitiatingOffline True testCfg testCfgSlow
describe "connect when accepting client goes offline" $ do
it "curr" $ testAsyncAcceptingOffline True testCfg testCfg
it "v5" $ testAsyncAcceptingOffline False testCfgSlow testCfgSlow
it "v5/curr" $ testAsyncAcceptingOffline False testCfgSlow testCfg
it "curr/v5" $ testAsyncAcceptingOffline True testCfg testCfgSlow
describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do
it "curr" testFullAsyncFast
-- fails in CI
xit'' "v5" $ testFullAsyncSlow False testCfgSlow testCfgSlow
xit'' "v5/curr" $ testFullAsyncSlow False testCfgSlow testCfg
xit'' "curr/v5" $ testFullAsyncSlow True testCfg testCfgSlow
it "connect when initiating client goes offline" $ testAsyncInitiatingOffline True
it "connect when accepting client goes offline" $ testAsyncAcceptingOffline True
it "connect, fully asynchronous (when clients are never simultaneously online)" $ testFullAsyncFast
describe "webrtc calls api" $ do
it "negotiate call" testNegotiateCall
#if !defined(dbPostgres)
@@ -1241,33 +1228,33 @@ testOperators =
where
opts' = testOpts {coreOptions = testCoreOpts {smpServers = [], xftpServers = []}}
testAsyncInitiatingOffline :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO ()
testAsyncInitiatingOffline withShortLink aliceCfg bobCfg ps = do
inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do
testAsyncInitiatingOffline :: HasCallStack => Bool -> TestParams -> IO ()
testAsyncInitiatingOffline withShortLink ps = do
inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do
threadDelay 250000
alice ##> "/c"
(if withShortLink then getInvitation else getInvitationNoShortLink) alice
withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do
withNewTestChat ps "bob" bobProfile $ \bob -> do
threadDelay 250000
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
withTestChatCfg ps aliceCfg "alice" $ \alice -> do
withTestChat ps "alice" $ \alice -> do
alice <## "subscribed 1 connections on server localhost"
concurrently_
(bob <## "alice (Alice): contact is connected")
(alice <## "bob (Bob): contact is connected")
testAsyncAcceptingOffline :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO ()
testAsyncAcceptingOffline withShortLink aliceCfg bobCfg ps = do
inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do
testAsyncAcceptingOffline :: HasCallStack => Bool -> TestParams -> IO ()
testAsyncAcceptingOffline withShortLink ps = do
inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do
alice ##> "/c"
(if withShortLink then getInvitation else getInvitationNoShortLink) alice
withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do
withNewTestChat ps "bob" bobProfile $ \bob -> do
threadDelay 250000
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
withTestChatCfg ps aliceCfg "alice" $ \alice -> do
withTestChatCfg ps bobCfg "bob" $ \bob -> do
withTestChat ps "alice" $ \alice -> do
withTestChat ps "bob" $ \bob -> do
alice <## "subscribed 1 connections on server localhost"
bob <## "subscribed 1 connections on server localhost"
concurrently_
@@ -1292,30 +1279,6 @@ testFullAsyncFast ps = do
bob <## "subscribed 1 connections on server localhost"
bob <## "alice (Alice): contact is connected"
testFullAsyncSlow :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO ()
testFullAsyncSlow withShortLink aliceCfg bobCfg ps = do
inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do
threadDelay 250000
alice ##> "/c"
(if withShortLink then getInvitation else getInvitationNoShortLink) alice
withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do
threadDelay 250000
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
withAlice $ \alice ->
alice <## "subscribed 1 connections on server localhost"
withBob $ \bob ->
bob <## "subscribed 1 connections on server localhost"
withAlice $ \alice -> do
alice <## "subscribed 1 connections on server localhost"
alice <## "bob (Bob): contact is connected"
withBob $ \bob -> do
bob <## "subscribed 1 connections on server localhost"
bob <## "alice (Alice): contact is connected"
where
withAlice = withTestChatCfg ps aliceCfg "alice"
withBob = withTestChatCfg ps aliceCfg "bob"
testCallType :: CallType
testCallType = CallType {media = CMVideo, capabilities = CallCapabilities {encryption = True}}
@@ -1341,7 +1304,7 @@ repeatM_ n a = forM_ [1 .. n] $ const a
testNegotiateCall :: HasCallStack => TestParams -> IO ()
testNegotiateCall =
testChat2 aliceProfile bobProfile $ \alice bob -> do
withTestOutput $ testChat2 aliceProfile bobProfile $ \alice bob -> do
connectUsers alice bob
-- just for testing db query
alice ##> "/_call get"
@@ -2200,7 +2163,7 @@ testUsersDifferentCIExpirationTTL ps = do
showActiveUser alice "alisa"
alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
threadDelay 2000000
threadDelay 2100000
alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")])
where
@@ -2419,7 +2382,7 @@ testDisableCIExpirationOnlyForOneUser ps = do
cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000}
testUsersTimedMessages :: HasCallStack => TestParams -> IO ()
testUsersTimedMessages ps = do
testUsersTimedMessages ps' = do
withNewTestChat ps "bob" bobProfile $ \bob -> do
withNewTestChat ps "alice" aliceProfile $ \alice -> do
connectUsers alice bob
@@ -2462,10 +2425,8 @@ testUsersTimedMessages ps = do
threadDelay 1000000
alice <## "[user: alice] timed message deleted: alice 1"
alice <## "[user: alice] timed message deleted: alice 2"
bob <## "timed message deleted: alice 1"
bob <## "timed message deleted: alice 2"
alice <### ["[user: alice] timed message deleted: alice 1", "[user: alice] timed message deleted: alice 2"]
bob <### ["timed message deleted: alice 1", "timed message deleted: alice 2"]
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
@@ -2477,10 +2438,8 @@ testUsersTimedMessages ps = do
threadDelay 1000000
alice <## "timed message deleted: alisa 1"
alice <## "timed message deleted: alisa 2"
bob <## "timed message deleted: alisa 1"
bob <## "timed message deleted: alisa 2"
alice <### ["timed message deleted: alisa 1", "timed message deleted: alisa 2"]
bob <### ["timed message deleted: alisa 1", "timed message deleted: alisa 2"]
alice ##> "/user"
showActiveUser alice "alisa"
@@ -2519,10 +2478,8 @@ testUsersTimedMessages ps = do
-- messages are deleted after restart
threadDelay 1000000
alice <## "[user: alice] timed message deleted: alice 3"
alice <## "[user: alice] timed message deleted: alice 4"
bob <## "timed message deleted: alice 3"
bob <## "timed message deleted: alice 4"
alice <### ["[user: alice] timed message deleted: alice 3", "[user: alice] timed message deleted: alice 4"]
bob <### ["timed message deleted: alice 3", "timed message deleted: alice 4"]
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
@@ -2534,15 +2491,14 @@ testUsersTimedMessages ps = do
threadDelay 1000000
alice <## "timed message deleted: alisa 3"
alice <## "timed message deleted: alisa 4"
bob <## "timed message deleted: alisa 3"
bob <## "timed message deleted: alisa 4"
alice <### ["timed message deleted: alisa 3", "timed message deleted: alisa 4"]
bob <### ["timed message deleted: alisa 3", "timed message deleted: alisa 4"]
alice ##> "/user"
showActiveUser alice "alisa"
alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")])
where
ps = ps' {printOutput = True} :: TestParams
configureTimedMessages :: HasCallStack => TestCC -> TestCC -> String -> String -> IO ()
configureTimedMessages alice bob bobId ttl = do
aliceName <- userName alice
@@ -2699,7 +2655,7 @@ testUserPrivacy =
testSetChatItemTTL :: HasCallStack => TestParams -> IO ()
testSetChatItemTTL =
testChat2 aliceProfile bobProfile $
\alice bob -> do
\alice bob -> withXFTPServer $ do
connectUsers alice bob
alice #> "@bob 1"
bob <# "alice> 1"
@@ -2713,6 +2669,7 @@ testSetChatItemTTL =
alice <## "use /fc 1 to cancel sending"
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
alice <## "completed uploading file 1 (test.jpg) for bob"
-- above items should be deleted after we set ttl
threadDelay 3000000
alice #> "@bob 3"
+37 -7
View File
@@ -68,7 +68,7 @@ runTestMessageWithFile :: HasCallStack => TestParams -> IO ()
runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do
connectUsers alice bob
alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}]"
alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"file\", \"text\": \"hi, sending a file\"}}]"
alice <# "@bob hi, sending a file"
alice <# "/f @bob ./tests/fixtures/test.jpg"
alice <## "use /fc 1 to cancel sending"
@@ -83,12 +83,22 @@ runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withX
"started receiving file 1 (test.jpg) from alice"
]
bob <## "completed receiving file 1 (test.jpg) from alice"
bob #> "@alice received"
alice <# "bob> received"
src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src
alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")])
bob #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")])
alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg"), ((0, "received"), Nothing)])
alice ##> "/_get content types @2"
alice <## "Chat content types: file, text"
alice #$> ("/_get chat @2 content=file count=100", chatF, [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")])
bob #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg"), ((1, "received"), Nothing)])
bob ##> "/_get content types @2"
bob <## "Chat content types: file, text"
bob #$> ("/_get chat @2 content=file count=100", chatF, [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")])
testSendImage :: HasCallStack => TestParams -> IO ()
testSendImage =
@@ -343,15 +353,33 @@ testGroupSendImage =
"started receiving file 1 (test.jpg) from alice"
]
cath <## "completed receiving file 1 (test.jpg) from alice"
threadDelay 1000000
bob #> "#team received"
[alice, cath] *<# "#team bob> received"
threadDelay 1000000
cath #> "#team received too"
[alice, bob] *<# "#team cath> received too"
src <- B.readFile "./tests/fixtures/test.jpg"
dest <- B.readFile "./tests/tmp/test.jpg"
dest `shouldBe` src
dest2 <- B.readFile "./tests/tmp/test_1.jpg"
dest2 `shouldBe` src
alice #$> ("/_get chat #1 count=1", chatF, [((1, ""), Just "./tests/fixtures/test.jpg")])
bob #$> ("/_get chat #1 count=1", chatF, [((0, ""), Just "./tests/tmp/test.jpg")])
cath #$> ("/_get chat #1 count=1", chatF, [((0, ""), Just "./tests/tmp/test_1.jpg")])
alice #$> ("/_get chat #1 count=3", chatF, [((1, ""), Just "./tests/fixtures/test.jpg"), ((0, "received"), Nothing), ((0, "received too"), Nothing)])
alice ##> "/_get content types #1"
alice <## "Chat content types: image, text"
alice #$> ("/_get chat #1 content=image count=100", chatF, [((1, ""), Just "./tests/fixtures/test.jpg")])
bob #$> ("/_get chat #1 count=3", chatF, [((0, ""), Just "./tests/tmp/test.jpg"), ((1, "received"), Nothing), ((0, "received too"), Nothing)])
bob ##> "/_get content types #1"
bob <## "Chat content types: image, text"
bob #$> ("/_get chat #1 content=image count=100", chatF, [((0, ""), Just "./tests/tmp/test.jpg")])
cath #$> ("/_get chat #1 count=3", chatF, [((0, ""), Just "./tests/tmp/test_1.jpg"), ((0, "received"), Nothing), ((1, "received too"), Nothing)])
cath ##> "/_get content types #1"
cath <## "Chat content types: image, text"
cath #$> ("/_get chat #1 content=image count=100", chatF, [((0, ""), Just "./tests/tmp/test_1.jpg")])
testGroupSendImageWithTextAndQuote :: HasCallStack => TestParams -> IO ()
testGroupSendImageWithTextAndQuote =
@@ -761,7 +789,9 @@ testXFTPDeleteUploadedFileGroup =
alice ##> "/fc 1"
concurrentlyN_
[ alice <## "cancelled sending file 1 (test.pdf) to bob, cath",
[ do
recipients <- dropStrPrefix "cancelled sending file 1 (test.pdf) to " <$> getTermLine alice
recipients == "bob, cath" || recipients == "cath, bob" `shouldBe` True,
cath <## "alice cancelled sending file 1 (test.pdf)"
]
+22 -87
View File
@@ -94,10 +94,7 @@ chatGroupTests = do
describe "batch send messages" $ do
it "send multiple messages api" testSendMulti
it "send multiple timed messages" testSendMultiTimed
#if !defined(dbPostgres)
-- TODO [postgres] this test hangs with PostgreSQL
it "send multiple messages (many chat batches)" testSendMultiManyBatches
#endif
it "shared message body is reused" testSharedMessageBody
it "shared batch body is reused" testSharedBatchBody
describe "async group connections" $ do
@@ -124,7 +121,6 @@ chatGroupTests = do
it "ok to connect; known group" testPlanGroupLinkKnown
it "own group link" testPlanGroupLinkOwn
it "group link without contact - connecting" testPlanGroupLinkConnecting
it "group link without contact - connecting (slow handshake)" testPlanGroupLinkConnectingSlow
it "re-join existing group after leaving" testPlanGroupLinkLeaveRejoin
#if !defined(dbPostgres)
-- TODO [postgres] restore from outdated db backup (same as in agent)
@@ -1774,8 +1770,6 @@ testGroupDelayedModeration ps = do
-- imitate not implemented group forwarding
-- (real client wouldn't have forwarding code, but tests use "current code" with configured version,
-- and forwarding client doesn't check compatibility)
void $ withCCTransaction alice $ \db ->
DB.execute_ db "UPDATE group_member_intros SET intro_status='con'"
updateGroupForwardingVectors alice "bob" "cath" MRConnected
cath #> "#team hi" -- message is pending for bob
@@ -1821,8 +1815,6 @@ testGroupDelayedModerationFullDelete ps = do
-- imitate not implemented group forwarding
-- (real client wouldn't have forwarding code, but tests use "current code" with configured version,
-- and forwarding client doesn't check compatibility)
void $ withCCTransaction alice $ \db ->
DB.execute_ db "UPDATE group_member_intros SET intro_status='con'"
updateGroupForwardingVectors alice "bob" "cath" MRConnected
cath #> "#team hi" -- message is pending for bob
@@ -2048,20 +2040,17 @@ testSendMultiManyBatches =
(bob <# ("#team alice> message " <> show i))
(cath <# ("#team alice> message " <> show i))
aliceItemsCount <- withCCTransaction alice $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]]
aliceItemsCount `shouldBe` [[300]]
bobItemsCount <- withCCTransaction bob $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]]
bobItemsCount `shouldBe` [[300]]
cathItemsCount <- withCCTransaction cath $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdCath) :: IO [[Int]]
cathItemsCount `shouldBe` [[300]]
checkItemCount alice msgIdAlice 300
checkItemCount bob msgIdBob 300
checkItemCount cath msgIdCath 300
where
checkItemCount c msgId n = do
itemsCount <- withCCTransaction c $ \db ->
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgId) :: IO [[Int]]
itemsCount `shouldBe` [[n]]
testSharedMessageBody :: HasCallStack => TestParams -> IO ()
testSharedMessageBody ps =
testSharedMessageBody ps' =
withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> do
withSmpServer' serverCfg' $
withNewTestChatOpts ps opts' "bob" bobProfile $ \bob ->
@@ -2070,9 +2059,7 @@ testSharedMessageBody ps =
alice <## "disconnected 4 connections on server localhost"
alice #> "#team hello"
bodiesCount1 <- withCCAgentTransaction alice $ \db ->
DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]]
bodiesCount1 `shouldBe` [[1]]
checkMsgBodyCount alice 1
withSmpServer' serverCfg' $
withTestChatOpts ps opts' "bob" $ \bob ->
@@ -2084,12 +2071,12 @@ testSharedMessageBody ps =
]
bob <# "#team alice> hello"
cath <# "#team alice> hello"
bodiesCount2 <- withCCAgentTransaction alice $ \db ->
DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]]
bodiesCount2 `shouldBe` [[0]]
threadDelay 500000
checkMsgBodyCount alice 0
alice <## "disconnected 4 connections on server localhost"
where
ps = ps' {printOutput = True} :: TestParams
tmp = tmpPath ps
serverCfg' =
smpServerCfg
@@ -2104,6 +2091,12 @@ testSharedMessageBody ps =
}
}
checkMsgBodyCount :: TestCC -> Int -> IO ()
checkMsgBodyCount c n = do
bodiesCount <- withCCAgentTransaction c $ \db ->
DB.query_ db "SELECT count(1) FROM snd_message_bodies"
bodiesCount `shouldBe` [[n]]
testSharedBatchBody :: HasCallStack => TestParams -> IO ()
testSharedBatchBody ps =
withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> do
@@ -2120,9 +2113,7 @@ testSharedBatchBody ps =
_ <- getTermLine alice
alice <## "300 messages sent"
bodiesCount1 <- withCCAgentTransaction alice $ \db ->
DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]]
bodiesCount1 `shouldBe` [[3]]
checkMsgBodyCount alice 3
withSmpServer' serverCfg' $
withTestChatOpts ps opts' "bob" $ \bob ->
@@ -2136,9 +2127,7 @@ testSharedBatchBody ps =
concurrently_
(bob <# ("#team alice> message " <> show i))
(cath <# ("#team alice> message " <> show i))
bodiesCount2 <- withCCAgentTransaction alice $ \db ->
DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]]
bodiesCount2 `shouldBe` [[0]]
checkMsgBodyCount alice 0
alice <## "disconnected 4 connections on server localhost"
where
@@ -3615,49 +3604,6 @@ testPlanGroupLinkConnecting ps = do
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
testPlanGroupLinkConnectingSlow :: HasCallStack => TestParams -> IO ()
testPlanGroupLinkConnectingSlow ps = do
gLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team"
getGroupLinkNoShortLink alice "team" GRMember True
withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do
threadDelay 100000
bob ##> ("/c " <> gLink)
bob <## "connection request sent!"
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting, allowed to reconnect"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: connecting, allowed to reconnect"
threadDelay 100000
withTestChatCfg ps testCfgSlow "alice" $ \alice -> do
alice
<### [ "subscribed 1 connections on server localhost",
"bob (Bob): accepting request to join group #team..."
]
withTestChatCfg ps testCfgSlow "bob" $ \bob -> do
threadDelay 500000
bob <## "subscribed 1 connections on server localhost"
bob <## "#team: joining the group..."
bob ##> ("/_connect plan 1 " <> gLink)
bob <## "group link: connecting to group #team"
let gLinkSchema2 = linkAnotherSchema gLink
bob ##> ("/_connect plan 1 " <> gLinkSchema2)
bob <## "group link: connecting to group #team"
bob ##> ("/c " <> gLink)
bob <## "group link: connecting to group #team"
#if !defined(dbPostgres)
testGroupMsgDecryptError :: HasCallStack => TestParams -> IO ()
testGroupMsgDecryptError ps =
@@ -5049,15 +4995,6 @@ setupGroupForwarding host invitee1 invitee2 = do
WHERE group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?)
|]
(Only invitee1Name)
void $ withCCTransaction host $ \db ->
DB.execute
db
[sql|
UPDATE group_member_intros SET intro_status='fwd'
WHERE re_group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?)
AND to_group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?)
|]
(invitee1Name, invitee2Name)
setupGroupForwardingVectors host invitee1 invitee2
@@ -5110,8 +5047,6 @@ testGroupMsgForwardDeduplicate =
createGroup3 "team" alice bob cath
threadDelay 1000000 -- delay so member relations don't get overwritten to connected
void $ withCCTransaction alice $ \db ->
DB.execute_ db "UPDATE group_member_intros SET intro_status='fwd'"
setupGroupForwardingVectors alice bob cath
bob #> "#team hi there"
@@ -8448,7 +8383,7 @@ testChannelsRelayDeliver =
createChannel5 :: TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> GroupMemberRole -> IO ()
createChannel5 alice bob cath dan eve mRole = do
createGroup2 "team" alice bob
bob ##> ("/create link #team " <> B.unpack (strEncode mRole))
bob ##> ("/create link #team " <> T.unpack (textEncode mRole))
gLink <- getGroupLink bob "team" mRole True
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"
+7 -4
View File
@@ -134,10 +134,13 @@ testFiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
alice ##> "/tail"
alice <# "* hi myself"
alice <# "* file 1 (test.jpg)"
alice `send` "/* text note"
alice <# "* text note"
alice ##> "/_get chat *1 count=100"
r <- chatF <$> getTermLine alice
r `shouldBe` [((1, "hi myself"), Just "test.jpg")]
alice #$> ("/_get chat *1 count=100", chatF, [((1, "hi myself"), Just "test.jpg"), ((1, "text note"), Nothing)])
alice ##> "/_get content types *1"
alice <## "Chat content types: image, text"
alice #$> ("/_get chat *1 content=image count=100", chatF, [((1, "hi myself"), Just "test.jpg")])
alice ##> "/fs 1"
alice <## "bad chat command: not supported for local files"
@@ -151,7 +154,7 @@ testFiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
alice ##> "/_create *1 json [{\"filePath\": \"another_test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]"
alice <# "* file 2 (another_test.jpg)"
alice ##> "/_delete item *1 2 internal"
alice ##> "/_delete item *1 3 internal"
alice <## "message deleted"
doesFileExist stored2 `shouldReturn` False
doesFileExist stored `shouldReturn` True
+23 -74
View File
@@ -61,7 +61,6 @@ chatProfileTests = do
it "contact address ok to connect; known contact" testPlanAddressOkKnown
it "own contact address" testPlanAddressOwn
it "connecting via contact address" testPlanAddressConnecting
it "connecting via contact address (slow handshake)" testPlanAddressConnectingSlow
it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected
it "contact via address" testPlanAddressContactViaAddress
it "contact via short address" testPlanAddressContactViaShortAddress
@@ -72,7 +71,6 @@ chatProfileTests = do
it "set connection incognito" testSetConnectionIncognito
it "reset connection incognito" testResetConnectionIncognito
it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation
it "set connection incognito prohibited during negotiation (slow handshake)" testSetConnectionIncognitoProhibitedDuringNegotiationSlow
it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors
it "set, reset, set connection incognito" testSetResetSetConnectionIncognito
it "join group incognito" testJoinGroupIncognito
@@ -1110,46 +1108,6 @@ testPlanAddressConnecting ps = do
bob <## "contact address: known contact alice"
bob <## "use @alice <message> to send messages"
testPlanAddressConnectingSlow :: HasCallStack => TestParams -> IO ()
testPlanAddressConnectingSlow ps = do
cLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do
alice ##> "/ad"
getContactLinkNoShortLink alice True
withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do
threadDelay 100000
bob ##> ("/c " <> cLink)
bob <## "connection request sent!"
bob ##> ("/_connect plan 1 " <> cLink)
bob <## "contact address: connecting, allowed to reconnect"
let cLinkSchema2 = linkAnotherSchema cLink
bob ##> ("/_connect plan 1 " <> cLinkSchema2)
bob <## "contact address: connecting, allowed to reconnect"
threadDelay 100000
withTestChatCfg ps testCfgSlow "alice" $ \alice -> do
alice <## "subscribed 1 connections on server localhost"
alice <## "bob (Bob) wants to connect to you!"
alice <## "to accept: /ac bob"
alice <## "to reject: /rc bob (the sender will NOT be notified)"
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request..."
withTestChatCfg ps testCfgSlow "bob" $ \bob -> do
threadDelay 500000
bob <## "subscribed 1 connections on server localhost"
bob @@@ [("@alice", "")]
bob ##> ("/_connect plan 1 " <> cLink)
bob <## "contact address: connecting to contact alice"
let cLinkSchema2 = linkAnotherSchema cLink
bob ##> ("/_connect plan 1 " <> cLinkSchema2)
bob <## "contact address: connecting to contact alice"
bob ##> ("/c " <> cLink)
bob <## "contact address: connecting to contact alice"
testPlanAddressContactDeletedReconnected :: HasCallStack => TestParams -> IO ()
testPlanAddressContactDeletedReconnected =
testChat2 aliceProfile bobProfile $
@@ -1559,30 +1517,6 @@ testSetConnectionIncognitoProhibitedDuringNegotiation ps = do
alice `hasContactProfiles` ["alice", "bob"]
bob `hasContactProfiles` ["alice", "bob"]
testSetConnectionIncognitoProhibitedDuringNegotiationSlow :: HasCallStack => TestParams -> IO ()
testSetConnectionIncognitoProhibitedDuringNegotiationSlow ps = do
inv <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do
threadDelay 250000
alice ##> "/connect"
getInvitationNoShortLink alice
withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do
threadDelay 250000
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
withTestChatCfg ps testCfgSlow "alice" $ \alice -> do
threadDelay 250000
alice <## "subscribed 1 connections on server localhost"
alice ##> "/_set incognito :1 on"
alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}"
withTestChatCfg ps testCfgSlow "bob" $ \bob -> do
bob <## "subscribed 1 connections on server localhost"
concurrently_
(bob <## "alice (Alice): contact is connected")
(alice <## "bob (Bob): contact is connected")
alice <##> bob
alice `hasContactProfiles` ["alice", "bob"]
bob `hasContactProfiles` ["alice", "bob"]
testConnectionIncognitoUnchangedErrors :: HasCallStack => TestParams -> IO ()
testConnectionIncognitoUnchangedErrors = testChat2 aliceProfile bobProfile $
\alice bob -> do
@@ -2022,8 +1956,14 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $
alice ##> "/user alisa"
showActiveUser alice "alisa"
-- Change connection back to other user
#if defined(dbPostgres)
alice ##> "/_set conn user :2 3"
alice <## "connection 2 changed from user alisa to user alisa2, new link:"
#else
-- connection ID does not change in SQLite because table has no auto-increment
alice ##> "/_set conn user :1 3"
alice <## "connection 1 changed from user alisa to user alisa2, new link:"
#endif
alice <## ""
_shortInv <- getTermLine alice
alice <## ""
@@ -2065,8 +2005,14 @@ testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $
alice ##> "/user alisa"
showActiveUser alice "alisa"
-- Change connection back to initial user
#if defined(dbPostgres)
alice ##> "/_set conn user :2 1"
alice <## "connection 2 changed from user alisa to user alice, new link:"
#else
-- connection ID does not change in SQLite because table has no auto-increment
alice ##> "/_set conn user :1 1"
alice <## "connection 1 changed from user alisa to user alice, new link:"
#endif
alice <## ""
_shortInv <- getTermLine alice
alice <## ""
@@ -2104,9 +2050,16 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $
alice ##> "/user alisa"
showActiveUser alice "alisa"
-- Change connection to incognito and make sure it's attached to the newly created user profile
#if defined(dbPostgres)
alice ##> "/_set incognito :2 on"
_ <- getTermLine alice
alice <## "connection 2 changed to incognito"
#else
-- connection ID does not change in SQLite because table has no auto-increment
alice ##> "/_set incognito :1 on"
_ <- getTermLine alice
alice <## "connection 1 changed to incognito"
#endif
bob ##> ("/connect " <> inv)
bob <## "confirmation sent!"
alisaIncognito <- getTermLine alice
@@ -2485,10 +2438,8 @@ testEnableTimedMessagesContact =
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (1, "hi"), (0, "hey")])
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (0, "hi"), (1, "hey")])
threadDelay 1000000
alice <## "timed message deleted: hi"
alice <## "timed message deleted: hey"
bob <## "timed message deleted: hi"
bob <## "timed message deleted: hey"
alice <### ["timed message deleted: hi", "timed message deleted: hey"]
bob <### ["timed message deleted: hi", "timed message deleted: hey"]
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")])
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")])
-- turn off, messages are not disappearing
@@ -2580,10 +2531,8 @@ testTimedMessagesEnabledGlobally =
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (1, "hi"), (0, "hey")])
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (0, "hi"), (1, "hey")])
threadDelay 1000000
alice <## "timed message deleted: hi"
bob <## "timed message deleted: hi"
alice <## "timed message deleted: hey"
bob <## "timed message deleted: hey"
alice <### ["timed message deleted: hi", "timed message deleted: hey"]
bob <### ["timed message deleted: hi", "timed message deleted: hey"]
alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")])
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")])
+43 -29
View File
@@ -81,19 +81,19 @@ businessProfile = mkProfile "biz" "Biz Inc" Nothing
mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile
mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs}
it :: HasCallStack => String -> (TestParams -> Expectation) -> SpecWith (Arg (TestParams -> Expectation))
it :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation))
it name test =
Hspec.it name $ \tmp -> timeout t (test tmp) >>= maybe (error "test timed out") pure
where
t = 90 * 1000000
xit' :: HasCallStack => String -> (TestParams -> Expectation) -> SpecWith (Arg (TestParams -> Expectation))
xit' :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation))
xit' = if os == "linux" then xit else it
xit'' :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a)
xit'' = ifCI xit Hspec.it
xitMacCI :: HasCallStack => String -> (TestParams -> Expectation) -> SpecWith (Arg (TestParams -> Expectation))
xitMacCI :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation))
xitMacCI = ifCI (if os == "darwin" then xit else it) it
xdescribe'' :: HasCallStack => String -> SpecWith a -> SpecWith a
@@ -171,7 +171,8 @@ cc ?#> cmd = do
(#$>) :: (Eq a, Show a, HasCallStack) => TestCC -> (String, String -> a, a) -> Expectation
cc #$> (cmd, f, res) = do
cc ##> cmd
(f <$> getTermLine cc) `shouldReturn` res
let expected = "result of " <> cmd <> ": " <> show res
(f <$> getTermLine' (Just expected) cc) `shouldReturn` res
-- / PQ combinators
@@ -345,7 +346,7 @@ chats = mapChats . read
getChats :: HasCallStack => (Eq a, Show a) => ([(String, String, Maybe ConnStatus)] -> [a]) -> TestCC -> [a] -> Expectation
getChats f cc res = do
cc ##> "/_get chats 1 pcc=on"
line <- getTermLine cc
line <- getTermLine' (Just "chat list") cc
f (read line) `shouldMatchList` res
send :: TestCC -> String -> IO ()
@@ -353,41 +354,41 @@ send TestCC {chatController = cc} cmd = atomically $ writeTBQueue (inputQ cc) cm
(<##) :: HasCallStack => TestCC -> String -> Expectation
cc <## line = do
l <- getTermLine cc
l <- getTermLine' (Just line) cc
when (l /= line) $ print ("expected: " <> line, ", got: " <> l)
l `shouldBe` line
(<##.) :: HasCallStack => TestCC -> String -> Expectation
cc <##. line = do
l <- getTermLine cc
l <- getTermLine' (Just $ "prefix: " <> line) cc
let prefix = line `isPrefixOf` l
unless prefix $ print ("expected to start from: " <> line, ", got: " <> l)
prefix `shouldBe` True
(.<##) :: HasCallStack => TestCC -> String -> Expectation
cc .<## line = do
l <- getTermLine cc
l <- getTermLine' (Just $ "suffix: " <> line) cc
let suffix = line `isSuffixOf` l
unless suffix $ print ("expected to end with: " <> line, ", got: " <> l)
suffix `shouldBe` True
(<#.) :: HasCallStack => TestCC -> String -> Expectation
cc <#. line = do
l <- dropTime <$> getTermLine cc
l <- dropTime <$> getTermLine' (Just $ "prefix: " <> line) cc
let prefix = line `isPrefixOf` l
unless prefix $ print ("expected to start from: " <> line, ", got: " <> l)
prefix `shouldBe` True
(.<#) :: HasCallStack => TestCC -> String -> Expectation
cc .<# line = do
l <- dropTime <$> getTermLine cc
l <- dropTime <$> getTermLine' (Just $ "suffix: " <> line) cc
let suffix = line `isSuffixOf` l
unless suffix $ print ("expected to end with: " <> line, ", got: " <> l)
suffix `shouldBe` True
(<##..) :: HasCallStack => TestCC -> [String] -> Expectation
cc <##.. ls = do
l <- getTermLine cc
l <- getTermLine' (Just $ "one of prefixes: " <> show ls) cc
let prefix = any (`isPrefixOf` l) ls
unless prefix $ print ("expected to start from one of: " <> show ls, ", got: " <> l)
prefix `shouldBe` True
@@ -395,7 +396,8 @@ cc <##.. ls = do
(>*) :: HasCallStack => TestCC -> String -> IO ()
cc >* note = do
cc `send` ("/* " <> note)
(dropTime <$> getTermLine cc) `shouldReturn` ("* " <> note)
let expected = "* " <> note
(dropTime <$> getTermLine' (Just expected) cc) `shouldReturn` expected
data ConsoleResponse
= ConsoleString String
@@ -404,13 +406,21 @@ data ConsoleResponse
| StartsWith String
| Predicate (String -> Bool)
instance Show ConsoleResponse where
show (ConsoleString s) = show s
show (WithTime s) = "WithTime " <> show s
show (EndsWith s) = "EndsWith " <> show s
show (StartsWith s) = "StartsWith " <> show s
show (Predicate _) = "<predicate>"
instance IsString ConsoleResponse where fromString = ConsoleString
-- this assumes that the string can only match one option
getInAnyOrder :: HasCallStack => (String -> String) -> TestCC -> [ConsoleResponse] -> Expectation
getInAnyOrder _ _ [] = pure ()
getInAnyOrder f cc ls = do
line <- f <$> getTermLine cc
let expectedDesc = "one of " <> show (length ls) <> " responses: " <> show ls
line <- f <$> getTermLine' (Just expectedDesc) cc
let rest = filterFirst (expected line) ls
if length rest < length ls
then getInAnyOrder f cc rest
@@ -436,25 +446,27 @@ getInAnyOrder f cc ls = do
(<##?) = getInAnyOrder dropTime
(<#) :: HasCallStack => TestCC -> String -> Expectation
cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line
cc <# line = (dropTime <$> getTermLine' (Just line) cc) `shouldReturn` line
(*<#) :: HasCallStack => [TestCC] -> String -> Expectation
ccs *<# line = mapConcurrently_ (<# line) ccs
(?<#) :: HasCallStack => TestCC -> String -> Expectation
cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line
cc ?<# line = do
let expected = "i " <> line
(dropTime <$> getTermLine' (Just expected) cc) `shouldReturn` expected
($<#) :: HasCallStack => (TestCC, String) -> String -> Expectation
(cc, uName) $<# line = (dropTime . dropUser uName <$> getTermLine cc) `shouldReturn` line
(cc, uName) $<# line = (dropTime . dropUser uName <$> getTermLine' (Just $ "for user " <> uName <> ": " <> line) cc) `shouldReturn` line
(^<#) :: HasCallStack => (TestCC, String) -> String -> Expectation
(cc, p) ^<# line = (dropTime . dropStrPrefix p <$> getTermLine cc) `shouldReturn` line
(cc, p) ^<# line = (dropTime . dropStrPrefix p <$> getTermLine' (Just $ "without prefix " <> p <> ": " <> line) cc) `shouldReturn` line
(⩗) :: HasCallStack => TestCC -> String -> Expectation
cc ⩗ line = (dropTime . dropReceipt <$> getTermLine cc) `shouldReturn` line
cc ⩗ line = (dropTime . dropReceipt <$> getTermLine' (Just $ "receipt: " <> line) cc) `shouldReturn` line
(%) :: HasCallStack => TestCC -> String -> Expectation
cc % line = (dropTime . dropPartialReceipt <$> getTermLine cc) `shouldReturn` line
cc % line = (dropTime . dropPartialReceipt <$> getTermLine' (Just $ "partial receipt: " <> line) cc) `shouldReturn` line
(</) :: HasCallStack => TestCC -> Expectation
(</) = (<// 500000)
@@ -527,7 +539,7 @@ getInvitations :: HasCallStack => TestCC -> IO (String, String)
getInvitations cc = do
shortInv <- getInvitation_ cc
cc <##. "The invitation link for old clients:"
fullInv <- getTermLine cc
fullInv <- getTermLine' (Just "full invitation link") cc
pure (shortInv, fullInv)
getInvitationNoShortLink :: HasCallStack => TestCC -> IO String
@@ -537,7 +549,7 @@ getInvitation_ :: HasCallStack => TestCC -> IO String
getInvitation_ cc = do
cc <## "pass this invitation link to your contact (via another channel):"
cc <## ""
inv <- getTermLine cc
inv <- getTermLine' (Just "invitation link") cc
cc <## ""
cc <## "and ask them to connect: /c <invitation_link_above>"
pure inv
@@ -550,7 +562,8 @@ getContactLink cc created = do
getContactLinks :: HasCallStack => TestCC -> Bool -> IO (String, String)
getContactLinks cc created = do
shortLink <- getContactLink_ cc created
fullLink <- dropLinePrefix "The contact link for old clients: " =<< getTermLine cc
line <- getTermLine' (Just "full contact link line") cc
fullLink <- dropLinePrefix "The contact link for old clients: " line
pure (shortLink, fullLink)
getContactLinkNoShortLink :: HasCallStack => TestCC -> Bool -> IO String
@@ -560,7 +573,7 @@ getContactLink_ :: HasCallStack => TestCC -> Bool -> IO String
getContactLink_ cc created = do
cc <## if created then "Your new chat address is created!" else "Your chat address:"
cc <## ""
link <- getTermLine cc
link <- getTermLine' (Just "contact link") cc
cc <## ""
cc <## "Anybody can send you contact requests with: /c <contact_link_above>"
cc <## "to show it again: /sa"
@@ -581,7 +594,8 @@ getGroupLink cc gName mRole created = do
getGroupLinks :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO (String, String)
getGroupLinks cc gName mRole created = do
shortLink <- getGroupLink_ cc gName mRole created
fullLink <- dropLinePrefix "The group link for old clients: " =<< getTermLine cc
line <- getTermLine' (Just "full group link line") cc
fullLink <- dropLinePrefix "The group link for old clients: " line
pure (shortLink, fullLink)
getGroupLinkNoShortLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO String
@@ -591,9 +605,9 @@ getGroupLink_ :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool ->
getGroupLink_ cc gName mRole created = do
cc <## if created then "Group link is created!" else "Group link:"
cc <## ""
link <- getTermLine cc
link <- getTermLine' (Just $ "group link for " <> gName) cc
cc <## ""
cc <## ("Anybody can connect to you and join group as " <> B.unpack (strEncode mRole) <> " with: /c <group_link_above>")
cc <## ("Anybody can connect to you and join group as " <> T.unpack (textEncode mRole) <> " with: /c <group_link_above>")
cc <## ("to show it again: /show link #" <> gName)
cc <## ("to delete it: /delete link #" <> gName <> " (joined members will remain connected to you)")
pure link
@@ -669,7 +683,7 @@ getTestCCContact cc contactId = do
lastItemId :: HasCallStack => TestCC -> IO String
lastItemId cc = do
cc ##> "/last_item_id"
getTermLine cc
getTermLine' (Just "last item id") cc
showActiveUser :: HasCallStack => TestCC -> String -> Expectation
showActiveUser cc name = do
@@ -809,12 +823,12 @@ fullAddMember :: HasCallStack => String -> String -> TestCC -> TestCC -> GroupMe
fullAddMember gName fullName inviting invitee role = do
name1 <- userName inviting
memName <- userName invitee
inviting ##> ("/a " <> gName <> " " <> memName <> " " <> B.unpack (strEncode role))
inviting ##> ("/a " <> gName <> " " <> memName <> " " <> T.unpack (textEncode role))
let fullName' = if null fullName || fullName == gName then "" else " (" <> fullName <> ")"
concurrentlyN_
[ inviting <## ("invitation to join the group #" <> gName <> " sent to " <> memName),
do
invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> B.unpack (strEncode role))
invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> T.unpack (textEncode role))
invitee <## ("use /j " <> gName <> " to accept")
]
+3 -1
View File
@@ -76,5 +76,7 @@ postgresSchemaDumpTest migrations testDBOpts@DBOpts {connstr, schema = testDBSch
skipComparisonForDownMigrations :: [String]
skipComparisonForDownMigrations =
[ -- via_group field moves
"20250922_remove_unused_connections"
"20250922_remove_unused_connections",
-- group_member_intro_id field moves
"20251128_migrate_member_relations"
]
+80 -52
View File
@@ -2,6 +2,7 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module RemoteTests where
@@ -15,7 +16,7 @@ import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.List (find, isPrefixOf)
import qualified Data.Map.Strict as M
import Simplex.Chat.Controller (versionNumber)
import Simplex.Chat.Controller (ChatConfig (..), versionNumber)
import qualified Simplex.Chat.Controller as Controller
import Simplex.Chat.Mobile.File
import Simplex.Chat.Remote (remoteFilesFolder)
@@ -31,6 +32,13 @@ import UnliftIO.Directory
remoteTests :: SpecWith TestParams
remoteTests = describe "Remote" $ do
xdescribe "No compression" $ aroundWith (. ((False, False),)) runRemoteTests
xdescribe "Mobile offers compression" $ aroundWith (. ((True, False),)) runRemoteTests
xdescribe "Desktop offers compression" $ aroundWith (. ((False, True),)) runRemoteTests
describe "With compression" $ aroundWith (. ((True, True),)) runRemoteTests
runRemoteTests :: SpecWith ((Bool, Bool), TestParams)
runRemoteTests = do
describe "protocol handshake" $ do
it "connects with new pairing (stops mobile)" $ remoteHandshakeTest False
it "connects with new pairing (stops desktop)" $ remoteHandshakeTest True
@@ -48,14 +56,14 @@ remoteTests = describe "Remote" $ do
-- * Chat commands
remoteHandshakeTest :: HasCallStack => Bool -> TestParams -> IO ()
remoteHandshakeTest viaDesktop = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do
remoteHandshakeTest :: HasCallStack => Bool -> ((Bool, Bool), TestParams) -> IO ()
remoteHandshakeTest viaDesktop = testRemote $ \compress mobile desktop -> do
desktop ##> "/list remote hosts"
desktop <## "No remote hosts"
mobile ##> "/list remote ctrls"
mobile <## "No remote controllers"
startRemote mobile desktop
startRemote compress mobile desktop
desktop ##> "/list remote hosts"
desktop <## "Remote hosts:"
@@ -77,14 +85,14 @@ remoteHandshakeTest viaDesktop = testChat2 aliceProfile aliceDesktopProfile $ \m
mobile ##> "/list remote ctrls"
mobile <## "No remote controllers"
remoteHandshakeStoredTest :: HasCallStack => TestParams -> IO ()
remoteHandshakeStoredTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do
remoteHandshakeStoredTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
remoteHandshakeStoredTest = testRemote $ \compress mobile desktop -> do
logNote "Starting new session"
startRemote mobile desktop
startRemote compress mobile desktop
stopMobile mobile desktop `catchAny` (logError . tshow)
logNote "Starting stored session"
startRemoteStored mobile desktop
startRemoteStored compress mobile desktop
stopDesktop mobile desktop `catchAny` (logError . tshow)
desktop ##> "/list remote hosts"
@@ -95,23 +103,23 @@ remoteHandshakeStoredTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile
mobile <## "1. My desktop"
logNote "Starting stored session again"
startRemoteStored mobile desktop
startRemoteStored compress mobile desktop
stopMobile mobile desktop `catchAny` (logError . tshow)
remoteHandshakeDiscoverTest :: HasCallStack => TestParams -> IO ()
remoteHandshakeDiscoverTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do
remoteHandshakeDiscoverTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
remoteHandshakeDiscoverTest = testRemote $ \compress mobile desktop -> do
logNote "Preparing new session"
startRemote mobile desktop
startRemote compress mobile desktop
stopMobile mobile desktop `catchAny` (logError . tshow)
logNote "Starting stored session with multicast"
startRemoteDiscover mobile desktop
startRemoteDiscover compress mobile desktop
stopMobile mobile desktop `catchAny` (logError . tshow)
remoteHandshakeRejectTest :: HasCallStack => TestParams -> IO ()
remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop mobileBob -> do
remoteHandshakeRejectTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
remoteHandshakeRejectTest = testRemote3 $ \compress mobile desktop mobileBob -> do
logNote "Starting new session"
startRemote mobile desktop
startRemote compress mobile desktop
stopMobile mobile desktop
mobileBob ##> "/set device name MobileBob"
@@ -135,12 +143,12 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil
mobile <## "Compare session code with controller and use:"
mobile <## ("/verify remote ctrl " <> sessId)
mobile ##> ("/verify remote ctrl " <> sessId)
mobile <## "remote controller 1 session started with My desktop"
desktop <## "remote host 1 connected"
mobile <## ("remote controller 1 session started with My desktop" <> compress)
desktop <## ("remote host 1 connected" <> compress)
stopMobile mobile desktop
storedBindingsTest :: HasCallStack => TestParams -> IO ()
storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do
storedBindingsTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
storedBindingsTest = testRemote $ \compress mobile desktop -> do
desktop ##> "/set device name My desktop"
desktop <## "ok"
mobile ##> "/set device name Mobile"
@@ -166,9 +174,9 @@ storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile deskto
desktop <## "new remote host connecting"
mobile <## "new remote controller connected"
verifyRemoteCtrl mobile desktop
mobile <## "remote controller 1 session started with My desktop"
mobile <## ("remote controller 1 session started with My desktop" <> compress)
desktop <## "new remote host 1 added: Mobile"
desktop <## "remote host 1 connected"
desktop <## ("remote host 1 connected" <> compress)
desktop ##> "/list remote hosts"
desktop <## "Remote hosts:"
@@ -180,9 +188,9 @@ storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile deskto
-- TODO: more parser tests
remoteMessageTest :: HasCallStack => TestParams -> IO ()
remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do
startRemote mobile desktop
remoteMessageTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
remoteMessageTest = testRemote3 $ \compress mobile desktop bob -> do
startRemote compress mobile desktop
contactBob desktop bob
logNote "sending messages"
@@ -206,9 +214,9 @@ remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob
threadDelay 1000000
logNote "done"
remoteStoreFileTest :: HasCallStack => TestParams -> IO ()
remoteStoreFileTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
remoteStoreFileTest =
testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob ->
testRemote3 $ \compress mobile desktop bob ->
withXFTPServer $ do
let mobileFiles = "./tests/tmp/mobile_files"
mobile ##> ("/_files_folder " <> mobileFiles)
@@ -223,7 +231,7 @@ remoteStoreFileTest =
bob ##> ("/_files_folder " <> bobFiles)
bob <## "ok"
startRemote mobile desktop
startRemote compress mobile desktop
contactBob desktop bob
rhs <- readTVarIO (Controller.remoteHostSessions $ chatController desktop)
@@ -336,8 +344,8 @@ remoteStoreFileTest =
r `shouldStartWith` "remote host 1 error"
r `shouldContain` err
remoteCLIFileTest :: HasCallStack => TestParams -> IO ()
remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> withXFTPServer $ do
remoteCLIFileTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
remoteCLIFileTest = testRemote3 $ \compress mobile desktop bob -> withXFTPServer $ do
let mobileFiles = "./tests/tmp/mobile_files"
mobile ##> ("/_files_folder " <> mobileFiles)
mobile <## "ok"
@@ -347,7 +355,7 @@ remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob
desktop ##> ("/remote_hosts_folder " <> desktopHostFiles)
desktop <## "ok"
startRemote mobile desktop
startRemote compress mobile desktop
contactBob desktop bob
rhs <- readTVarIO (Controller.remoteHostSessions $ chatController desktop)
@@ -405,9 +413,9 @@ remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob
stopMobile mobile desktop
switchRemoteHostTest :: TestParams -> IO ()
switchRemoteHostTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do
startRemote mobile desktop
switchRemoteHostTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
switchRemoteHostTest = testRemote3 $ \compress mobile desktop bob -> do
startRemote compress mobile desktop
contactBob desktop bob
desktop ##> "/contacts"
@@ -431,10 +439,10 @@ switchRemoteHostTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \
desktop <## "remote host 1 error: RHEInactive"
desktop ##> "/contacts"
indicateRemoteHostTest :: TestParams -> IO ()
indicateRemoteHostTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cathProfile $ \mobile desktop bob cath -> do
indicateRemoteHostTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
indicateRemoteHostTest = testRemote4 $ \compress mobile desktop bob cath -> do
connectUsers desktop cath
startRemote mobile desktop
startRemote compress mobile desktop
contactBob desktop bob
-- remote contact -> remote host
bob #> "@alice hi"
@@ -455,8 +463,8 @@ indicateRemoteHostTest = testChat4 aliceProfile aliceDesktopProfile bobProfile c
desktop <##> cath
cath <##> desktop
multipleProfilesTest :: TestParams -> IO ()
multipleProfilesTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cathProfile $ \mobile desktop bob cath -> do
multipleProfilesTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO ()
multipleProfilesTest = testRemote4 $ \compress mobile desktop bob cath -> do
connectUsers desktop cath
desktop ##> "/create user desk_bottom"
@@ -466,7 +474,7 @@ multipleProfilesTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cat
desktop <## "alice_desktop (Alice Desktop)"
desktop <## "desk_bottom (active)"
startRemote mobile desktop
startRemote compress mobile desktop
contactBob desktop bob
desktop ##> "/users"
desktop <## "alice (Alice) (active)"
@@ -502,8 +510,28 @@ multipleProfilesTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cat
-- * Utils
startRemote :: TestCC -> TestCC -> IO ()
startRemote mobile desktop = do
testRemote :: HasCallStack => (String -> TestCC -> TestCC -> IO()) -> ((Bool, Bool), TestParams) -> IO ()
testRemote test ((mobileCompression, desktopCompression), ps) =
withNewTestChatCfg ps testCfg {remoteCompression = mobileCompression} "mobile" aliceProfile $ \mobile ->
withNewTestChatCfg ps testCfg {remoteCompression = desktopCompression} "desktop" aliceDesktopProfile $ \desktop ->
let compress = " (" <> (if mobileCompression && desktopCompression then "with" else "no") <> " compression)"
in test compress mobile desktop
testRemote3 :: HasCallStack => (String -> TestCC -> TestCC -> TestCC -> IO()) -> ((Bool, Bool), TestParams) -> IO ()
testRemote3 test ps =
testRemote
(\compress mobile desktop -> withNewTestChat (snd ps) "bob" bobProfile $ test compress mobile desktop)
ps
testRemote4 :: HasCallStack => (String -> TestCC -> TestCC -> TestCC -> TestCC -> IO()) -> ((Bool, Bool), TestParams) -> IO ()
testRemote4 test ps =
testRemote3
(\compress mobile desktop bob -> withNewTestChat (snd ps) "cath" cathProfile $ test compress mobile desktop bob)
ps
startRemote :: String -> TestCC -> TestCC -> IO ()
startRemote compress mobile desktop = do
desktop ##> "/set device name My desktop"
desktop <## "ok"
mobile ##> "/set device name Mobile"
@@ -518,12 +546,12 @@ startRemote mobile desktop = do
desktop <## "new remote host connecting"
mobile <## "new remote controller connected"
verifyRemoteCtrl mobile desktop
mobile <## "remote controller 1 session started with My desktop"
mobile <## ("remote controller 1 session started with My desktop" <> compress)
desktop <## "new remote host 1 added: Mobile"
desktop <## "remote host 1 connected"
desktop <## ("remote host 1 connected" <> compress)
startRemoteStored :: TestCC -> TestCC -> IO ()
startRemoteStored mobile desktop = do
startRemoteStored :: String -> TestCC -> TestCC -> IO ()
startRemoteStored compress mobile desktop = do
desktop ##> "/start remote host 1"
desktop <##. "remote host 1 started on "
desktop <##. "other addresses: "
@@ -534,11 +562,11 @@ startRemoteStored mobile desktop = do
desktop <## "remote host 1 connecting"
mobile <## "remote controller 1 connected"
verifyRemoteCtrl mobile desktop
mobile <## "remote controller 1 session started with My desktop"
desktop <## "remote host 1 connected"
mobile <## ("remote controller 1 session started with My desktop" <> compress)
desktop <## ("remote host 1 connected" <> compress)
startRemoteDiscover :: TestCC -> TestCC -> IO ()
startRemoteDiscover mobile desktop = do
startRemoteDiscover :: String -> TestCC -> TestCC -> IO ()
startRemoteDiscover compress mobile desktop = do
desktop ##> "/start remote host 1 multicast=on"
desktop <##. "remote host 1 started on "
desktop <##. "other addresses: "
@@ -554,8 +582,8 @@ startRemoteDiscover mobile desktop = do
desktop <## "remote host 1 connecting"
mobile <## "remote controller 1 connected"
verifyRemoteCtrl mobile desktop
mobile <## "remote controller 1 session started with My desktop"
desktop <## "remote host 1 connected"
mobile <## ("remote controller 1 session started with My desktop" <> compress)
desktop <## ("remote host 1 connected" <> compress)
verifyRemoteCtrl :: TestCC -> TestCC -> IO ()
verifyRemoteCtrl mobile desktop = do
+11 -2
View File
@@ -17,7 +17,7 @@ import Data.Maybe (fromJust, isJust)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Database.SQLite.Simple (Query (..))
import Database.SQLite.Simple (Only (..), Query (..))
import Simplex.Chat.Options.SQLite (chatDBFunctions)
import Simplex.Chat.Store (createChatStore)
import qualified Simplex.Chat.Store as Store
@@ -59,6 +59,7 @@ schemaDumpTest = do
it "verify and overwrite schema dump" testVerifySchemaDump
it "verify .lint fkey-indexes" testVerifyLintFKeyIndexes
it "verify schema down migrations" testSchemaMigrations
it "verify strict tables" testVerifyStrict
testVerifySchemaDump :: IO ()
testVerifySchemaDump = withTmpFiles $ do
@@ -101,6 +102,12 @@ testSchemaMigrations = withTmpFiles $ do
schema''' <- getSchema testDB testSchema
schema''' `shouldBe` schema'
testVerifyStrict :: IO ()
testVerifyStrict = do
Right st <- createDBStore (DBOpts testDB chatDBFunctions "" False True TQOff) Store.migrations (MigrationConfig MCConsole Nothing)
withConnection st (`DB.query_` "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND sql NOT LIKE '% STRICT'")
`shouldReturn` ([] :: [Only Text])
skipComparisonForUpMigrations :: [String]
skipComparisonForUpMigrations =
[ -- schema doesn't change
@@ -132,7 +139,9 @@ skipComparisonForDownMigrations =
-- index moves down to the end of the file
"20250721_indexes",
-- indexes move down to the end of the file
"20250922_remove_unused_connections"
"20250922_remove_unused_connections",
-- group_member_intros table moves down to the end of the file
"20251128_migrate_member_relations"
]
getSchema :: FilePath -> FilePath -> IO String
+2
View File
@@ -55,7 +55,9 @@ main = do
"src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql"
#else
describe "Schema dump" schemaDumpTest
#if MIN_VERSION_base(4,18,0)
describe "Bot API docs" apiDocsTest
#endif
around tmpBracket $ describe "WebRTC encryption" webRTCTests
#endif
describe "SimpleX chat markdown" markdownTests
+1
View File
@@ -241,6 +241,7 @@
"docs-dropdown-11": "FAQ",
"docs-dropdown-12": "Security",
"docs-dropdown-14": "SimpleX for business",
"docs-dropdown-15": "Verify & reproduce builds",
"newer-version-of-eng-msg": "There is a newer version of this page in English.",
"click-to-see": "Click to see",
"menu": "Menu",
+4
View File
@@ -32,6 +32,10 @@
"title": "docs-dropdown-9",
"url": "/downloads/"
},
{
"title": "docs-dropdown-15",
"url": "/reproduce/"
},
{
"title": "docs-dropdown-10",
"url": "/transparency/"
+1
View File
@@ -28,6 +28,7 @@
"WEBRTC.md",
"XFTP-SERVER.md",
"DOWNLOADS.md",
"REPRODUCE.md",
"TRANSPARENCY.md",
"SECURITY.md",
"FAQ.md"
+15 -15
View File
@@ -21,7 +21,7 @@
<a href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank" title="Public iOS preview on TestFlight"><img class="h-[40px] w-auto" src="/img/new/google_play.svg" /></a>
<a href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid" title="SimpleX F-Droid Repository"><img class="h-[40px] w-auto" src="/img/new/f_droid.svg" /></a>
<a href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="h-[40px] w-auto" src="/img/new/testflight.png" /></a>
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
</div>
</div>
@@ -30,7 +30,7 @@
<div class="absolute mt-[-100px]">
<img class="" src="/img/new/contact_page_mobile.png" alt="">
</div>
<noscript class="z-10 flex flex-col items-center pt-[40px] ml-[-15px]">
<p class="text-2xl font-medium text-center max-w-[234px] mb-32">{{ "please-enable-javascript" | i18n({}, lang ) | safe }}</p>
</noscript>
@@ -92,7 +92,7 @@
<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px] d-none-if-js-disabled">
<div class="container px-5">
<div class="text-grey-black dark:text-white">
{# For Tablet #}
<div class="hidden md:block xl:hidden for-tablet">
<div class="contact-tab">
@@ -100,10 +100,10 @@
<h2 class="text-xl font-bold">{{ "scan-the-qr-code-with-the-simplex-chat-app" | i18n({}, lang ) | safe }}</h2>
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
</svg>
</svg>
</div>
<div class="contact-tab-content flex flex-col gap-10">
<p class="text-base mb-5">
{{ "scan-the-qr-code-with-the-simplex-chat-app-description" | i18n({}, lang ) | safe }}
@@ -112,7 +112,7 @@
</div>
</div>
</div>
{# For Desktop #}
<div class="hidden xl:block">
<div class="contact-tab">
@@ -120,17 +120,17 @@
<h2 class="text-xl font-bold">{{ "installing-simplex-chat-to-terminal" | i18n({}, lang ) | safe }}</h2>
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
</svg>
</svg>
</div>
<div class="contact-tab-content">
<p class="text-base mb-4">{{ "use-this-command" | i18n({}, lang ) | safe }}</p>
<p class="bg-white flex items-center justify-between rounded p-3 shadow-[inset_0px_2px_2px_rgba(0,0,0,0.15)] mb-8">
<span class="text-grey-black font-light text-[14px] leading-6">curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash</span>
<!-- <img class="content_copy" src="/img/new/content-copy.svg" /> -->
</p>
<p class="flex text-base font-medium mb-[76px]">{{ "see-simplex-chat" | i18n({}, lang ) | safe }} &nbsp;
<a href="" class="flex gap-1 no-underline">
<span class="text-primary-light dark:text-primary-dark underline underline-offset-4 text-base font-medium">{{ "github-repository" | i18n({}, lang ) | safe }}</span>
@@ -143,17 +143,17 @@
{{ "the-instructions--source-code" | i18n({}, lang ) | safe }}</p>
</div>
</div>
<hr class="block mb-7 dark:opacity-[0.2] w-full">
<div class="contact-tab">
<div class="flex items-center justify-between my-[40px] contact-tab-btn cursor-pointer">
<h2 class="text-xl font-bold">{{ "if-you-already-installed-simplex-chat-for-the-terminal" | i18n({}, lang ) | safe }}</h2>
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
</svg>
</svg>
</div>
<div class="contact-tab-content">
<p class="text-base font-medium mb-[46px]">{{ "if-you-already-installed" | i18n({}, lang ) | safe }}&nbsp;
<a href="" class="text-base font-medium">{{ "simplex-chat-for-the-terminal" | i18n({}, lang ) | safe }}</a>&nbsp;
+1 -1
View File
@@ -55,7 +55,7 @@
<a href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank" title="Public iOS preview on TestFlight"><img class="h-[40px] w-auto" src="/img/new/google_play.svg" /></a>
<a href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid" title="SimpleX F-Droid Repository"><img class="h-[40px] w-auto" src="/img/new/f_droid.svg" /></a>
<a href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="h-[40px] w-auto" src="/img/new/testflight.png" /></a>
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
</div>
</div>
</article>
+1 -1
View File
@@ -148,7 +148,7 @@
</a>
<ul class="sub-menu">
{% if ("docs" in page.url) or ('downloads' in page.url) or ('security' in page.url) or ('transparency' in page.url) or ('jobs' in page.url) or ('faq' in page.url) %}
{% if ("docs" in page.url) or ('downloads' in page.url) or ('security' in page.url) or ('reproduce' in page.url) or ('transparency' in page.url) or ('jobs' in page.url) or ('faq' in page.url) %}
{% for supportedLang in supportedLangsForDoc %}
{% for language in languages.languages %}
{% if language.label == supportedLang %}
@@ -32,7 +32,7 @@
<a href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank" title="Public iOS preview on TestFlight"><img class="h-[40px] w-auto" src="/img/new/google_play.svg" /></a>
<a href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid" title="SimpleX F-Droid Repository"><img class="h-[40px] w-auto" src="/img/new/f_droid.svg" /></a>
<a href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="h-[40px] w-auto" src="/img/new/testflight.png" /></a>
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
</div>
</div>
</section>
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more