From bc688014354e7c59dce2a33a3ff8305d44e75ecf Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 23 Aug 2025 19:34:41 +0100 Subject: [PATCH 001/137] docs: update FAQ (#6215) * Update FAQ.md - Fix "How do I connect to people?" button - Add group roles FAQ - Remove simplex.chat domain FAQ * Update FAQ.md * update --------- Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> --- docs/FAQ.md | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index eb68e948ee..a24f9f9c56 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,13 +1,13 @@ --- title: Frequently Asked Questions permalink: /faq/index.html -revision: 23.04.2024 +revision: 13.08.2025 --- # Frequently Asked Questions [How to use it](#how-to-use-it) -- [How do I add contacts?](#how-do-i-add-contacts) +- [How do I connect to people?](#how-do-i-connect-to-people) - [I have nobody to chat with! Where can I find any groups?](#i-have-nobody-to-chat-with-where-can-i-find-any-groups) - [What is database? What can I do with it?](#what-is-database-what-can-i-do-with-it) - [Can I send files over SimpleX? ](#can-i-send-files-over-simplex) @@ -19,7 +19,8 @@ revision: 23.04.2024 - [I want to see when my contacts read my messages](#i-want-to-see-when-my-contacts-read-my-messages) - [Can I use the same profile on desktop? Do messages sync cross-platform?](#can-i-use-the-same-profile-on-desktop-do-messages-sync-cross-platform) - [Why cannot I delete messages I sent from my contact's device?](#why-cannot-i-delete-messages-i-sent-from-my-contacts-device) -- [Why invitation links use simplex.chat domain?](#why-invitation-links-use-simplex.chat-domain) +- [What do group roles mean?](#what-do-group-roles-mean) +- [I don't want to share a web link or a QR code. How can I connect?](#i-dont-want-to-share-a-web-link-or-a-qr-code-how-can-i-connect) [Troubleshooting](#troubleshooting) - [I do not receive messages or message notifications](#i-do-not-receive-messages-or-message-notifications) @@ -50,7 +51,7 @@ revision: 23.04.2024 ### How do I connect to people? -Tap "pencil" button in the right corner, then "Create 1-time link". Share the link with the person you want to connect to. Your contact has to paste the link in the app's search bar. The link will can also be opened via the browser, once the app is installed. +Tap "pencil" button in the right corner, then "Create 1-time link". Share the link with the person you want to connect to. Your contact has to paste the link to the app's search bar. The link will can also be opened via the browser, once the app is installed. Alternatively, you can show the QR code when meeting in person or in a video call. @@ -135,13 +136,44 @@ It is also important to remember, that even if your contact enabled "Delete for When "Delete for everyone" is not enabled, you can still mark the sent message as deleted within 24 hours of sending it. In this case the recipient will see it as "deleted message", and will be able to reveal the original message. -### Why invitation links use simplex.chat domain? +### What do group roles mean? -You can replace `https://simplex.chat/` with `simplex:/` or with any other domain - the app never connect with it, ignoring it completely. It is only used to make it easier to connect for the new users who did not install the app yet. +Groups support 5 member roles: owner, admin, moderator, member and observer. -The invitation links will soon move to servers' domains. The servers already can host the pages that will be used to show QR codes. +An observer can: +- add reactions +- send reports +- talk to admins via "Chat with admins" -The link itself and the key exchange are not hosted anywhere, and the server that hosts the page to show QR code does not observe the actual connection link, because it is in the hash part of the link. The part after hash character (`#`) is not sent over the internet - the server can only see `https://simplex.chat/contact/` and the rest is processed on user's device in the browser, if you open it as a page. +A member can: +- send messages + +A moderator can: +- approve members in review ("knocking") +- moderate messages* +- block members temporarily* +- participate in "Chats with members" + +An admin can: +- add and remove members* +- change member roles*, including their own +- create group links (but the new members will join via them, so use links of admins who are always online). + +A group owner can: +- edit group profile +- delete the group + +Each role can do everything that the previous roles can, except moderators and higher cannot send reports or "Chat with admins". + +*Any actions affecting members or their messages can only be applied to members of the same or lower role (so admins can't demote owners or promote anybody to owners, etc., but they can demote other admins and themselves, or make anybody an admin). + +### I don't want to share a web link or a QR code. How can I connect? + +You can replace the server part of the link (e.g., `https://smp18.simplex.im/`) with `simplex:/` and add `?h=smp18.simplex.im` (replace it with server of your link) to the end of the link. Then it will not be possible to use it in a web browser, but it will be recognized by SimpleX Chat app. + +For example, this link: `https://smp18.simplex.im/i#E74vSxMwDnEx6DAvRCZmzBeZwwAseJUD/yVTHjaaH_EzL19DG7fvd46Mjry3IBqYT0UMo5G7l4jQ` + +becomes: `simplex:/i#E74vSxMwDnEx6DAvRCZmzBeZwwAseJUD/yVTHjaaH_EzL19DG7fvd46Mjry3IBqYT0UMo5G7l4jQ?h=smp18.simplex.im` ## Troubleshooting From 1df92b7ff4b9d617152e9fe11f04c4d1c8fb6d34 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 23 Aug 2025 19:55:43 +0100 Subject: [PATCH 002/137] docs: update downloads page (#6216) * docs: fix & update downloads - Removed non-existent Ubuntu 20.04 downloads - Added Ubuntu 24.04 downloads - Added aarch64 downloads * Update DOWNLOADS.md * update * update --------- Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> --- docs/DOWNLOADS.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index df22ff64a2..a23a920bbb 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -19,9 +19,12 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch You can link your mobile device with desktop to use the same profile remotely, but this is only possible when both devices are connected to the same local network. -**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-x86_64.AppImage) or [Flatpak](https://flathub.org/apps/chat.simplex.simplex) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb). +**Linux**: +- Most Linux distros: AppImage ([x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-x86_64.AppImage), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-aarch64.AppImage)) or [Flatpak](https://flathub.org/apps/chat.simplex.simplex). +- 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)). -**Mac**: [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-aarch64.dmg) (Apple Silicon), [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-x86_64.dmg) (Intel). +**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). @@ -35,8 +38,8 @@ You can link your mobile device with desktop to use the same profile remotely, b See [Using terminal app](/docs/CLI.md). -**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-22_04-x86-64). +**Linux**: Ubuntu 22.04 ([x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-22_04-x86_64), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-22_04-aarch64)), Ubuntu 24.04 ([x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-24_04-x86_64), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-24_04-aarch64)). -**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-macos-x86-64), aarch64 - [compile from source](/docs/CLI.md#). +**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-macos-x86-64), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-macos-aarch64). **Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-windows-x86-64). From 3454935b3bfb295bf363fe3fc48f331a6224af25 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 24 Aug 2025 10:24:00 +0100 Subject: [PATCH 003/137] core: pre- and post-start chat hooks for CLI-based bots (#6217) --- src/Simplex/Chat/Controller.hs | 10 ++++++++-- src/Simplex/Chat/Core.hs | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index eb7ed53283..998d7542b3 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -168,7 +168,13 @@ data RandomAgentServers = RandomAgentServers -- The hooks can be used to extend or customize chat core in mobile or CLI clients. data ChatHooks = ChatHooks - { -- preCmdHook can be used to process or modify the commands before they are processed. + { -- preStartHook can be used to verify some data, + -- It is called before chat controller is started, unless the core is started in maintenance mode. + preStartHook :: Maybe (ChatController -> IO ()), + -- postStartHook can be used to update some data after start (e.g. commands in bot or group profiles), + -- It is called after chat controller is started. + postStartHook :: Maybe (ChatController -> IO ()), + -- preCmdHook can be used to process or modify the commands before they are processed. -- This hook should be used to process CustomChatCommand. -- if this hook returns ChatResponse, the command processing will be skipped. preCmdHook :: Maybe (ChatController -> ChatCommand -> IO (Either (Either ChatError ChatResponse) ChatCommand)), @@ -180,7 +186,7 @@ data ChatHooks = ChatHooks } defaultChatHooks :: ChatHooks -defaultChatHooks = ChatHooks Nothing Nothing Nothing +defaultChatHooks = ChatHooks Nothing Nothing Nothing Nothing Nothing data PresetServers = PresetServers { operators :: NonEmpty PresetOperator, diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 7ad822da91..fabc45a04c 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -38,7 +38,7 @@ import Text.Read (readMaybe) import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations}, createBot, maintenance} chat = +simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations}, createBot, maintenance} chat = case logAgent of Just level -> do setLogLevel level @@ -56,13 +56,15 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {core cc <- newChatController db u_ cfg opts backgroundMode u <- maybe (createActiveUser cc createBot) pure u_ unless testView $ putStrLn $ "Current user: " <> userStr u + unless maintenance $ forM_ (preStartHook chatHooks) ($ cc) runSimplexChat opts u cc chat runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO () -runSimplexChat ChatOpts {maintenance} u cc chat +runSimplexChat ChatOpts {maintenance} u cc@ChatController {config = ChatConfig {chatHooks}} chat | maintenance = wait =<< async (chat u cc) | otherwise = do a1 <- runReaderT (startChatController True True) cc + forM_ (postStartHook chatHooks) ($ cc) a2 <- async $ chat u cc waitEither_ a1 a2 From e5300fec8425856ff36d936fcc1677c0d56eee37 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 24 Aug 2025 12:35:59 +0100 Subject: [PATCH 004/137] core: update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 05457262b7..07a0b427e8 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a2d35281b2e20022e75a4dcbecf16a7970d52e8f + tag: a2d777bda0af2a7ee7cd68952eaf7c86329427ad source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index df4ed32f05..6558a66e31 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a2d35281b2e20022e75a4dcbecf16a7970d52e8f" = "0sbq5mmsp8q4p527b286anbjidk2mlfdjsqrbiw90bc1rqwjwpm2"; + "https://github.com/simplex-chat/simplexmq.git"."a2d777bda0af2a7ee7cd68952eaf7c86329427ad" = "04h8vdxf732jwsim2fcrql47gsmv680lgg2kylgmfk4al0pnpkdk"; "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"; From 7a5e520cbe925470ddc6f3545b17fe9f65c3f35a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 24 Aug 2025 15:54:08 +0100 Subject: [PATCH 005/137] docs: simplex name and logo (#6218) * docs: simplex name and logo * update --- README.md | 2 +- docs/DIRECTORY.md | 2 +- docs/TRADEMARK.md | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 docs/TRADEMARK.md diff --git a/README.md b/README.md index dc4b9658f7..6ed444c7b5 100644 --- a/README.md +++ b/README.md @@ -442,7 +442,7 @@ Please do NOT report security vulnerabilities via GitHub issues. ## License -[AGPL v3](./LICENSE) +This software is licensed under the GNU Affero General Public License version 3 (AGPLv3). See the [LICENSE](./LICENSE) file for details. The SimpleX and SimpleX Chat name, logo, and associated branding materials are not covered by this license and are subject to the terms outlined in the [TRADEMARK](./docs/TRADEMARK.md) file. [iOS app](https://apps.apple.com/us/app/simplex-chat/id1605771084)   diff --git a/docs/DIRECTORY.md b/docs/DIRECTORY.md index a55f4fc510..9ffeeef2d2 100644 --- a/docs/DIRECTORY.md +++ b/docs/DIRECTORY.md @@ -94,7 +94,7 @@ Group owners are expected to moderate the content in the groups, if members post We reserve the right to not accept the group listing in the directory or cancel its listing, and there may be cases when we can't provide an explanation. We will certainly try to avoid it by communicating with the group owners first. -The combination of display name and full name has to be unique for the listed groups. +The combination of display name and full name has to be unique for the listed groups. If a group uses the name or logo of SimpleX, SimpleX network or SimpleX Chat it must be consistent with [Permitted Uses or SimpleX trademark](./TRADEMARK.md). Once the group is listed in the directory, the bot will invite you to join the group of the group owners, where you can send any ideas or suggestions for how the groups functionality should evolve, and help steer both the product and the policies. diff --git a/docs/TRADEMARK.md b/docs/TRADEMARK.md new file mode 100644 index 0000000000..2f8617416b --- /dev/null +++ b/docs/TRADEMARK.md @@ -0,0 +1,33 @@ +# SimpleX Chat Trademark Policy + +The SimpleX Chat name and logo are trademarks of SimpleX Chat Ltd. This policy protects the brand from confusion. All uses must follow both the context rules (permitted/prohibited) and usage guidelines below. Unauthorized uses require written permission. + +## Permitted Contexts + +- **Communities/Events**: For unofficial groups or events. +- **Addresses/Entities**: To indicate a SimpleX address, person, business, or community. +- **Publications**: In articles, blogs, videos, social media posts and presentations about SimpleX Chat or SimpleX network. +- **Software Descriptions**: To state compatibility, e.g., "Compatible with SimpleX network", in docs or descriptions (not in software name/branding). + +## Prohibited Contexts + +- **Software Branding**: As part of any software's name, branding, or logo, including forks. +- **Implied Affiliation**: Suggesting endorsement, sponsorship, or official ties without permission. +- **Commercial Activities**: In ads, merchandise, or promotions without permission. +- **Any Other**: Uses not listed as permitted. + +## Usage Guidelines + +When using in permitted contexts: +- Include "Unofficial" for communities/events. +- Use logo unchanged (shape/colors), except grayscale if needed. +- Use icon or icon with "SimpleX" word. See logos [here](../media-logos/). +- Avoid modifications or implications of official status. + +## General Terms + +We reserve all trademark rights. For permissions, contact us via email or SimpleX. We may enforce against violations. Policy updates posted here. + +## Note + +SimpleX Chat software is licensed under AGPLv3 - see [LICENSE](../LICENSE). This policy covers only trademarks, not code use. From 7f9e4cece28839bd9c0367c8e4d7b9657b7ffcac Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 24 Aug 2025 17:40:39 +0100 Subject: [PATCH 006/137] ios: update core library --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 4a6e7d3936..0ce1c25f88 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-E5t4CKBkMyT7dUtbJzIAJ5.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a */, ); path = Libraries; sourceTree = ""; From 78b4431dea3b6465ad5fc9c84d3dd13639017c72 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 25 Aug 2025 07:51:53 +0100 Subject: [PATCH 007/137] directory: use bot commands and menu in SimpleX Directory (#6219) * directory: use bot commands and menu in SimpleX Directory * rename (remove hyphen), fix test --- .../src/Directory/Options.hs | 4 +- .../src/Directory/Service.hs | 97 ++- tests/Bots/DirectoryTests.hs | 584 +++++++++--------- tests/ChatTests/Utils.hs | 3 +- 4 files changed, 363 insertions(+), 325 deletions(-) diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 2eba633609..7ad3512fe9 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -116,8 +116,8 @@ directoryOpts appDir defaultDbName = do strOption ( long "service-name" <> metavar "SERVICE_NAME" - <> help "The display name of the directory service bot, without *'s and spaces (SimpleX-Directory)" - <> value "SimpleX-Directory" + <> help "The display name of the directory service bot, without *'s and spaces (SimpleX Directory)" + <> value "SimpleX Directory" ) runCLI <- switch diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 1cbf419f41..f95b04dee1 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -8,6 +8,7 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Directory.Service ( welcomeGetOpts, @@ -59,6 +60,7 @@ import Simplex.Chat.Store.Shared (StoreError (..)) import Simplex.Chat.Terminal (terminalChatConfig) import Simplex.Chat.Terminal.Main (simplexChatCLI') import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..)) @@ -70,6 +72,7 @@ import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) +import System.Exit (exitFailure) import System.Process (readProcess) data GroupProfileUpdate @@ -144,7 +147,7 @@ directoryServiceCLI st opts = do env <- newServiceState opts eventQ <- newTQueueIO let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) - chatHooks = defaultChatHooks {eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env} + chatHooks = defaultChatHooks {postStartHook = Just postStartHook, eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env} race_ (simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing) (processEvents eventQ env) @@ -153,6 +156,34 @@ directoryServiceCLI st opts = do (cc, resp) <- atomically $ readTQueue eventQ u_ <- readTVarIO (currentUser cc) forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp + postStartHook cc = + readTVarIO (currentUser cc) >>= \case + Nothing -> putStrLn "No current user" >> exitFailure + Just User {userId, profile = p@LocalProfile {preferences}} -> do + let cmds = fromMaybe [] $ preferences >>= commands_ + unless (cmds == directoryCommands) $ do + let prefs = (fromMaybe emptyChatPrefs preferences) {files = Just FilesPreference {allow = FANo}, commands = Just directoryCommands} :: Preferences + p' = (fromLocalProfile p) {displayName = serviceName opts, peerType = Just CPTBot, preferences = Just prefs} :: Profile + liftIO $ sendChatCmd cc (APIUpdateProfile userId p') >>= \case + Right CRUserProfileUpdated {} -> putStrLn "Updated directory commands" + Right r -> putStrLn ("Error: unexpected response " <> show r) >> exitFailure + Left e -> putStrLn ("Error: " <> show e) >> exitFailure + +directoryCommands :: [ChatBotCommand] +directoryCommands = + [ CBCCommand "new" "New groups" Nothing, + CBCCommand "help" "How to submit your group" Nothing, + CBCCommand "list" "Your own groups" Nothing, + CBCMenu + "Group settings" + [ CBCCommand "role" "View new member role" idParam, + CBCCommand "filter" "Anti-spam filter" idParam, + CBCCommand "link" "View and upgrade group link" idParam, + CBCCommand "delete" "Remove a group from directory" (Just ":''") + ] + ] + where + idParam = Just "" directoryService :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> IO () directoryService st opts@DirectoryOpts {testing} env user cc = do @@ -299,11 +330,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deContactConnected ct = when (contactDirect ct) $ do logInfo $ (viewContactName ct) <> " connected" sendMessage cc ct $ - ("Welcome to " <> serviceName <> " service!\n") - <> "Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ - \For example, send _privacy_ to find groups about privacy.\n\ - \Or send */all* or */new* to list groups.\n\n\ - \Content and privacy policy: https://simplex.chat/docs/directory.html" + ("Welcome to " <> serviceName <> "!\n\n") + <> "🔍 Send search string to find groups - try _security_.\n\ + \/help - how to submit your group.\n\ + \/new - recent groups.\n\n\ + \[Directory rules](https://simplex.chat/docs/directory.html)." deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO () deGroupInvitation ct g@GroupInfo {groupProfile = p@GroupProfile {displayName}} fromMemberRole memberRole = do @@ -674,24 +705,22 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deUserCommand ct ciId = \case DCHelp DHSRegistration -> sendMessage cc ct $ - "You must be the owner to add the group to the directory:\n\ - \1. Invite " + "You must be the group owner to add it to the directory:\n\n\ + \1️⃣ *Invite* " <> serviceName - <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\ - \2. " - <> serviceName - <> " bot will create a public group link for the new members to join even when you are offline.\n\ - \3. You will then need to add this link to the group welcome message.\n\ - \4. Once the link is added, service admins will approve the group (it can take up to 48 hours), and everybody will be able to find it in directory.\n\n\ - \Start from inviting the bot to your group as admin - it will guide you through the process." + <> " bot to your group as *admin* - it will create a link for new members to join.\n\ + \2️⃣ *Add* this link to the group's welcome message.\n\ + \3️⃣ We *review* your group. Once *approved*, anybody can find it.\n\n\ + \_We usually approve within a day, except holidays_. [More details](https://simplex.chat/docs/directory.html#adding-groups-to-the-directory)." DCHelp DHSCommands -> sendMessage cc ct $ - "*/help commands* - receive this help message.\n\ - \*/help* - how to register your group to be added to directory.\n\ - \*/list* - list the groups you registered.\n\ - \*/delete :* - remove the group you submitted from directory, with _ID_ and _name_ as shown by */list* command.\n\ - \*/role * - view and set default member role for your group.\n\ - \*/filter * - view and set spam filter settings for group.\n\n\ + "/'help commands' - receive this help message.\n\ + \/help - how to register your group to be added to directory.\n\ + \/list - list the groups you registered.\n\ + \`/role ` - view and set default member role for your group.\n\ + \`/filter ` - view and set spam filter settings for group.\n\ + \`/link ` - view and upgrade group link.\n\ + \`/delete :` - remove the group you submitted from directory, with _ID_ and _name_ as shown by /list command.\n\n\ \To search for groups, send the search text." DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s DCSearchNext -> @@ -741,7 +770,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 <> " " <> strEncodeTxt anotherRole <> "' to change it.\n\n") <> onlyViaLink gLink Nothing -> sendReply $ "Error: failed reading the initial member role for the group " <> n Just mRole -> do @@ -765,16 +794,19 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName where sendSettigns n a setTo = sendReply $ - T.unlines + T.unlines $ [ "Spam filter settings for group " <> n <> setTo <> ":", "- reject long/inappropriate names: " <> showCondition (rejectNames a), "- pass captcha to join: " <> showCondition (passCaptcha a), -- "- make observer: " <> showCondition (makeObserver a) <> (if isJust (makeObserver a) then "" else " (use default set with /role command)"), - "", + "" -- "Use */filter " <> tshow gId <> " * to change spam filter level: no (disable), basic, moderate, strong.", -- "Or use */filter " <> tshow gId <> " [name[=noimage]] [captcha[=noimage]] [observer[=noimage]]* for advanced filter configuration." - "Use */filter " <> tshow gId <> " [name] [captcha]* to enable and */filter " <> tshow gId <> " off* to disable filter." ] + <> ["/'filter " <> tshow gId <> " name' - enable name filter" | isNothing (rejectNames a)] + <> ["/'filter " <> tshow gId <> " captcha' - enable captcha challenge" | isNothing (passCaptcha a)] + <> ["/'filter " <> tshow gId <> " name captcha' - enable both" | isNothing (rejectNames a) || isNothing (passCaptcha a)] + <> ["/'filter " <> tshow gId <> " off' - disable filter" | isJust (rejectNames a) || isJust (passCaptcha a)] showCondition = \case Nothing -> "_disabled_" Just PCAll -> "_enabled_" @@ -887,7 +919,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName showId = if isAdmin then tshow groupId <> ". " else "" text = showId <> groupInfoText p <> "\n" <> membersStr in (Nothing, maybe (MCText text) (\image -> MCImage {text, image}) image_) - moreMsg = (Nothing, MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s).") + moreMsg = (Nothing, MCText $ "Send /next for " <> tshow moreGroups <> " more result(s).") deAdminCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRAdmin -> IO () deAdminCommand ct ciId cmd @@ -907,11 +939,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let approved = "The group " <> userGroupReference' gr n <> " is approved" notifyOwner gr $ (approved <> " and listed in directory - please moderate it!\n") - <> "Please note: if you change the group profile it will be hidden from directory until it is re-approved.\n\n" + <> "_Please note_: if you change the group profile it will be hidden from directory until it is re-approved.\n\n" <> "Supported commands:\n" - <> ("- */filter " <> tshow ugrId <> "* - to configure anti-spam filter.\n") - <> ("- */role " <> tshow ugrId <> "* - to set default member role.\n") - <> "- */help commands* - other commands." + <> ("/'filter " <> tshow ugrId <> "' - to configure anti-spam filter.\n") + <> ("/'role " <> tshow ugrId <> "' - to set default member role.\n") + <> ("/'link " <> tshow ugrId <> "' - to view/upgrade group link.") invited <- forM ownersGroup $ \og@KnownGroup {localDisplayName = ogName} -> do inviteToOwnersGroup og gr $ \case @@ -920,7 +952,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName pure $ "Invited " <> owner <> " to owners' group " <> viewName ogName Left err -> pure err sendReply $ "Group approved!" <> maybe "" ("\n" <>) invited - notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct) <> fromMaybe "" invited + notifyOtherSuperUsers $ approved <> " by " <> viewName (localDisplayName' ct) <> maybe "" ("\n" <>) invited Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin Just GRSContactNotOwner -> replyNotApproved "user is not an owner." Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin @@ -1062,7 +1094,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName getGroupAndSummary cc user dbGroupId >>= \case Just (GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do let membersStr = "_" <> tshow currentMembers <> " members_" - text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] <> maybeToList ownerStr_ <> [membersStr, statusStr] + cmds = "/'role " <> tshow useGroupId <> "', /'filter " <> tshow useGroupId <> "'" + text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] <> maybeToList ownerStr_ <> [membersStr, statusStr, cmds] msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ sendComposedMessage cc ct Nothing msg Nothing -> do diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index a69f57cfca..3f59863f25 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -75,7 +75,7 @@ directoryServiceTests = do it "should accept some incorrect spellings" testCaptcha directoryProfile :: Profile -directoryProfile = Profile {displayName = "SimpleX-Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing} +directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing} mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> DirectoryOpts mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup = @@ -101,7 +101,7 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup = profileNameLimit = maxBound, captchaGenerator = Nothing, directoryLog = Just $ ps "directory_service.log", - serviceName = "SimpleX-Directory", + serviceName = "SimpleX Directory", runCLI = False, searchResults = 3, testing = True @@ -119,101 +119,102 @@ testDirectoryService ps = withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink - bob #> "@SimpleX-Directory privacy" - bob <# "SimpleX-Directory> > privacy" + bob #> "@'SimpleX Directory' privacy" + bob <# "'SimpleX Directory'> > privacy" bob <## " No groups found" -- putStrLn "*** create a group" bob ##> "/g PSA Privacy, Security & Anonymity" bob <## "group #PSA (Privacy, Security & Anonymity) is created" bob <## "to add members use /a PSA or /create link #PSA" - bob ##> "/a PSA SimpleX-Directory member" - bob <## "invitation to join the group #PSA sent to SimpleX-Directory" - bob <# "SimpleX-Directory> You must grant directory service admin role to register the group" - bob ##> "/mr PSA SimpleX-Directory admin" + bob ##> "/a PSA 'SimpleX Directory' member" + bob <## "invitation to join the group #PSA sent to 'SimpleX Directory'" + bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group" + bob ##> "/mr PSA 'SimpleX Directory' admin" -- putStrLn "*** discover service joins group and creates the link for profile" - bob <## "#PSA: you changed the role of SimpleX-Directory to admin" - bob <# "SimpleX-Directory> Joining the group PSA…" - bob <## "#PSA: SimpleX-Directory joined the group" - bob <# "SimpleX-Directory> Joined the group PSA, creating the link…" - bob <# "SimpleX-Directory> Created the public link to join the group via this directory service that is always online." + bob <## "#PSA: you changed the role of 'SimpleX Directory' to admin" + bob <# "'SimpleX Directory'> Joining the group PSA…" + bob <## "#PSA: 'SimpleX Directory' joined the group" + bob <# "'SimpleX Directory'> Joined the group PSA, creating the link…" + bob <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online." bob <## "" bob <## "Please add it to the group welcome message." bob <## "For example, add:" - welcomeWithLink <- dropStrPrefix "SimpleX-Directory> " . dropTime <$> getTermLine bob + welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob -- putStrLn "*** update profile without link" updateGroupProfile bob "Welcome!" - bob <# "SimpleX-Directory> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message." + bob <# "'SimpleX Directory'> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message." (superUser Thank you! The group link for ID 1 (PSA) is added to the welcome message." + bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." approvalRequested superUser welcomeWithLink (1 :: Int) -- putStrLn "*** update profile so that it still has link" let welcomeWithLink' = "Welcome! " <> welcomeWithLink updateGroupProfile bob welcomeWithLink' - bob <# "SimpleX-Directory> The group ID 1 (PSA) is updated!" + bob <# "'SimpleX Directory'> The group ID 1 (PSA) is updated!" bob <## "It is hidden from the directory until approved." - superUser <# "SimpleX-Directory> The group ID 1 (PSA) is updated." + superUser <# "'SimpleX Directory'> The group ID 1 (PSA) is updated." approvalRequested superUser welcomeWithLink' (2 :: Int) -- putStrLn "*** try approving with the old registration code" - bob #> "@SimpleX-Directory /approve 1:PSA 1" - bob <# "SimpleX-Directory> > /approve 1:PSA 1" + bob #> "@'SimpleX Directory' /approve 1:PSA 1" + bob <# "'SimpleX Directory'> > /approve 1:PSA 1" bob <## " You are not allowed to use this command" - superUser #> "@SimpleX-Directory /approve 1:PSA 1" - superUser <# "SimpleX-Directory> > /approve 1:PSA 1" + superUser #> "@'SimpleX Directory' /approve 1:PSA 1" + superUser <# "'SimpleX Directory'> > /approve 1:PSA 1" superUser <## " Incorrect approval code" -- putStrLn "*** update profile so that it has no link" updateGroupProfile bob "Welcome!" - bob <# "SimpleX-Directory> The group link for ID 1 (PSA) is removed from the welcome message." + bob <# "'SimpleX Directory'> The group link for ID 1 (PSA) is removed from the welcome message." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." - superUser <# "SimpleX-Directory> The group link is removed from ID 1 (PSA), de-listed." - superUser #> "@SimpleX-Directory /approve 1:PSA 2" - superUser <# "SimpleX-Directory> > /approve 1:PSA 2" + superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (PSA), de-listed." + superUser #> "@'SimpleX Directory' /approve 1:PSA 2" + superUser <# "'SimpleX Directory'> > /approve 1:PSA 2" superUser <## " Error: the group ID 1 (PSA) is not pending approval." -- putStrLn "*** update profile so that it has link again" updateGroupProfile bob welcomeWithLink' - bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message." + bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (PSA) is added to the welcome message." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." approvalRequested superUser welcomeWithLink' (1 :: Int) - superUser #> "@SimpleX-Directory /pending" - superUser <# "SimpleX-Directory> > /pending" + superUser #> "@'SimpleX Directory' /pending" + superUser <# "'SimpleX Directory'> > /pending" superUser <## " 1 registered group(s)" - superUser <# "SimpleX-Directory> 1. PSA (Privacy, Security & Anonymity)" + superUser <# "'SimpleX Directory'> 1. PSA (Privacy, Security & Anonymity)" superUser <## "Welcome message:" superUser <##. "Welcome! Link to join the group PSA: " superUser <## "Owner: bob" superUser <## "2 members" superUser <## "Status: pending admin approval" - superUser #> "@SimpleX-Directory /approve 1:PSA 1" - superUser <# "SimpleX-Directory> > /approve 1:PSA 1" + superUser <## "/'role 1', /'filter 1'" + superUser #> "@'SimpleX Directory' /approve 1:PSA 1" + superUser <# "'SimpleX Directory'> > /approve 1:PSA 1" superUser <## " Group approved!" - bob <# "SimpleX-Directory> The group ID 1 (PSA) is approved and listed in directory - please moderate it!" + bob <# "'SimpleX Directory'> The group ID 1 (PSA) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." bob <## "" bob <## "Supported commands:" - bob <## "- /filter 1 - to configure anti-spam filter." - bob <## "- /role 1 - to set default member role." - bob <## "- /help commands - other commands." + bob <## "/'filter 1' - to configure anti-spam filter." + bob <## "/'role 1' - to set default member role." + bob <## "/'link 1' - to view/upgrade group link." search bob "privacy" welcomeWithLink' search bob "security" welcomeWithLink' cath `connectVia` dsLink search cath "privacy" welcomeWithLink' - bob #> "@SimpleX-Directory /exec /contacts" - bob <# "SimpleX-Directory> > /exec /contacts" + bob #> "@'SimpleX Directory' /exec /contacts" + bob <# "'SimpleX Directory'> > /exec /contacts" bob <## " You are not allowed to use this command" - superUser #> "@SimpleX-Directory /exec /contacts" - superUser <# "SimpleX-Directory> > /exec /contacts" + superUser #> "@'SimpleX Directory' /exec /contacts" + superUser <# "'SimpleX Directory'> > /exec /contacts" superUser <## " alice (Alice)" superUser <## "bob (Bob)" superUser <## "cath (Catherine)" where search u s welcome = do - u #> ("@SimpleX-Directory " <> s) - u <# ("SimpleX-Directory> > " <> s) + u #> ("@'SimpleX Directory' " <> s) + u <# ("'SimpleX Directory'> > " <> s) u <## " Found 1 group(s)." - u <# "SimpleX-Directory> PSA (Privacy, Security & Anonymity)" + u <# "'SimpleX Directory'> PSA (Privacy, Security & Anonymity)" u <## "Welcome message:" u <## welcome u <## "2 members" @@ -222,14 +223,14 @@ testDirectoryService ps = u <## "welcome message changed to:" u <## welcome approvalRequested su welcome grId = do - su <# "SimpleX-Directory> bob submitted the group ID 1:" + su <# "'SimpleX Directory'> bob submitted the group ID 1:" su <## "PSA (Privacy, Security & Anonymity)" su <## "Welcome message:" su <## welcome su <## "2 members" su <## "" su <## "To approve send:" - su <# ("SimpleX-Directory> /approve 1:PSA " <> show grId) + su <# ("'SimpleX Directory'> /approve 1:PSA " <> show grId) testSuspendResume :: HasCallStack => TestParams -> IO () testSuspendResume ps = @@ -238,26 +239,26 @@ testSuspendResume ps = bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" groupFound bob "privacy" - superUser #> "@SimpleX-Directory /suspend 1:privacy" - superUser <# "SimpleX-Directory> > /suspend 1:privacy" + superUser #> "@'SimpleX Directory' /suspend 1:privacy" + superUser <# "'SimpleX Directory'> > /suspend 1:privacy" superUser <## " Group suspended!" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is suspended and hidden from directory. Please contact the administrators." + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is suspended and hidden from directory. Please contact the administrators." groupNotFound bob "privacy" - superUser #> "@SimpleX-Directory /resume 1:privacy" - superUser <# "SimpleX-Directory> > /resume 1:privacy" + superUser #> "@'SimpleX Directory' /resume 1:privacy" + superUser <# "'SimpleX Directory'> > /resume 1:privacy" superUser <## " Group listing resumed!" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is listed in the directory again!" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is listed in the directory again!" groupFound bob "privacy" - superUser #> "@SimpleX-Directory privacy" + superUser #> "@'SimpleX Directory' privacy" groupFoundN_ "" (Just 1) 2 superUser "privacy" - superUser #> "@SimpleX-Directory /link 1:privacy" - superUser <# "SimpleX-Directory> > /link 1:privacy" + superUser #> "@'SimpleX Directory' /link 1:privacy" + superUser <# "'SimpleX Directory'> > /link 1:privacy" superUser <## " The link to join the group ID 1 (privacy):" superUser <##. "https://localhost/g#" superUser <## "New member role: member" -- get and change the link to the equivalent - should not ask to re-approve - bob #> "@SimpleX-Directory /link 1" - bob <# "SimpleX-Directory> > /link 1" + bob #> "@'SimpleX Directory' /link 1" + bob <# "'SimpleX Directory'> > /link 1" bob <## " The link to join the group ID 1 (privacy):" gLink <- getTermLine bob gLink `shouldStartWith` "https://localhost/g#" @@ -268,38 +269,38 @@ testSuspendResume ps = bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink <> "?same_link=true") bob <## "welcome message changed to:" bob <## ("Link to join the group privacy: " <> gLink <> "?same_link=true") - bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!" bob <## "The group is listed in directory." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated - only link or whitespace changes." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes." superUser <## "The group remained listed in directory." -- upgrade link -- make it upgradeable first - superUser #> "@SimpleX-Directory /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL" - superUser <# "SimpleX-Directory> > /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL" + superUser #> "@'SimpleX Directory' /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL" + superUser <# "'SimpleX Directory'> > /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL" superUser <## "" - bob #> "@SimpleX-Directory /link 1" - bob <# "SimpleX-Directory> > /link 1" + bob #> "@'SimpleX Directory' /link 1" + bob <# "'SimpleX Directory'> > /link 1" bob <## " The link to join the group ID 1 (privacy):" bob <##. "https://simplex.chat/contact#/" bob <## "New member role: member" bob <## "The link is being upgraded..." - bob <# "SimpleX-Directory> Please replace the old link in welcome message of your group ID 1 (privacy)" + bob <# "'SimpleX Directory'> Please replace the old link in welcome message of your group ID 1 (privacy)" bob <## "If this is the only change, the group will remain listed in directory without re-approval." bob <## "" bob <## "The new link:" - gLink' <- dropStrPrefix "SimpleX-Directory> " . dropTime <$> getTermLine bob + gLink' <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink') bob <## "welcome message changed to:" bob <## ("Link to join the group privacy: " <> gLink') - bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!" bob <## "The group is listed in directory." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated - only link or whitespace changes." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated - only link or whitespace changes." superUser <## "The group remained listed in directory." -- send message to group owner - superUser #> "@SimpleX-Directory /owner 1:privacy hello there" - superUser <# "SimpleX-Directory> > /owner 1:privacy hello there" + superUser #> "@'SimpleX Directory' /owner 1:privacy hello there" + superUser <# "'SimpleX Directory'> > /owner 1:privacy hello there" superUser <## " Forwarded to @bob, the owner of the group ID 1 (privacy)" - bob <# "SimpleX-Directory> hello there" + bob <# "'SimpleX Directory'> hello there" testDeleteGroup :: HasCallStack => TestParams -> IO () testDeleteGroup ps = @@ -308,8 +309,8 @@ testDeleteGroup ps = bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" groupFound bob "privacy" - bob #> "@SimpleX-Directory /delete 1:privacy" - bob <# "SimpleX-Directory> > /delete 1:privacy" + bob #> "@'SimpleX Directory' /delete 1:privacy" + bob <# "'SimpleX Directory'> > /delete 1:privacy" bob <## " Your group privacy is deleted from the directory" groupNotFound bob "privacy" @@ -326,19 +327,19 @@ testDeleteGroupAdmin ps = groupFound bob "security" listUserGroup bob "privacy" "Privacy" listUserGroup cath "security" "Security" - superUser #> "@SimpleX-Directory /last" - superUser <# "SimpleX-Directory> > /last" + superUser #> "@'SimpleX Directory' /last" + superUser <# "'SimpleX Directory'> > /last" superUser <## " 2 registered group(s)" memberGroupListing superUser bob 1 "privacy" "Privacy" 2 "active" memberGroupListing superUser cath 2 "security" "Security" 2 "active" -- trying to register group with the same name submitGroup bob "security" "Security" - bob <# "SimpleX-Directory> The group security (Security) is already listed in the directory, please choose another name." + bob <# "'SimpleX Directory'> The group security (Security) is already listed in the directory, please choose another name." bob ##> "/d #security" bob <## "#security: you deleted the group" -- admin can delete the group - superUser #> "@SimpleX-Directory /delete 2:security" - superUser <# "SimpleX-Directory> > /delete 2:security" + superUser #> "@'SimpleX Directory' /delete 2:security" + superUser <# "'SimpleX Directory'> > /delete 2:security" superUser <## " The group security is deleted from the directory" groupFound cath "privacy" listUserGroup bob "privacy" "Privacy" @@ -355,8 +356,8 @@ testSetRole ps = bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" groupFound bob "privacy" - bob #> "@SimpleX-Directory /role 1:privacy observer" - bob <# "SimpleX-Directory> > /role 1:privacy observer" + bob #> "@'SimpleX Directory' /role 1:privacy observer" + bob <# "'SimpleX Directory'> > /role 1:privacy observer" bob <## " The initial member role for the group privacy is set to observer" bob <## "" note <- getTermLine bob @@ -365,13 +366,13 @@ testSetRole ps = cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group" - cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://localhost/g#" + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://localhost/g#" cath <## "#privacy: member bob (Bob) is connected" - bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" bob ##> "/ms #privacy" bob <## "bob (Bob): owner, you, created group" - bob <## "SimpleX-Directory: admin, invited, connected" + bob <## "'SimpleX Directory': admin, invited, connected" bob <## "cath (Catherine): observer, connected" cath ##> "#privacy hello" cath <## "#privacy: you don't have permission to send messages" @@ -385,10 +386,10 @@ testJoinGroup ps = bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" cath `connectVia` dsLink - cath #> "@SimpleX-Directory privacy" - cath <# "SimpleX-Directory> > privacy" + cath #> "@'SimpleX Directory' privacy" + cath <# "'SimpleX Directory'> > privacy" cath <## " Found 1 group(s)." - cath <# "SimpleX-Directory> privacy (Privacy)" + cath <# "'SimpleX Directory'> privacy (Privacy)" cath <## "Welcome message:" welcomeMsg <- getTermLine cath let groupLink = dropStrPrefix "Link to join the group privacy: " welcomeMsg @@ -397,11 +398,11 @@ testJoinGroup ps = cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group" - cath <## "contact and member are merged: SimpleX-Directory, #privacy SimpleX-Directory_1" - cath <## "use @SimpleX-Directory to send messages" - cath <# ("#privacy SimpleX-Directory> " <> welcomeMsg) + cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" + cath <## "use @'SimpleX Directory' to send messages" + cath <# ("#privacy 'SimpleX Directory'> " <> welcomeMsg) cath <## "#privacy: member bob (Bob) is connected" - bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" bob ##> "/create link #privacy" bobLink <- getGroupLink bob "privacy" GRMember True @@ -416,7 +417,7 @@ testJoinGroup ps = dan <## "#privacy: you joined the group" dan <# ("#privacy bob> " <> welcomeMsg) dan - <### [ "#privacy: member SimpleX-Directory is connected", + <### [ "#privacy: member 'SimpleX Directory' is connected", "#privacy: member cath (Catherine) is connected" ], do @@ -431,15 +432,15 @@ testGroupNameWithSpaces ps = bob `connectVia` dsLink registerGroup superUser bob "Privacy & Security" "" groupFound bob "Privacy & Security" - superUser #> "@SimpleX-Directory /suspend 1:'Privacy & Security'" - superUser <# "SimpleX-Directory> > /suspend 1:'Privacy & Security'" + superUser #> "@'SimpleX Directory' /suspend 1:'Privacy & Security'" + superUser <# "'SimpleX Directory'> > /suspend 1:'Privacy & Security'" superUser <## " Group suspended!" - bob <# "SimpleX-Directory> The group ID 1 (Privacy & Security) is suspended and hidden from directory. Please contact the administrators." + bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is suspended and hidden from directory. Please contact the administrators." groupNotFound bob "privacy" - superUser #> "@SimpleX-Directory /resume 1:'Privacy & Security'" - superUser <# "SimpleX-Directory> > /resume 1:'Privacy & Security'" + superUser #> "@'SimpleX Directory' /resume 1:'Privacy & Security'" + superUser <# "'SimpleX Directory'> > /resume 1:'Privacy & Security'" superUser <## " Group listing resumed!" - bob <# "SimpleX-Directory> The group ID 1 (Privacy & Security) is listed in the directory again!" + bob <# "'SimpleX Directory'> The group ID 1 (Privacy & Security) is listed in the directory again!" groupFound bob "Privacy & Security" testSearchGroups :: HasCallStack => TestParams -> IO () @@ -453,58 +454,58 @@ testSearchGroups ps = connectUsers bob cath fullAddMember "MyGroup" "" bob cath GRMember joinGroup "MyGroup" cath bob - cath <## "#MyGroup: member SimpleX-Directory_1 is connected" - cath <## "contact and member are merged: SimpleX-Directory, #MyGroup SimpleX-Directory_1" - cath <## "use @SimpleX-Directory to send messages" - cath #> "@SimpleX-Directory MyGroup" - cath <# "SimpleX-Directory> > MyGroup" + cath <## "#MyGroup: member 'SimpleX Directory_1' is connected" + cath <## "contact and member are merged: 'SimpleX Directory', #MyGroup 'SimpleX Directory_1'" + cath <## "use @'SimpleX Directory' to send messages" + cath #> "@'SimpleX Directory' MyGroup" + cath <# "'SimpleX Directory'> > MyGroup" cath <## " Found 7 group(s), sending top 3." receivedGroup cath 0 3 receivedGroup cath 1 2 receivedGroup cath 2 2 - cath <# "SimpleX-Directory> Send /next or just . for 4 more result(s)." - cath #> "@SimpleX-Directory /next" - cath <# "SimpleX-Directory> > /next" + cath <# "'SimpleX Directory'> Send /next for 4 more result(s)." + cath #> "@'SimpleX Directory' /next" + cath <# "'SimpleX Directory'> > /next" cath <## " Sending 3 more group(s)." receivedGroup cath 3 2 receivedGroup cath 4 2 receivedGroup cath 5 2 - cath <# "SimpleX-Directory> Send /next or just . for 1 more result(s)." + cath <# "'SimpleX Directory'> Send /next for 1 more result(s)." -- search of another user does not affect the search of the first user groupFound bob "Another" - cath #> "@SimpleX-Directory ." - cath <# "SimpleX-Directory> > ." + cath #> "@'SimpleX Directory' ." + cath <# "'SimpleX Directory'> > ." cath <## " Sending 1 more group(s)." receivedGroup cath 6 2 - cath #> "@SimpleX-Directory /all" - cath <# "SimpleX-Directory> > /all" + cath #> "@'SimpleX Directory' /all" + cath <# "'SimpleX Directory'> > /all" cath <## " 8 group(s) listed, sending top 3." receivedGroup cath 0 3 receivedGroup cath 1 2 receivedGroup cath 2 2 - cath <# "SimpleX-Directory> Send /next or just . for 5 more result(s)." - cath #> "@SimpleX-Directory /new" - cath <# "SimpleX-Directory> > /new" + cath <# "'SimpleX Directory'> Send /next for 5 more result(s)." + cath #> "@'SimpleX Directory' /new" + cath <# "'SimpleX Directory'> > /new" cath <## " 8 group(s) listed, sending the most recent 3." receivedGroup cath 7 2 receivedGroup cath 6 2 receivedGroup cath 5 2 - cath <# "SimpleX-Directory> Send /next or just . for 5 more result(s)." - cath #> "@SimpleX-Directory term3" - cath <# "SimpleX-Directory> > term3" + cath <# "'SimpleX Directory'> Send /next for 5 more result(s)." + cath #> "@'SimpleX Directory' term3" + cath <# "'SimpleX Directory'> > term3" cath <## " Found 3 group(s)." receivedGroup cath 4 2 receivedGroup cath 5 2 receivedGroup cath 6 2 - cath #> "@SimpleX-Directory term1" - cath <# "SimpleX-Directory> > term1" + cath #> "@'SimpleX Directory' term1" + cath <# "'SimpleX Directory'> > term1" cath <## " Found 6 group(s), sending top 3." receivedGroup cath 1 2 receivedGroup cath 2 2 receivedGroup cath 3 2 - cath <# "SimpleX-Directory> Send /next or just . for 3 more result(s)." - cath #> "@SimpleX-Directory ." - cath <# "SimpleX-Directory> > ." + cath <# "'SimpleX Directory'> Send /next for 3 more result(s)." + cath #> "@'SimpleX Directory' ." + cath <# "'SimpleX Directory'> > ." cath <## " Sending 3 more group(s)." receivedGroup cath 4 2 receivedGroup cath 5 2 @@ -523,7 +524,7 @@ testSearchGroups ps = ] receivedGroup :: TestCC -> Int -> Int -> IO () receivedGroup u ix count = do - u <#. ("SimpleX-Directory> " <> groups !! ix) + u <#. ("'SimpleX Directory'> " <> groups !! ix) u <## "Welcome message:" u <##. "Link to join the group " u <## (show count <> " members") @@ -534,13 +535,13 @@ testInviteToOwnersGroup ps = withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> do bob `connectVia` dsLink registerGroupId superUser bob "privacy" "Privacy" 2 1 - bob <## "#owners: SimpleX-Directory invites you to join the group as member" + bob <## "#owners: 'SimpleX Directory' invites you to join the group as member" bob <## "use /j owners to accept" superUser <## "Invited @bob, the owner of the group ID 2 (privacy) to owners' group owners" bob ##> "/j owners" bob <## "#owners: you joined the group" bob <## "#owners: member alice (Alice) is connected" - superUser <## "#owners: SimpleX-Directory added bob (Bob) to the group (connecting...)" + superUser <## "#owners: 'SimpleX Directory' added bob (Bob) to the group (connecting...)" superUser <## "#owners: new member bob is connected" -- second group registerGroupId superUser bob "security" "Security" 3 2 @@ -556,13 +557,13 @@ testDelistedOwnerLeaves ps = addCathAsOwner bob cath leaveGroup "privacy" bob cath <## "#privacy: bob left the group" - bob <# "SimpleX-Directory> You left the group ID 1 (privacy)." + bob <# "'SimpleX Directory'> You left the group ID 1 (privacy)." bob <## "" bob <## "The group is no longer listed in the directory." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (group owner left)." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner left)." cath `connectVia` dsLink - cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" - cath <## "use @SimpleX-Directory to send messages" + cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" + cath <## "use @'SimpleX Directory' to send messages" groupNotFound cath "privacy" testDelistedOwnerRemoved :: HasCallStack => TestParams -> IO () @@ -574,13 +575,13 @@ testDelistedOwnerRemoved ps = registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath removeMember "privacy" cath bob - bob <# "SimpleX-Directory> You are removed from the group ID 1 (privacy)." + bob <# "'SimpleX Directory'> You are removed from the group ID 1 (privacy)." bob <## "" bob <## "The group is no longer listed in the directory." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (group owner is removed)." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group owner is removed)." cath `connectVia` dsLink - cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" - cath <## "use @SimpleX-Directory to send messages" + cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" + cath <## "use @'SimpleX Directory' to send messages" groupNotFound cath "privacy" testNotDelistedMemberLeaves :: HasCallStack => TestParams -> IO () @@ -595,7 +596,7 @@ testNotDelistedMemberLeaves ps = bob <## "#privacy: cath left the group" (superUser "@SimpleX-Directory_1 privacy" + cath #> "@'SimpleX Directory_1' privacy" groupFoundN_ "_1" Nothing 2 cath "privacy" testNotDelistedMemberRemoved :: HasCallStack => TestParams -> IO () @@ -609,7 +610,7 @@ testNotDelistedMemberRemoved ps = removeMember "privacy" bob cath (superUser "@SimpleX-Directory_1 privacy" + cath #> "@'SimpleX Directory_1' privacy" groupFoundN_ "_1" Nothing 2 cath "privacy" testDelistedServiceRemoved :: HasCallStack => TestParams -> IO () @@ -620,13 +621,13 @@ testDelistedServiceRemoved ps = bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath - bob ##> "/rm #privacy SimpleX-Directory" - bob <## "#privacy: you removed SimpleX-Directory from the group" - cath <## "#privacy: bob removed SimpleX-Directory from the group" - bob <# "SimpleX-Directory> SimpleX-Directory is removed from the group ID 1 (privacy)." + bob ##> "/rm #privacy 'SimpleX Directory'" + bob <## "#privacy: you removed 'SimpleX Directory' from the group" + cath <## "#privacy: bob removed 'SimpleX Directory' from the group" + bob <# "'SimpleX Directory'> SimpleX Directory is removed from the group ID 1 (privacy)." bob <## "" bob <## "The group is no longer listed in the directory." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (directory service is removed)." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (directory service is removed)." cath `connectVia` dsLink groupNotFound_ "_1" cath "privacy" @@ -641,17 +642,17 @@ testDelistedGroupDeleted ps = connectUsers bob cath fullAddMember "privacy" "Privacy" bob cath GROwner joinGroup "privacy" cath bob - cath <## "#privacy: member SimpleX-Directory_1 is connected" - cath <## "contact and member are merged: SimpleX-Directory, #privacy SimpleX-Directory_1" - cath <## "use @SimpleX-Directory to send messages" + cath <## "#privacy: member 'SimpleX Directory_1' is connected" + cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" + cath <## "use @'SimpleX Directory' to send messages" bob ##> "/d #privacy" bob <## "#privacy: you deleted the group" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is deleted." + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is deleted." bob <## "" bob <## "The group is no longer listed in the directory." cath <## "#privacy: bob deleted the group" cath <## "use /d #privacy to delete the local copy of the group" - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (group is deleted)." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (group is deleted)." groupNotFound cath "privacy" testDelistedRoleChanges :: HasCallStack => TestParams -> IO () @@ -663,44 +664,44 @@ testDelistedRoleChanges ps = registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath `connectVia` dsLink - cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" - cath <## "use @SimpleX-Directory to send messages" + cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" + cath <## "use @'SimpleX Directory' to send messages" groupFoundN 3 cath "privacy" -- de-listed if service role changed - bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory to member" - cath <## "#privacy: bob changed the role of SimpleX-Directory from admin to member" - bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to member." + bob ##> "/mr privacy 'SimpleX Directory' member" + bob <## "#privacy: you changed the role of 'SimpleX Directory' to member" + cath <## "#privacy: bob changed the role of 'SimpleX Directory' from admin to member" + bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to member." bob <## "" bob <## "The group is no longer listed in the directory." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (SimpleX-Directory role is changed to member)." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (SimpleX Directory role is changed to member)." groupNotFound cath "privacy" -- re-listed if service role changed back without profile changes - cath ##> "/mr privacy SimpleX-Directory admin" - cath <## "#privacy: you changed the role of SimpleX-Directory to admin" - bob <## "#privacy: cath changed the role of SimpleX-Directory from member to admin" - bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." + cath ##> "/mr privacy 'SimpleX Directory' admin" + cath <## "#privacy: you changed the role of 'SimpleX Directory' to admin" + bob <## "#privacy: cath changed the role of 'SimpleX Directory' from member to admin" + bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is listed in the directory again." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is listed (SimpleX-Directory role is changed to admin)." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (SimpleX Directory role is changed to admin)." groupFoundN 3 cath "privacy" -- de-listed if owner role changed cath ##> "/mr privacy bob admin" cath <## "#privacy: you changed the role of bob to admin" bob <## "#privacy: cath changed your role from owner to admin" - bob <# "SimpleX-Directory> Your role in the group ID 1 (privacy) is changed to admin." + bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is no longer listed in the directory." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (user role is set to admin)." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is de-listed (user role is set to admin)." groupNotFound cath "privacy" -- re-listed if owner role changed back without profile changes cath ##> "/mr privacy bob owner" cath <## "#privacy: you changed the role of bob to owner" bob <## "#privacy: cath changed your role from admin to owner" - bob <# "SimpleX-Directory> Your role in the group ID 1 (privacy) is changed to owner." + bob <# "'SimpleX Directory'> Your role in the group ID 1 (privacy) is changed to owner." bob <## "" bob <## "The group is listed in the directory again." - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is listed (user role is set to owner)." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is listed (user role is set to owner)." groupFoundN 3 cath "privacy" testNotDelistedMemberRoleChanged :: HasCallStack => TestParams -> IO () @@ -712,8 +713,8 @@ testNotDelistedMemberRoleChanged ps = registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath `connectVia` dsLink - cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" - cath <## "use @SimpleX-Directory to send messages" + cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" + cath <## "use @'SimpleX Directory' to send messages" groupFoundN 3 cath "privacy" bob ##> "/mr privacy cath member" bob <## "#privacy: you changed the role of cath to member" @@ -729,13 +730,13 @@ testNotSentApprovalBadRoles ps = cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" welcomeWithLink <- groupAccepted bob "privacy" - bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory to member" + bob ##> "/mr privacy 'SimpleX Directory' member" + bob <## "#privacy: you changed the role of 'SimpleX Directory' to member" updateProfileWithLink bob "privacy" welcomeWithLink 1 - bob <# "SimpleX-Directory> You must grant directory service admin role to register the group" - bob ##> "/mr privacy SimpleX-Directory admin" - bob <## "#privacy: you changed the role of SimpleX-Directory to admin" - bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." + bob <# "'SimpleX Directory'> You must grant directory service admin role to register the group" + bob ##> "/mr privacy 'SimpleX Directory' admin" + bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin" + bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is submitted for approval." notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 @@ -754,16 +755,16 @@ testNotApprovedBadRoles ps = welcomeWithLink <- groupAccepted bob "privacy" updateProfileWithLink bob "privacy" welcomeWithLink 1 notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 - bob ##> "/mr privacy SimpleX-Directory member" - bob <## "#privacy: you changed the role of SimpleX-Directory to member" + bob ##> "/mr privacy 'SimpleX Directory' member" + bob <## "#privacy: you changed the role of 'SimpleX Directory' to member" let approve = "/approve 1:privacy 1" - superUser #> ("@SimpleX-Directory " <> approve) - superUser <# ("SimpleX-Directory> > " <> approve) - superUser <## " Group is not approved: SimpleX-Directory is not an admin." + superUser #> ("@'SimpleX Directory' " <> approve) + superUser <# ("'SimpleX Directory'> > " <> approve) + superUser <## " Group is not approved: SimpleX Directory is not an admin." groupNotFound cath "privacy" - bob ##> "/mr privacy SimpleX-Directory admin" - bob <## "#privacy: you changed the role of SimpleX-Directory to admin" - bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." + bob ##> "/mr privacy 'SimpleX Directory' admin" + bob <## "#privacy: you changed the role of 'SimpleX Directory' to admin" + bob <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (privacy) is changed to admin." bob <## "" bob <## "The group is submitted for approval." notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 @@ -780,15 +781,15 @@ testRegOwnerChangedProfile ps = addCathAsOwner bob cath bob ##> "/gp privacy privacy Privacy and Security" bob <## "description changed to: Privacy and Security" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!" bob <## "It is hidden from the directory until approved." cath <## "bob updated group #privacy:" cath <## "description changed to: Privacy and Security" cath `connectVia` dsLink - cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" - cath <## "use @SimpleX-Directory to send messages" + cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" + cath <## "use @'SimpleX Directory' to send messages" groupNotFound cath "privacy" - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" @@ -801,16 +802,16 @@ testAnotherOwnerChangedProfile ps = registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath `connectVia` dsLink - cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" - cath <## "use @SimpleX-Directory to send messages" + cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" + cath <## "use @'SimpleX Directory' to send messages" cath ##> "/gp privacy privacy Privacy and Security" cath <## "description changed to: Privacy and Security" bob <## "cath updated group #privacy:" bob <## "description changed to: Privacy and Security" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath!" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!" bob <## "It is hidden from the directory until approved." groupNotFound cath "privacy" - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" @@ -828,10 +829,10 @@ testNotConnectedOwnerChangedProfile ps = cath <## "description changed to: Privacy and Security" bob <## "cath updated group #privacy:" bob <## "description changed to: Privacy and Security" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath!" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath!" bob <## "It is hidden from the directory until approved." groupNotFound dan "privacy" - superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated by cath." + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated by cath." reapproveGroup 3 superUser bob groupFoundN 3 dan "privacy" @@ -849,22 +850,22 @@ testRegOwnerRemovedLink ps = bob ##> "/set welcome #privacy Welcome!" bob <## "welcome message changed to:" bob <## "Welcome!" - bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message." + bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." cath <## "bob updated group #privacy:" cath <## "welcome message changed to:" cath <## "Welcome!" - superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." + superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed." cath `connectVia` dsLink - cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" - cath <## "use @SimpleX-Directory to send messages" + cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" + cath <## "use @'SimpleX Directory' to send messages" groupNotFound cath "privacy" let withChangedLink = T.unpack $ T.replace "contact#/?v=2-7&" "contact#/?v=3-7&" $ T.pack welcomeWithLink bob ##> ("/set welcome #privacy " <> withChangedLink) bob <## "welcome message changed to:" bob <## withChangedLink - bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." + bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." cath <## "bob updated group #privacy:" cath <## "welcome message changed to:" @@ -881,8 +882,8 @@ testAnotherOwnerRemovedLink ps = registerGroup superUser bob "privacy" "Privacy" addCathAsOwner bob cath cath `connectVia` dsLink - cath <## "contact and member are merged: SimpleX-Directory_1, #privacy SimpleX-Directory" - cath <## "use @SimpleX-Directory to send messages" + cath <## "contact and member are merged: 'SimpleX Directory_1', #privacy 'SimpleX Directory'" + cath <## "use @'SimpleX Directory' to send messages" bob ##> "/show welcome #privacy" bob <## "Welcome message:" welcomeWithLink <- getTermLine bob @@ -892,10 +893,10 @@ testAnotherOwnerRemovedLink ps = bob <## "cath updated group #privacy:" bob <## "welcome message changed to:" bob <## "Welcome!" - bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message by cath." + bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." - superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." + superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed." groupNotFound cath "privacy" cath ##> ("/set welcome #privacy " <> welcomeWithLink) cath <## "welcome message changed to:" @@ -903,7 +904,7 @@ testAnotherOwnerRemovedLink ps = bob <## "cath updated group #privacy:" bob <## "welcome message changed to:" bob <## welcomeWithLink - bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." + bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." reapproveGroup 3 superUser bob groupFoundN 3 cath "privacy" @@ -927,10 +928,10 @@ testNotConnectedOwnerRemovedLink ps = bob <## "cath updated group #privacy:" bob <## "welcome message changed to:" bob <## "Welcome!" - bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message by cath." + bob <# "'SimpleX Directory'> The group link for ID 1 (privacy) is removed from the welcome message by cath." bob <## "" bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." - superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." + superUser <# "'SimpleX Directory'> The group link is removed from ID 1 (privacy), de-listed." groupNotFound dan "privacy" cath ##> ("/set welcome #privacy " <> welcomeWithLink) cath <## "welcome message changed to:" @@ -938,13 +939,13 @@ testNotConnectedOwnerRemovedLink ps = bob <## "cath updated group #privacy:" bob <## "welcome message changed to:" bob <## welcomeWithLink - -- bob <# "SimpleX-Directory> The group link is added by another group member, your registration will not be processed." + -- bob <# "'SimpleX Directory'> The group link is added by another group member, your registration will not be processed." -- bob <## "" -- bob <## "Please update the group profile yourself." -- bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!") -- bob <## "welcome message changed to:" -- bob <## (welcomeWithLink <> " - welcome!") - bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." + bob <# "'SimpleX Directory'> Thank you! The group link for ID 1 (privacy) is added to the welcome message by cath." bob <## "You will be notified once the group is added to the directory - it may take up to 48 hours." -- cath <## "bob updated group #privacy:" -- cath <## "welcome message changed to:" @@ -962,10 +963,10 @@ testDuplicateAskConfirmation ps = _ <- groupAccepted bob "privacy" cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" - cath <# "SimpleX-Directory> The group privacy (Privacy) is already submitted to the directory." + cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" - cath <# "SimpleX-Directory> /confirm 1:privacy" - cath #> "@SimpleX-Directory /confirm 1:privacy" + cath <# "'SimpleX Directory'> /confirm 1:privacy" + cath #> "@'SimpleX Directory' /confirm 1:privacy" welcomeWithLink <- groupAccepted cath "privacy" groupNotFound bob "privacy" completeRegistration superUser cath "privacy" "Privacy" welcomeWithLink 2 @@ -981,7 +982,7 @@ testDuplicateProhibitRegistration ps = cath `connectVia` dsLink groupFound cath "privacy" _ <- submitGroup cath "privacy" "Privacy" - cath <# "SimpleX-Directory> The group privacy (Privacy) is already listed in the directory, please choose another name." + cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name." testDuplicateProhibitConfirmation :: HasCallStack => TestParams -> IO () testDuplicateProhibitConfirmation ps = @@ -993,14 +994,14 @@ testDuplicateProhibitConfirmation ps = welcomeWithLink <- groupAccepted bob "privacy" cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" - cath <# "SimpleX-Directory> The group privacy (Privacy) is already submitted to the directory." + cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" - cath <# "SimpleX-Directory> /confirm 1:privacy" + cath <# "'SimpleX Directory'> /confirm 1:privacy" groupNotFound cath "privacy" completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 groupFound cath "privacy" - cath #> "@SimpleX-Directory /confirm 1:privacy" - cath <# "SimpleX-Directory> The group privacy (Privacy) is already listed in the directory, please choose another name." + cath #> "@'SimpleX Directory' /confirm 1:privacy" + cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name." testDuplicateProhibitWhenUpdated :: HasCallStack => TestParams -> IO () testDuplicateProhibitWhenUpdated ps = @@ -1012,10 +1013,10 @@ testDuplicateProhibitWhenUpdated ps = welcomeWithLink <- groupAccepted bob "privacy" cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" - cath <# "SimpleX-Directory> The group privacy (Privacy) is already submitted to the directory." + cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" - cath <# "SimpleX-Directory> /confirm 1:privacy" - cath #> "@SimpleX-Directory /confirm 1:privacy" + cath <# "'SimpleX Directory'> /confirm 1:privacy" + cath #> "@'SimpleX Directory' /confirm 1:privacy" welcomeWithLink' <- groupAccepted cath "privacy" groupNotFound cath "privacy" completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 @@ -1023,10 +1024,10 @@ testDuplicateProhibitWhenUpdated ps = cath ##> ("/set welcome privacy " <> welcomeWithLink') cath <## "welcome message changed to:" cath <## welcomeWithLink' - cath <# "SimpleX-Directory> The group privacy (Privacy) is already listed in the directory, please choose another name." + cath <# "'SimpleX Directory'> The group privacy (Privacy) is already listed in the directory, please choose another name." cath ##> "/gp privacy security Security" cath <## "changed to #security (Security)" - cath <# "SimpleX-Directory> Thank you! The group link for ID 2 (security) is added to the welcome message." + cath <# "'SimpleX Directory'> Thank you! The group link for ID 2 (security) is added to the welcome message." cath <## "You will be notified once the group is added to the directory - it may take up to 48 hours." notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2 approveRegistration superUser cath "security" 2 @@ -1043,10 +1044,10 @@ testDuplicateProhibitApproval ps = welcomeWithLink <- groupAccepted bob "privacy" cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" - cath <# "SimpleX-Directory> The group privacy (Privacy) is already submitted to the directory." + cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" - cath <# "SimpleX-Directory> /confirm 1:privacy" - cath #> "@SimpleX-Directory /confirm 1:privacy" + cath <# "'SimpleX Directory'> /confirm 1:privacy" + cath #> "@'SimpleX Directory' /confirm 1:privacy" welcomeWithLink' <- groupAccepted cath "privacy" updateProfileWithLink cath "privacy" welcomeWithLink' 2 notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2 @@ -1055,8 +1056,8 @@ testDuplicateProhibitApproval ps = groupFound cath "privacy" -- fails at approval, as already listed let approve = "/approve 2:privacy 1" - superUser #> ("@SimpleX-Directory " <> approve) - superUser <# ("SimpleX-Directory> > " <> approve) + superUser #> ("@'SimpleX Directory' " <> approve) + superUser <# ("'SimpleX Directory'> > " <> approve) superUser <## " The group ID 2 (privacy) is already listed in the directory." testListUserGroups :: HasCallStack => TestParams -> IO () @@ -1070,20 +1071,20 @@ testListUserGroups ps = connectUsers bob cath fullAddMember "privacy" "Privacy" bob cath GRMember joinGroup "privacy" cath bob - cath <## "#privacy: member SimpleX-Directory_1 is connected" - cath <## "contact and member are merged: SimpleX-Directory, #privacy SimpleX-Directory_1" - cath <## "use @SimpleX-Directory to send messages" + cath <## "#privacy: member 'SimpleX Directory_1' is connected" + cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" + cath <## "use @'SimpleX Directory' to send messages" registerGroupId superUser bob "security" "Security" 2 2 registerGroupId superUser cath "anonymity" "Anonymity" 3 1 listUserGroup cath "anonymity" "Anonymity" -- with de-listed group groupFound cath "anonymity" - cath ##> "/mr anonymity SimpleX-Directory member" - cath <## "#anonymity: you changed the role of SimpleX-Directory to member" - cath <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (anonymity) is changed to member." + cath ##> "/mr anonymity 'SimpleX Directory' member" + cath <## "#anonymity: you changed the role of 'SimpleX Directory' to member" + cath <# "'SimpleX Directory'> SimpleX Directory role in the group ID 1 (anonymity) is changed to member." cath <## "" cath <## "The group is no longer listed in the directory." - superUser <# "SimpleX-Directory> The group ID 3 (anonymity) is de-listed (SimpleX-Directory role is changed to member)." + superUser <# "'SimpleX Directory'> The group ID 3 (anonymity) is de-listed (SimpleX Directory role is changed to member)." groupNotFound cath "anonymity" listGroups superUser bob cath @@ -1095,32 +1096,34 @@ testCapthaScreening ps = bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" -- check default role - bob #> "@SimpleX-Directory /role 1" - bob <# "SimpleX-Directory> > /role 1" + bob #> "@'SimpleX Directory' /role 1" + bob <# "'SimpleX Directory'> > /role 1" bob <## " The initial member role for the group privacy is set to member" - bob <## "Send /role 1 observer to change it." + bob <## "Send /'role 1 observer' to change it." bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note -- enable captcha - bob #> "@SimpleX-Directory /filter 1 captcha" - bob <# "SimpleX-Directory> > /filter 1 captcha" + bob #> "@'SimpleX Directory' /filter 1 captcha" + bob <# "'SimpleX Directory'> > /filter 1 captcha" bob <## " Spam filter settings for group privacy set to:" bob <## "- reject long/inappropriate names: disabled" bob <## "- pass captcha to join: enabled" bob <## "" - bob <## "Use /filter 1 [name] [captcha] to enable and /filter 1 off to disable filter." + bob <## "/'filter 1 name' - enable name filter" + bob <## "/'filter 1 name captcha' - enable both" + 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) SimpleX-Directory!> > cath 123" + cath <# "#privacy (support) 'SimpleX Directory'!> > cath 123" cath <## " Incorrect text, please try again." - captcha <- dropStrPrefix "#privacy (support) SimpleX-Directory> " . dropTime <$> getTermLine cath + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath sendCaptcha cath captcha - cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" - bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" cath #> "#privacy hello" bob <# "#privacy cath> hello" @@ -1131,8 +1134,8 @@ testCapthaScreening ps = cath ##> "/d #privacy" cath <## "#privacy: you deleted the group" -- change default role to observer - bob #> "@SimpleX-Directory /role 1 observer" - bob <# "SimpleX-Directory> > /role 1 observer" + bob #> "@'SimpleX Directory' /role 1 observer" + bob <# "'SimpleX Directory'> > /role 1 observer" bob <## " The initial member role for the group privacy is set to observer" bob <## "" bob <##. "Please note: it applies only to members joining via this link: https://" @@ -1140,18 +1143,18 @@ testCapthaScreening ps = captcha' <- join cath groupLink sendCaptcha cath captcha' -- message from cath that left - pastMember <- dropStrPrefix "#privacy: SimpleX-Directory forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath + pastMember <- dropStrPrefix "#privacy: 'SimpleX Directory' forwarded a message from an unknown member, creating unknown member record " <$> getTermLine cath cath <# ("#privacy " <> pastMember <> "> hello [>>]") - cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" - bob <## "#privacy: SimpleX-Directory added cath_1 (Catherine) to the group (connecting...)" + bob <## "#privacy: 'SimpleX Directory' added cath_1 (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath_1 is connected" cath ##> "#privacy hello" cath <## "#privacy: you don't have permission to send messages" (bob "/ms privacy" cath <## "cath (Catherine): observer, you, connected" - cath <## "SimpleX-Directory: admin, host, connected" + cath <## "'SimpleX Directory': admin, host, connected" cath <## "bob (Bob): owner, connected" cath <## (pastMember <> ": author, status unknown") where @@ -1160,14 +1163,14 @@ testCapthaScreening ps = cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" - cath <# "#privacy (support) SimpleX-Directory> Captcha is generated by SimpleX Directory service." + cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." - dropStrPrefix "#privacy (support) SimpleX-Directory> " . dropTime <$> getTermLine cath + 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) SimpleX-Directory!> > cath " <> captcha) + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" @@ -1191,7 +1194,7 @@ testRestoreDirectory ps = do groupFoundN 3 bob "privacy" groupFound bob "security" groupFoundN 3 cath "privacy" - cath #> "@SimpleX-Directory security" + cath #> "@'SimpleX Directory' security" groupFoundN' 2 cath "security" testCaptcha :: HasCallStack => TestParams -> IO () @@ -1215,18 +1218,18 @@ listGroups superUser bob cath = do sendListCommand cath 1 groupListing cath 1 "anonymity" "Anonymity" 2 "suspended because roles changed" -- superuser lists all groups - bob #> "@SimpleX-Directory /last" - bob <# "SimpleX-Directory> > /last" + bob #> "@'SimpleX Directory' /last" + bob <# "'SimpleX Directory'> > /last" bob <## " You are not allowed to use this command" - superUser #> "@SimpleX-Directory /last" - superUser <# "SimpleX-Directory> > /last" + superUser #> "@'SimpleX Directory' /last" + superUser <# "'SimpleX Directory'> > /last" superUser <## " 3 registered group(s)" memberGroupListing superUser bob 1 "privacy" "Privacy" 3 "active" memberGroupListing superUser bob 2 "security" "Security" 2 "active" memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed" -- showing last 1 group - superUser #> "@SimpleX-Directory /last 1" - superUser <# "SimpleX-Directory> > /last 1" + superUser #> "@'SimpleX Directory' /last 1" + superUser <# "'SimpleX Directory'> > /last 1" superUser <## " 3 registered group(s), showing the last 1" memberGroupListing superUser cath 3 "anonymity" "Anonymity" 2 "suspended because roles changed" @@ -1237,8 +1240,8 @@ listUserGroup u n fn = do sendListCommand :: HasCallStack => TestCC -> Int -> IO () sendListCommand u count = do - u #> "@SimpleX-Directory /list" - u <# "SimpleX-Directory> > /list" + u #> "@'SimpleX Directory' /list" + u <# "'SimpleX Directory'> > /list" u <## (" " <> show count <> " registered group(s)") groupListing :: HasCallStack => TestCC -> Int -> String -> String -> Int -> String -> IO () @@ -1249,7 +1252,7 @@ memberGroupListing su owner = groupListing_ su (Just owner) groupListing_ :: HasCallStack => TestCC -> Maybe TestCC -> Int -> String -> String -> Int -> String -> IO () groupListing_ su owner_ gId n fn count status = do - su <# ("SimpleX-Directory> " <> show gId <> ". " <> n <> " (" <> fn <> ")") + su <# ("'SimpleX Directory'> " <> show gId <> ". " <> n <> " (" <> fn <> ")") su <## "Welcome message:" su <##. ("Link to join the group " <> n <> ": ") forM_ owner_ $ \owner -> do @@ -1257,34 +1260,35 @@ groupListing_ su owner_ gId n fn count status = do su <## ("Owner: " <> ownerName) su <## (show count <> " members") su <## ("Status: " <> status) + su <## ("/'role " <> show gId <> "', /'filter " <> show gId <> "'") reapproveGroup :: HasCallStack => Int -> TestCC -> TestCC -> IO () reapproveGroup count superUser bob = do - superUser <# "SimpleX-Directory> bob submitted the group ID 1:" + superUser <# "'SimpleX Directory'> bob submitted the group ID 1:" superUser <##. "privacy (" superUser <## "Welcome message:" superUser <##. "Link to join the group privacy: " superUser <## (show count <> " members") superUser <## "" superUser <## "To approve send:" - superUser <# "SimpleX-Directory> /approve 1:privacy 1" - superUser #> "@SimpleX-Directory /approve 1:privacy 1" - superUser <# "SimpleX-Directory> > /approve 1:privacy 1" + superUser <# "'SimpleX Directory'> /approve 1:privacy 1" + superUser #> "@'SimpleX Directory' /approve 1:privacy 1" + superUser <# "'SimpleX Directory'> > /approve 1:privacy 1" superUser <## " Group approved!" - bob <# "SimpleX-Directory> The group ID 1 (privacy) is approved and listed in directory - please moderate it!" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!" bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." bob <## "" bob <## "Supported commands:" - bob <## "- /filter 1 - to configure anti-spam filter." - bob <## "- /role 1 - to set default member role." - bob <## "- /help commands - other commands." + bob <## "/'filter 1' - to configure anti-spam filter." + bob <## "/'role 1' - to set default member role." + bob <## "/'link 1' - to view/upgrade group link." addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO () addCathAsOwner bob cath = do connectUsers bob cath fullAddMember "privacy" "Privacy" bob cath GROwner joinGroup "privacy" cath bob - cath <## "#privacy: member SimpleX-Directory is connected" + cath <## "#privacy: member 'SimpleX Directory' is connected" withDirectoryService :: HasCallStack => TestParams -> (TestCC -> String -> IO ()) -> IO () withDirectoryService ps = withDirectoryServiceCfg ps testCfg @@ -1302,13 +1306,13 @@ withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup test = do superUser ##> "/g owners" superUser <## "group #owners is created" superUser <## "to add members use /a owners or /create link #owners" - superUser ##> "/a owners SimpleX-Directory admin" - superUser <## "invitation to join the group #owners sent to SimpleX-Directory" + superUser ##> "/a owners 'SimpleX Directory' admin" + superUser <## "invitation to join the group #owners sent to 'SimpleX Directory'" ds <## "#owners: alice invites you to join the group as admin" ds <## "use /j owners to accept" ds ##> "/j owners" ds <## "#owners: you joined the group" - superUser <## "#owners: SimpleX-Directory joined the group" + superUser <## "#owners: 'SimpleX Directory' joined the group" ds ##> "/ad" getContactLink ds True withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test @@ -1366,19 +1370,19 @@ submitGroup u n fn = do u ##> ("/g " <> viewName n <> if null fn then "" else " " <> fn) u <## ("group #" <> viewName n <> (if null fn then "" else " (" <> fn <> ")") <> " is created") u <## ("to add members use /a " <> viewName n <> " or /create link #" <> viewName n) - u ##> ("/a " <> viewName n <> " SimpleX-Directory admin") - u <## ("invitation to join the group #" <> viewName n <> " sent to SimpleX-Directory") + u ##> ("/a " <> viewName n <> " 'SimpleX Directory' admin") + u <## ("invitation to join the group #" <> viewName n <> " sent to 'SimpleX Directory'") 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 <# ("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 <# ("'SimpleX Directory'> Joining the group " <> n <> "…") + u <## ("#" <> 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 <## "" u <## "Please add it to the group welcome message." u <## "For example, add:" - dropStrPrefix "SimpleX-Directory> " . dropTime <$> getTermLine u -- welcome message with link + dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u -- welcome message with link completeRegistration :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () completeRegistration su u n fn welcomeWithLink gId = @@ -1395,13 +1399,13 @@ updateProfileWithLink u n welcomeWithLink ugId = do u ##> ("/set welcome " <> viewName n <> " " <> welcomeWithLink) u <## "welcome message changed to:" u <## welcomeWithLink - u <# ("SimpleX-Directory> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.") + u <# ("'SimpleX Directory'> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.") u <## "You will be notified once the group is added to the directory - it may take up to 48 hours." notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () notifySuperUser su u n fn welcomeWithLink gId = do uName <- userName u - su <# ("SimpleX-Directory> " <> uName <> " submitted the group ID " <> show gId <> ":") + su <# ("'SimpleX Directory'> " <> uName <> " submitted the group ID " <> show gId <> ":") su <## (n <> if null fn then "" else " (" <> fn <> ")") su <## "Welcome message:" su <## welcomeWithLink @@ -1409,7 +1413,7 @@ notifySuperUser su u n fn welcomeWithLink gId = do su <## "" su <## "To approve send:" let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1" - su <# ("SimpleX-Directory> " <> approve) + su <# ("'SimpleX Directory'> " <> approve) approveRegistration :: TestCC -> TestCC -> String -> Int -> IO () approveRegistration su u n gId = @@ -1418,29 +1422,29 @@ approveRegistration su u n gId = approveRegistrationId :: TestCC -> TestCC -> String -> Int -> Int -> IO () approveRegistrationId su u n gId ugId = do let approve = "/approve " <> show gId <> ":" <> viewName n <> " 1" - su #> ("@SimpleX-Directory " <> approve) - su <# ("SimpleX-Directory> > " <> approve) + su #> ("@'SimpleX Directory' " <> approve) + su <# ("'SimpleX Directory'> > " <> approve) su <## " Group approved!" - u <# ("SimpleX-Directory> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory - please moderate it!") + u <# ("'SimpleX Directory'> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory - please moderate it!") u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." u <## "" u <## "Supported commands:" - u <## ("- /filter " <> show ugId <> " - to configure anti-spam filter.") - u <## ("- /role " <> show ugId <> " - to set default member role.") - u <## "- /help commands - other commands." + u <## ("/'filter " <> show ugId <> "' - to configure anti-spam filter.") + u <## ("/'role " <> show ugId <> "' - to set default member role.") + u <## ("/'link " <> show ugId <> "' - to view/upgrade group link.") connectVia :: TestCC -> String -> IO () u `connectVia` dsLink = do u ##> ("/c " <> dsLink) u <## "connection request sent!" u .<## ": contact is connected" - u .<# "> Welcome to SimpleX-Directory service!" - u <## "Send a search string to find groups or /help to learn how to add groups to directory." + u .<# "> Welcome to SimpleX Directory!" u <## "" - u <## "For example, send privacy to find groups about privacy." - u <## "Or send /all or /new to list groups." + u <## "🔍 Send search string to find groups - try security." + u <## "/help - how to submit your group." + u <## "/new - recent groups." u <## "" - u <## "Content and privacy policy: https://simplex.chat/docs/directory.html" + u <## "[Directory rules](https://simplex.chat/docs/directory.html)." joinGroup :: String -> TestCC -> TestCC -> IO () joinGroup gName member host = do @@ -1474,7 +1478,7 @@ groupFound = groupFoundN 2 groupFoundN :: Int -> TestCC -> String -> IO () groupFoundN count u name = do - u #> ("@SimpleX-Directory " <> name) + u #> ("@'SimpleX Directory' " <> name) groupFoundN' count u name groupFoundN' :: Int -> TestCC -> String -> IO () @@ -1482,9 +1486,9 @@ groupFoundN' = groupFoundN_ "" Nothing groupFoundN_ :: String -> Maybe Int -> Int -> TestCC -> String -> IO () groupFoundN_ suffix shownId_ count u name = do - u <# ("SimpleX-Directory" <> suffix <> "> > " <> name) + u <# ("'SimpleX Directory" <> suffix <> "'> > " <> name) u <## " Found 1 group(s)." - u <#. ("SimpleX-Directory" <> suffix <> "> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name) + u <#. ("'SimpleX Directory" <> suffix <> "'> " <> maybe "" (\gId -> show gId <> ". ") shownId_ <> name) u <## "Welcome message:" u <##. "Link to join the group " u <## (show count <> " members") @@ -1494,6 +1498,6 @@ groupNotFound = groupNotFound_ "" groupNotFound_ :: String -> TestCC -> String -> IO () groupNotFound_ suffix u s = do - u #> ("@SimpleX-Directory" <> suffix <> " " <> s) - u <# ("SimpleX-Directory" <> suffix <> "> > " <> s) + u #> ("@'SimpleX Directory" <> suffix <> "' " <> s) + u <# ("'SimpleX Directory" <> suffix <> "'> > " <> s) u <## " No groups found" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 8650b80b06..dfb0aece74 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -24,6 +24,7 @@ import Data.Maybe (fromMaybe) import Data.String import qualified Data.Text as T import Simplex.Chat.Controller (ChatConfig (..), ChatController (..)) +import Simplex.Chat.Markdown (viewName) import Simplex.Chat.Messages.CIContent (e2eInfoNoPQText, e2eInfoPQText) import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct (getContact) @@ -723,7 +724,7 @@ connectUsers_ cc1 cc2 noShortLink = do showName :: TestCC -> IO String showName (TestCC ChatController {currentUser} _ _ _ _ _) = do Just User {localDisplayName, profile = LocalProfile {fullName, shortDescr}} <- readTVarIO currentUser - pure . T.unpack $ localDisplayName <> optionalFullName localDisplayName fullName shortDescr + pure . T.unpack $ viewName localDisplayName <> optionalFullName localDisplayName fullName shortDescr createGroup2 :: HasCallStack => String -> TestCC -> TestCC -> IO () createGroup2 gName cc1 cc2 = createGroup2' gName cc1 (cc2, GRAdmin) True From aec455c923905c1952c4b5f4010eb1098a66b1c4 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Mon, 25 Aug 2025 08:10:28 +0000 Subject: [PATCH 008/137] install.sh: fix linux arch (#6173) --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 68acbccb14..237fbb666c 100755 --- a/install.sh +++ b/install.sh @@ -20,7 +20,7 @@ fi if [ $PLATFORM == "Darwin" ]; then PLATFORM="macos-x86-64" elif [ $PLATFORM == "Linux" ]; then - PLATFORM="ubuntu-22_04-x86-64" + PLATFORM="ubuntu-22_04-x86_64" else echo "Scripted installation on your platform is not supported." echo "See compiled binaries in the ${1:-latest} release: https://github.com/$APP_NAME/$APP_NAME/releases/$RELEASE" From e2d5c675d04da286982183e37e73ec3149638a8a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 26 Aug 2025 16:38:27 +0100 Subject: [PATCH 009/137] bots: generate code for TypeScript types module from Haskell tests (#6220) * bots: generate code for TypeScript types module from Haskell tests * types for API events and command responses * code for chat command types * license, readme * fix array types * fix more types * add response type * add Connect command to docs/ts * update typescript client package to use auto-generated types --- bots/api/COMMANDS.md | 281 +- bots/api/TYPES.md | 12 +- bots/src/API/Docs/Commands.hs | 76 +- bots/src/API/Docs/Generate.hs | 18 +- bots/src/API/Docs/Generate/TypeScript.hs | 189 + bots/src/API/Docs/Responses.hs | 2 +- bots/src/API/Docs/Syntax.hs | 37 +- bots/src/API/Docs/Syntax/Types.hs | 12 + bots/src/API/Docs/Types.hs | 6 +- bots/src/API/TypeInfo.hs | 20 +- .../types/typescript/.gitignore | 4 + .../types/typescript/LICENSE | 661 +++ .../types/typescript/README.md | 11 + .../types/typescript/package.json | 42 + .../types/typescript/src/commands.ts | 662 +++ .../types/typescript/src/events.ts | 430 ++ .../types/typescript/src/index.ts | 4 + .../types/typescript/src/responses.ts | 396 ++ .../types/typescript/src/types.ts | 4509 +++++++++++++++++ .../types/typescript/tsconfig.json | 22 + .../simplex-chat-client/typescript/README.md | 2 + .../typescript/package.json | 15 +- .../typescript/src/client.ts | 237 +- .../typescript/src/command.ts | 822 --- .../typescript/src/response.ts | 1109 ---- .../typescript/src/transport.ts | 12 +- .../typescript/tests/client.test.ts | 35 +- simplex-chat.cabal | 1 + src/Simplex/Chat/Controller.hs | 4 +- tests/APIDocs.hs | 35 +- 30 files changed, 7416 insertions(+), 2250 deletions(-) create mode 100644 bots/src/API/Docs/Generate/TypeScript.hs create mode 100644 packages/simplex-chat-client/types/typescript/.gitignore create mode 100644 packages/simplex-chat-client/types/typescript/LICENSE create mode 100644 packages/simplex-chat-client/types/typescript/README.md create mode 100644 packages/simplex-chat-client/types/typescript/package.json create mode 100644 packages/simplex-chat-client/types/typescript/src/commands.ts create mode 100644 packages/simplex-chat-client/types/typescript/src/events.ts create mode 100644 packages/simplex-chat-client/types/typescript/src/index.ts create mode 100644 packages/simplex-chat-client/types/typescript/src/responses.ts create mode 100644 packages/simplex-chat-client/types/typescript/src/types.ts create mode 100644 packages/simplex-chat-client/types/typescript/tsconfig.json delete mode 100644 packages/simplex-chat-client/typescript/src/command.ts delete mode 100644 packages/simplex-chat-client/typescript/src/response.ts diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index 4ea8b7e466..dd1dd256d0 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -42,6 +42,7 @@ This file is generated automatically. - [APIAddContact](#apiaddcontact) - [APIConnectPlan](#apiconnectplan) - [APIConnect](#apiconnect) +- [Connect](#connect) - [APIAcceptContact](#apiacceptcontact) - [APIRejectContact](#apirejectcontact) @@ -90,13 +91,17 @@ Create bot address. '/_address ' + str(userId) # Python ``` -**Response**: +**Responses**: UserContactLinkCreated: User contact address created. - type: "userContactLinkCreated" - user: [User](./TYPES.md#user) - connLinkContact: [CreatedConnLink](./TYPES.md#createdconnlink) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -123,12 +128,16 @@ Delete bot address. '/_delete_address ' + str(userId) # Python ``` -**Response**: +**Responses**: UserContactLinkDeleted: User contact address deleted. - type: "userContactLinkDeleted" - user: [User](./TYPES.md#user) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -155,13 +164,17 @@ Get bot address and settings. '/_show_address ' + str(userId) # Python ``` -**Response**: +**Responses**: UserContactLink: User contact address. - type: "userContactLink" - user: [User](./TYPES.md#user) - contactLink: [UserContactLink](./TYPES.md#usercontactlink) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -189,7 +202,7 @@ Add address to bot profile. '/_profile_address ' + str(userId) + ' ' + ('on' if enable else 'off') # Python ``` -**Response**: +**Responses**: UserProfileUpdated: User profile updated. - type: "userProfileUpdated" @@ -198,6 +211,10 @@ UserProfileUpdated: User profile updated. - toProfile: [Profile](./TYPES.md#profile) - updateSummary: [UserProfileUpdateSummary](./TYPES.md#userprofileupdatesummary) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -225,13 +242,17 @@ Set bot address settings. '/_address_settings ' + str(userId) + ' ' + json.dumps(settings) # Python ``` -**Response**: +**Responses**: UserContactLinkUpdated: User contact address updated. - type: "userContactLinkUpdated" - user: [User](./TYPES.md#user) - contactLink: [UserContactLink](./TYPES.md#usercontactlink) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -266,13 +287,17 @@ Send messages. '/_send ' + str(sendRef) + (' live=on' if liveMessage else '') + ((' ttl=' + str(ttl)) if ttl is not None else '') + ' json ' + json.dumps(composedMessages) # Python ``` -**Response**: +**Responses**: NewChatItems: New messages. - type: "newChatItems" - user: [User](./TYPES.md#user) - chatItems: [[AChatItem](./TYPES.md#achatitem)] +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -349,7 +374,7 @@ Delete message. '/_delete item ' + str(chatRef) + ' ' + ','.join(map(str, chatItemIds)) + ' ' + str(deleteMode) # Python ``` -**Response**: +**Responses**: ChatItemsDeleted: Messages deleted. - type: "chatItemsDeleted" @@ -358,6 +383,10 @@ ChatItemsDeleted: Messages deleted. - byUser: bool - timed: bool +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -385,7 +414,7 @@ Moderate message. Requires Moderator role (and higher than message author's). '/_delete member item #' + str(groupId) + ' ' + ','.join(map(str, chatItemIds)) # Python ``` -**Response**: +**Responses**: ChatItemsDeleted: Messages deleted. - type: "chatItemsDeleted" @@ -394,6 +423,10 @@ ChatItemsDeleted: Messages deleted. - byUser: bool - timed: bool +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -423,7 +456,7 @@ Add/remove message reaction. '/_reaction ' + str(chatRef) + ' ' + str(chatItemId) + ' ' + ('on' if add else 'off') + ' ' + json.dumps(reaction) # Python ``` -**Response**: +**Responses**: ChatItemReaction: Message reaction. - type: "chatItemReaction" @@ -431,6 +464,10 @@ ChatItemReaction: Message reaction. - added: bool - reaction: [ACIReaction](./TYPES.md#acireaction) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -478,6 +515,10 @@ RcvFileAcceptedSndCancelled: File accepted, but no longer sent. - user: [User](./TYPES.md#user) - rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -559,7 +600,7 @@ Add contact to group. Requires bot to have Admin role. '/_add #' + str(groupId) + ' ' + str(contactId) + ' ' + str(memberRole) # Python ``` -**Response**: +**Responses**: SentGroupInvitation: Group invitation sent. - type: "sentGroupInvitation" @@ -568,6 +609,10 @@ SentGroupInvitation: Group invitation sent. - contact: [Contact](./TYPES.md#contact) - member: [GroupMember](./TYPES.md#groupmember) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -594,7 +639,7 @@ Join group. '/_join #' + str(groupId) # Python ``` -**Response**: +**Responses**: UserAcceptedGroupSent: User accepted group invitation. - type: "userAcceptedGroupSent" @@ -602,6 +647,10 @@ UserAcceptedGroupSent: User accepted group invitation. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - hostContact: [Contact](./TYPES.md#contact)? +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -673,7 +722,7 @@ Set members role. Requires Admin role. '/_member role #' + str(groupId) + ' ' + ','.join(map(str, groupMemberIds)) + ' ' + str(memberRole) # Python ``` -**Response**: +**Responses**: MembersRoleUser: Members role changed by user. - type: "membersRoleUser" @@ -682,6 +731,10 @@ MembersRoleUser: Members role changed by user. - members: [[GroupMember](./TYPES.md#groupmember)] - toRole: [GroupMemberRole](./TYPES.md#groupmemberrole) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -710,7 +763,7 @@ Block members. Requires Moderator role. '/_block #' + str(groupId) + ' ' + ','.join(map(str, groupMemberIds)) + ' blocked=' + ('on' if blocked else 'off') # Python ``` -**Response**: +**Responses**: MembersBlockedForAllUser: Members blocked for all by admin. - type: "membersBlockedForAllUser" @@ -719,6 +772,10 @@ MembersBlockedForAllUser: Members blocked for all by admin. - members: [[GroupMember](./TYPES.md#groupmember)] - blocked: bool +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -789,13 +846,17 @@ Leave group. '/_leave #' + str(groupId) # Python ``` -**Response**: +**Responses**: LeftMemberUser: User left group. - type: "leftMemberUser" - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -822,13 +883,17 @@ Get group members. '/_members #' + str(groupId) # Python ``` -**Response**: +**Responses**: GroupMembers: Group members. - type: "groupMembers" - user: [User](./TYPES.md#user) - group: [Group](./TYPES.md#group) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -857,13 +922,17 @@ Create group. '/_group ' + str(userId) + (' incognito=on' if incognito else '') + ' ' + json.dumps(groupProfile) # Python ``` -**Response**: +**Responses**: GroupCreated: Group created. - type: "groupCreated" - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -891,7 +960,7 @@ Update group profile. '/_group_profile #' + str(groupId) + ' ' + json.dumps(groupProfile) # Python ``` -**Response**: +**Responses**: GroupUpdated: Group updated. - type: "groupUpdated" @@ -900,6 +969,10 @@ GroupUpdated: Group updated. - toGroup: [GroupInfo](./TYPES.md#groupinfo) - member_: [GroupMember](./TYPES.md#groupmember)? +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -932,7 +1005,7 @@ Create group link. '/_create link #' + str(groupId) + ' ' + str(memberRole) # Python ``` -**Response**: +**Responses**: GroupLinkCreated: Group link created. - type: "groupLinkCreated" @@ -940,6 +1013,10 @@ GroupLinkCreated: Group link created. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -967,7 +1044,7 @@ Set member role for group link. '/_set link role #' + str(groupId) + ' ' + str(memberRole) # Python ``` -**Response**: +**Responses**: GroupLink: Group link. - type: "groupLink" @@ -975,6 +1052,10 @@ GroupLink: Group link. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1001,13 +1082,17 @@ Delete group link. '/_delete link #' + str(groupId) # Python ``` -**Response**: +**Responses**: GroupLinkDeleted: Group link deleted. - type: "groupLinkDeleted" - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1034,7 +1119,7 @@ Get group link. '/_get link #' + str(groupId) # Python ``` -**Response**: +**Responses**: GroupLink: Group link. - type: "groupLink" @@ -1042,6 +1127,10 @@ GroupLink: Group link. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1074,7 +1163,7 @@ Create 1-time invitation link. '/_connect ' + str(userId) + (' incognito=on' if incognito else '') # Python ``` -**Response**: +**Responses**: Invitation: One-time invitation. - type: "invitation" @@ -1082,6 +1171,10 @@ Invitation: One-time invitation. - connLinkInvitation: [CreatedConnLink](./TYPES.md#createdconnlink) - connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1109,7 +1202,7 @@ Determine SimpleX link type and if the bot is already connected via this link. '/_connect plan ' + str(userId) + ' ' + connectionLink # Python ``` -**Response**: +**Responses**: ConnectionPlan: Connection link information. - type: "connectionPlan" @@ -1117,32 +1210,36 @@ ConnectionPlan: Connection link information. - connLink: [CreatedConnLink](./TYPES.md#createdconnlink) - connectionPlan: [ConnectionPlan](./TYPES.md#connectionplan) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- ### APIConnect -Connect via 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. **Parameters**: - userId: int64 - incognito: bool -- connLink_: [CreatedConnLink](./TYPES.md#createdconnlink)? +- preparedLink_: [CreatedConnLink](./TYPES.md#createdconnlink)? **Syntax**: ``` -/_connect +/_connect [ ] ``` ```javascript -'/_connect ' + userId + ' ' + connLink_.toString() // JavaScript +'/_connect ' + userId + (preparedLink_ ? ' ' + preparedLink_.toString() : '') // JavaScript ``` ```python -'/_connect ' + str(userId) + ' ' + str(connLink_) # Python +'/_connect ' + str(userId) + ((' ' + str(preparedLink_)) if preparedLink_ is not None else '') # Python ``` **Responses**: @@ -1164,6 +1261,60 @@ SentInvitation: Invitation sent to contact address. - connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) - customUserProfile: [Profile](./TYPES.md#profile)? +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + +### Connect + +Connect via SimpleX link as string in the active user profile. + +*Network usage*: interactive. + +**Parameters**: +- incognito: bool +- connLink_: string? + +**Syntax**: + +``` +/connect[ ] +``` + +```javascript +'/connect' + (connLink_ ? ' ' + connLink_ : '') // JavaScript +``` + +```python +'/connect' + ((' ' + connLink_) if connLink_ is not None else '') # Python +``` + +**Responses**: + +SentConfirmation: Confirmation sent to one-time invitation. +- type: "sentConfirmation" +- user: [User](./TYPES.md#user) +- connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) +- customUserProfile: [Profile](./TYPES.md#profile)? + +ContactAlreadyExists: Contact already exists. +- type: "contactAlreadyExists" +- user: [User](./TYPES.md#user) +- contact: [Contact](./TYPES.md#contact) + +SentInvitation: Invitation sent to contact address. +- type: "sentInvitation" +- user: [User](./TYPES.md#user) +- connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) +- customUserProfile: [Profile](./TYPES.md#profile)? + +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1190,13 +1341,17 @@ Accept contact request. '/_accept ' + str(contactReqId) # Python ``` -**Response**: +**Responses**: AcceptingContactRequest: Contact request accepted. - type: "acceptingContactRequest" - user: [User](./TYPES.md#user) - contact: [Contact](./TYPES.md#contact) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1223,7 +1378,7 @@ Reject contact request. The user who sent the request is **not notified**. '/_reject ' + str(contactReqId) # Python ``` -**Response**: +**Responses**: ContactRequestRejected: Contact request rejected. - type: "contactRequestRejected" @@ -1231,6 +1386,10 @@ ContactRequestRejected: Contact request rejected. - contactRequest: [UserContactRequest](./TYPES.md#usercontactrequest) - contact_: [Contact](./TYPES.md#contact)? +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1262,13 +1421,17 @@ Get contacts. '/_contacts ' + str(userId) # Python ``` -**Response**: +**Responses**: ContactsList: Contacts. - type: "contactsList" - user: [User](./TYPES.md#user) - contacts: [[Contact](./TYPES.md#contact)] +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1297,13 +1460,17 @@ Get groups. '/_groups ' + str(userId) + ((' @' + str(contactId_)) if contactId_ is not None else '') + ((' ' + search) if search is not None else '') # Python ``` -**Response**: +**Responses**: GroupsList: Groups. - type: "groupsList" - user: [User](./TYPES.md#user) - groups: [[GroupInfoSummary](./TYPES.md#groupinfosummary)] +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1348,6 +1515,10 @@ GroupDeletedUser: User deleted group. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1368,12 +1539,16 @@ Get active user profile /user ``` -**Response**: +**Responses**: ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1400,12 +1575,16 @@ Create new user profile '/_create user ' + json.dumps(newUser) # Python ``` -**Response**: +**Responses**: ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + **Errors**: - UserExists: User or contact with this name already exists. - InvalidDisplayName: Invalid user display name. @@ -1425,12 +1604,16 @@ Get all user profiles /users ``` -**Response**: +**Responses**: UsersList: Users. - type: "usersList" - users: [[UserInfo](./TYPES.md#userinfo)] +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1458,12 +1641,16 @@ Set active user profile '/_user ' + str(userId) + ((' ' + json.dumps(viewPwd)) if viewPwd is not None else '') # Python ``` -**Response**: +**Responses**: ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + **Errors**: - ChatNotStarted: Chat not started. @@ -1495,12 +1682,16 @@ Delete user profile. '/_delete user ' + str(userId) + ' del_smp=' + ('on' if delSMPQueues else 'off') + ((' ' + json.dumps(viewPwd)) if viewPwd is not None else '') # Python ``` -**Response**: +**Responses**: CmdOk: Ok. - type: "cmdOk" - user_: [User](./TYPES.md#user)? +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1528,7 +1719,7 @@ Update user profile. '/_profile ' + str(userId) + ' ' + json.dumps(profile) # Python ``` -**Response**: +**Responses**: UserProfileUpdated: User profile updated. - type: "userProfileUpdated" @@ -1537,6 +1728,14 @@ UserProfileUpdated: User profile updated. - toProfile: [Profile](./TYPES.md#profile) - updateSummary: [UserProfileUpdateSummary](./TYPES.md#userprofileupdatesummary) +UserProfileNoChange: User profile was not changed. +- type: "userProfileNoChange" +- user: [User](./TYPES.md#user) + +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- @@ -1564,7 +1763,7 @@ Configure chat preference overrides for the contact. '/_set prefs @' + str(contactId) + ' ' + json.dumps(preferences) # Python ``` -**Response**: +**Responses**: ContactPrefsUpdated: Contact preferences updated. - type: "contactPrefsUpdated" @@ -1572,4 +1771,8 @@ ContactPrefsUpdated: Contact preferences updated. - fromContact: [Contact](./TYPES.md#contact) - toContact: [Contact](./TYPES.md#contact) +ChatCmdError: Command error. +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + --- diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 449d6a9294..4743ccb881 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -228,14 +228,6 @@ DECRYPT_CB: RATCHET_HEADER: - type: "RATCHET_HEADER" -RATCHET_EARLIER: -- type: "RATCHET_EARLIER" -- : word32 - -RATCHET_SKIPPED: -- type: "RATCHET_SKIPPED" -- : word32 - RATCHET_SYNC: - type: "RATCHET_SYNC" @@ -1359,11 +1351,11 @@ str(chatType) + str(chatId) + ((str(chatScope)) if chatScope is not None else '' ``` ```javascript -self == 'contact' ? '@' : self == 'group' ? '#' : self == 'local' ? '*' : '' // JavaScript +self == 'direct' ? '@' : self == 'group' ? '#' : self == 'local' ? '*' : '' // JavaScript ``` ```python -'@' if str(self) == 'contact' else '#' if str(self) == 'group' else '*' if str(self) == 'local' else '' # Python +'@' if str(self) == 'direct' else '#' if str(self) == 'group' else '*' if str(self) == 'local' else '' # Python ``` diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 3804a23f31..279a74480b 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -77,16 +77,16 @@ chatCommandsDocsData :: [(String, String, [(ConsName, [String], Text, [ConsName] chatCommandsDocsData = [ ( "Address commands", "Bots can use these commands to automatically check and create address when initialized", - [ ("APICreateMyAddress", [], "Create bot address.", ["CRUserContactLinkCreated"], [], Just UNInteractive, "/_address " <> Param "userId"), - ("APIDeleteMyAddress", [], "Delete bot address.", ["CRUserContactLinkDeleted"], [], Just UNBackground, "/_delete_address " <> Param "userId"), - ("APIShowMyAddress", [], "Get bot address and settings.", ["CRUserContactLink"], [], Nothing, "/_show_address " <> Param "userId"), - ("APISetProfileAddress", [], "Add address to bot profile.", ["CRUserProfileUpdated"], [], Just UNInteractive, "/_profile_address " <> Param "userId" <> " " <> OnOff "enable"), - ("APISetAddressSettings", [], "Set bot address settings.", ["CRUserContactLinkUpdated"], [], Just UNInteractive, "/_address_settings " <> Param "userId" <> " " <> Json "settings") + [ ("APICreateMyAddress", [], "Create bot address.", ["CRUserContactLinkCreated", "CRChatCmdError"], [], Just UNInteractive, "/_address " <> Param "userId"), + ("APIDeleteMyAddress", [], "Delete bot address.", ["CRUserContactLinkDeleted", "CRChatCmdError"], [], Just UNBackground, "/_delete_address " <> Param "userId"), + ("APIShowMyAddress", [], "Get bot address and settings.", ["CRUserContactLink", "CRChatCmdError"], [], Nothing, "/_show_address " <> Param "userId"), + ("APISetProfileAddress", [], "Add address to bot profile.", ["CRUserProfileUpdated", "CRChatCmdError"], [], Just UNInteractive, "/_profile_address " <> Param "userId" <> " " <> OnOff "enable"), + ("APISetAddressSettings", [], "Set bot address settings.", ["CRUserContactLinkUpdated", "CRChatCmdError"], [], Just UNInteractive, "/_address_settings " <> Param "userId" <> " " <> Json "settings") ] ), ( "Message commands", "Commands to send, update, delete, moderate messages and set message reactions", - [ ("APISendMessages", [], "Send messages.", ["CRNewChatItems"], [], Just UNBackground, "/_send " <> Param "sendRef" <> OnOffParam "live" "liveMessage" (Just False) <> Optional "" (" ttl=" <> Param "$0") "ttl" <> " json " <> Json "composedMessages"), + [ ("APISendMessages", [], "Send messages.", ["CRNewChatItems", "CRChatCmdError"], [], Just UNBackground, "/_send " <> Param "sendRef" <> OnOffParam "live" "liveMessage" (Just False) <> Optional "" (" ttl=" <> Param "$0") "ttl" <> " json " <> Json "composedMessages"), ( "APIUpdateChatItem", [], "Update message.", @@ -95,53 +95,54 @@ chatCommandsDocsData = Just UNBackground, "/_update item " <> Param "chatRef" <> " " <> Param "chatItemId" <> OnOffParam "live" "liveMessage" (Just False) <> " json " <> Json "updatedMessage" ), - ("APIDeleteChatItem", [], "Delete message.", ["CRChatItemsDeleted"], [], Just UNBackground, "/_delete item " <> Param "chatRef" <> " " <> Join ',' "chatItemIds" <> " " <> Param "deleteMode"), - ("APIDeleteMemberChatItem", [], "Moderate message. Requires Moderator role (and higher than message author's).", ["CRChatItemsDeleted"], [], Just UNBackground, "/_delete member item #" <> Param "groupId" <> " " <> Join ',' "chatItemIds"), - ("APIChatItemReaction", [], "Add/remove message reaction.", ["CRChatItemReaction"], [], Just UNBackground, "/_reaction " <> Param "chatRef" <> " " <> Param "chatItemId" <> " " <> OnOff "add" <> " " <> Json "reaction") + ("APIDeleteChatItem", [], "Delete message.", ["CRChatItemsDeleted", "CRChatCmdError"], [], Just UNBackground, "/_delete item " <> Param "chatRef" <> " " <> Join ',' "chatItemIds" <> " " <> Param "deleteMode"), + ("APIDeleteMemberChatItem", [], "Moderate message. Requires Moderator role (and higher than message author's).", ["CRChatItemsDeleted", "CRChatCmdError"], [], Just UNBackground, "/_delete member item #" <> Param "groupId" <> " " <> Join ',' "chatItemIds"), + ("APIChatItemReaction", [], "Add/remove message reaction.", ["CRChatItemReaction", "CRChatCmdError"], [], Just UNBackground, "/_reaction " <> Param "chatRef" <> " " <> Param "chatItemId" <> " " <> OnOff "add" <> " " <> Json "reaction") ] ), ( "File commands", "Commands to receive and to cancel files. Files are sent as part of the message, there are no separate commands to send files.", - [ ("ReceiveFile", [], "Receive file.", ["CRRcvFileAccepted", "CRRcvFileAcceptedSndCancelled"], [], Nothing, "/freceive " <> Param "fileId" <> OnOffParam "approved_relays" "userApprovedRelays" (Just False) <> OnOffParam "encrypt" "storeEncrypted" Nothing <> OnOffParam "inline" "fileInline" Nothing <> Optional "" (" " <> Param "$0") "filePath"), + [ ("ReceiveFile", [], "Receive file.", ["CRRcvFileAccepted", "CRRcvFileAcceptedSndCancelled", "CRChatCmdError"], [], Nothing, "/freceive " <> Param "fileId" <> OnOffParam "approved_relays" "userApprovedRelays" (Just False) <> OnOffParam "encrypt" "storeEncrypted" Nothing <> OnOffParam "inline" "fileInline" Nothing <> Optional "" (" " <> Param "$0") "filePath"), ("CancelFile", [], "Cancel file.", ["CRSndFileCancelled", "CRRcvFileCancelled", "CRChatCmdError"], [TD "CEFileCancel" "Cannot cancel file"], Just UNBackground, "/fcancel " <> Param "fileId") ] ), ( "Group commands", "Commands to manage and moderate groups. These commands can be used with business chats as well - they are groups. E.g., a common scenario would be to add human agents to business chat with the customer who connected via business address.", - [ ("APIAddMember", [], "Add contact to group. Requires bot to have Admin role.", ["CRSentGroupInvitation"], [], Just UNInteractive, "/_add #" <> Param "groupId" <> " " <> Param "contactId" <> " " <> Param "memberRole"), - ("APIJoinGroup", ["enableNtfs"], "Join group.", ["CRUserAcceptedGroupSent"], [], Just UNInteractive, "/_join #" <> Param "groupId"), + [ ("APIAddMember", [], "Add contact to group. Requires bot to have Admin role.", ["CRSentGroupInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_add #" <> Param "groupId" <> " " <> Param "contactId" <> " " <> Param "memberRole"), + ("APIJoinGroup", ["enableNtfs"], "Join group.", ["CRUserAcceptedGroupSent", "CRChatCmdError"], [], Just UNInteractive, "/_join #" <> Param "groupId"), ("APIAcceptMember", [], "Accept group member. Requires Admin role.", ["CRMemberAccepted", "CRChatCmdError"], [TD "CEGroupMemberNotActive" "Member is not connected yet"], Just UNBackground, "/_accept member #" <> Param "groupId" <> " " <> Param "groupMemberId" <> " " <> Param "memberRole"), - ("APIMembersRole", [], "Set members role. Requires Admin role.", ["CRMembersRoleUser"], [], Just UNBackground, "/_member role #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> " " <> Param "memberRole"), - ("APIBlockMembersForAll", [], "Block members. Requires Moderator role.", ["CRMembersBlockedForAllUser"], [], Just UNBackground, "/_block #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> OnOffParam "blocked" "blocked" Nothing), + ("APIMembersRole", [], "Set members role. Requires Admin role.", ["CRMembersRoleUser", "CRChatCmdError"], [], Just UNBackground, "/_member role #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> " " <> Param "memberRole"), + ("APIBlockMembersForAll", [], "Block members. Requires Moderator role.", ["CRMembersBlockedForAllUser", "CRChatCmdError"], [], Just UNBackground, "/_block #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> OnOffParam "blocked" "blocked" Nothing), ("APIRemoveMembers", [], "Remove members. Requires Admin role.", ["CRUserDeletedMembers", "CRChatCmdError"], ["CEGroupMemberNotFound"], Just UNBackground, "/_remove #" <> Param "groupId" <> " " <> Join ',' "groupMemberIds" <> OnOffParam "messages" "withMessages" (Just False)), - ("APILeaveGroup", [], "Leave group.", ["CRLeftMemberUser"], [], Just UNBackground, "/_leave #" <> Param "groupId"), - ("APIListMembers", [], "Get group members.", ["CRGroupMembers"], [], Nothing, "/_members #" <> Param "groupId"), - ("APINewGroup", [], "Create group.", ["CRGroupCreated"], [], Nothing, "/_group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Json "groupProfile"), - ("APIUpdateGroupProfile", [], "Update group profile.", ["CRGroupUpdated"], [], Just UNBackground, "/_group_profile #" <> Param "groupId" <> " " <> Json "groupProfile") + ("APILeaveGroup", [], "Leave group.", ["CRLeftMemberUser", "CRChatCmdError"], [], Just UNBackground, "/_leave #" <> Param "groupId"), + ("APIListMembers", [], "Get group members.", ["CRGroupMembers", "CRChatCmdError"], [], Nothing, "/_members #" <> Param "groupId"), + ("APINewGroup", [], "Create group.", ["CRGroupCreated", "CRChatCmdError"], [], Nothing, "/_group " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False) <> " " <> Json "groupProfile"), + ("APIUpdateGroupProfile", [], "Update group profile.", ["CRGroupUpdated", "CRChatCmdError"], [], Just UNBackground, "/_group_profile #" <> Param "groupId" <> " " <> Json "groupProfile") ] ), ( "Group link commands", "These commands can be used by bots that manage multiple public groups", - [ ("APICreateGroupLink", [], "Create group link.", ["CRGroupLinkCreated"], [], Just UNInteractive, "/_create link #" <> Param "groupId" <> " " <> Param "memberRole"), - ("APIGroupLinkMemberRole", [], "Set member role for group link.", ["CRGroupLink"], [], Nothing, "/_set link role #" <> Param "groupId" <> " " <> Param "memberRole"), - ("APIDeleteGroupLink", [], "Delete group link.", ["CRGroupLinkDeleted"], [], Just UNBackground, "/_delete link #" <> Param "groupId"), - ("APIGetGroupLink", [], "Get group link.", ["CRGroupLink"], [], Nothing, "/_get link #" <> Param "groupId") + [ ("APICreateGroupLink", [], "Create group link.", ["CRGroupLinkCreated", "CRChatCmdError"], [], Just UNInteractive, "/_create link #" <> Param "groupId" <> " " <> Param "memberRole"), + ("APIGroupLinkMemberRole", [], "Set member role for group link.", ["CRGroupLink", "CRChatCmdError"], [], Nothing, "/_set link role #" <> Param "groupId" <> " " <> Param "memberRole"), + ("APIDeleteGroupLink", [], "Delete group link.", ["CRGroupLinkDeleted", "CRChatCmdError"], [], Just UNBackground, "/_delete link #" <> Param "groupId"), + ("APIGetGroupLink", [], "Get group link.", ["CRGroupLink", "CRChatCmdError"], [], Nothing, "/_get link #" <> Param "groupId") ] ), ( "Connection commands", "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"], [], 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"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectionLink"), - ("APIConnect", [], "Connect via SimpleX link. The link can be 1-time invitation link, contact address or group link", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation"], [], Just UNInteractive, "/_connect " <> Param "userId" <> " " <> Param "connLink_"), - ("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"), - ("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected"], [], Nothing, "/_reject " <> Param "contactReqId") + [ ("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_"), + ("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") ] ), ( "Chat commands", "Commands to list and delete conversations.", - [ ("APIListContacts", [], "Get contacts.", ["CRContactsList"], [], Nothing, "/_contacts " <> Param "userId"), - ("APIListGroups", [], "Get groups.", ["CRGroupsList"], [], Nothing, "/_groups " <> Param "userId" <> Optional "" (" @" <> Param "$0") "contactId_" <> Optional "" (" " <> Param "$0") "search"), - ("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode") + [ ("APIListContacts", [], "Get contacts.", ["CRContactsList", "CRChatCmdError"], [], Nothing, "/_contacts " <> Param "userId"), + ("APIListGroups", [], "Get groups.", ["CRGroupsList", "CRChatCmdError"], [], Nothing, "/_groups " <> Param "userId" <> Optional "" (" @" <> Param "$0") "contactId_" <> Optional "" (" " <> Param "$0") "search"), + ("APIDeleteChat", [], "Delete chat.", ["CRContactDeleted", "CRContactConnectionDeleted", "CRGroupDeletedUser", "CRChatCmdError"], [], Just UNBackground, "/_delete " <> Param "chatRef" <> " " <> Param "chatDeleteMode") -- ("APIChatItemsRead", [], "Mark items as read.", ["CRItemsReadForChat"], [], Nothing, ""), -- ("APIChatRead", [], "Mark chat as read.", ["CRCmdOk"], [], Nothing, ""), -- ("APIChatUnread", [], "Mark chat as unread.", ["CRCmdOk"], [], Nothing, ""), @@ -162,20 +163,20 @@ 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"], [], Nothing, "/user"), + [ ("ShowActiveUser", [], "Get active user profile", ["CRActiveUser", "CRChatCmdError"], [], Nothing, "/user"), ( "CreateActiveUser", [], "Create new user profile", - ["CRActiveUser"], + ["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"], [], Nothing, "/users"), - ("APISetActiveUser", [], "Set active user profile", ["CRActiveUser"], ["CEChatNotStarted"], Nothing, "/_user " <> Param "userId" <> Optional "" (" " <> Json "$0") "viewPwd"), - ("APIDeleteUser", [], "Delete user profile.", ["CRCmdOk"], [], Just UNBackground, "/_delete user " <> Param "userId" <> OnOffParam "del_smp" "delSMPQueues" Nothing <> Optional "" (" " <> Json "$0") "viewPwd"), - ("APIUpdateProfile", [], "Update user profile.", ["CRUserProfileUpdated"], [], Just UNBackground, "/_profile " <> Param "userId" <> " " <> Json "profile"), - ("APISetContactPrefs", [], "Configure chat preference overrides for the contact.", ["CRContactPrefsUpdated"], [], Just UNBackground, "/_set prefs @" <> Param "contactId" <> " " <> Json "preferences") + ("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") ] ) ] @@ -193,7 +194,6 @@ cliCommands = "ClearContact", "ClearGroup", "ClearNoteFolder", - "Connect", "ConnectSimplex", "ContactInfo", "ContactQueueInfo", diff --git a/bots/src/API/Docs/Generate.hs b/bots/src/API/Docs/Generate.hs index ad8f68bd36..334ad93bad 100644 --- a/bots/src/API/Docs/Generate.hs +++ b/bots/src/API/Docs/Generate.hs @@ -43,7 +43,7 @@ commandsDocText = <> foldMap commandDocText commands where commandDocText CCDoc {commandType = ATUnionMember tag params, commandDescr, network, syntax, responses, errors} = - ("\n\n### " <> T.pack (fstToUpper tag) <> "\n\n" <> commandDescr <> "\n\n*Network usage*: " <> networkUsage <> ".\n") + ("\n\n### " <> T.pack (fstToUpper tag) <> "\n\n" <> commandDescr <> "\n\n*Network usage*: " <> networkUsage network <> ".\n") <> (if null params then "" else paramsText) <> (if syntax == "" then "" else syntaxText (tag, params) syntax) <> (if length responses > 1 then "\n**Responses**:\n" else "\n**Response**:\n") @@ -52,10 +52,6 @@ commandsDocText = <> foldMap errorText errors <> "\n---\n" where - networkUsage = case network of - Nothing -> "no" - Just UNInteractive -> "interactive" - Just UNBackground -> "background" paramsText = "\n**Parameters**:\n" <> fieldsText "./TYPES.md" params responseText CRDoc {responseType = ATUnionMember tag fields, responseDescr} = (T.pack $ "\n" <> fstToUpper tag <> ": " <> respDescr <> ".\n") @@ -67,11 +63,17 @@ commandsDocText = let descr' = if null descr then camelToSpace err else descr in T.pack $ "- " <> fstToUpper err <> ": " <> descr' <> ".\n" +networkUsage :: Maybe UsesNetwork -> Text +networkUsage = \case + Nothing -> "no" + Just UNInteractive -> "interactive" + Just UNBackground -> "background" + 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 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 @@ -127,14 +129,14 @@ typesDocText = where self = APIRecordField "self" (ATDef td) typeFields = case typeDef of - ATDRecord fs -> L.toList fs + ATDRecord fs -> fs ATDUnion ms -> APIRecordField "type" tagType : concatMap (\(ATUnionMember _ fs) -> fs) ms where tagType = ATDef $ APITypeDef (name <> ".type") $ ATDEnum tags tags = L.map (\(ATUnionMember tag _) -> tag) ms ATDEnum _ -> [] typeDefText = \case - ATDRecord fields -> "\n**Record type**:\n" <> fieldsText "" (L.toList fields) + ATDRecord fields -> "\n**Record type**:\n" <> fieldsText "" fields ATDEnum cs -> "\n**Enum type**:\n" <> foldMap (\m -> "- \"" <> T.pack m <> "\"\n") cs ATDUnion cs -> "\n**Discriminated union type**:\n" <> foldMap constrText cs where diff --git a/bots/src/API/Docs/Generate/TypeScript.hs b/bots/src/API/Docs/Generate/TypeScript.hs new file mode 100644 index 0000000000..b69635086e --- /dev/null +++ b/bots/src/API/Docs/Generate/TypeScript.hs @@ -0,0 +1,189 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module API.Docs.Generate.TypeScript where + +import API.Docs.Commands +import API.Docs.Events +import API.Docs.Generate +import API.Docs.Responses +import API.Docs.Syntax +import API.Docs.Syntax.Types +import API.Docs.Types +import API.TypeInfo +import Data.Char (toUpper) +import qualified Data.List.NonEmpty as L +import Data.Text (Text) +import qualified Data.Text as T + +commandsCodeFile :: FilePath +commandsCodeFile = "./packages/simplex-chat-client/types/typescript/src/commands.ts" + +responsesCodeFile :: FilePath +responsesCodeFile = "./packages/simplex-chat-client/types/typescript/src/responses.ts" + +eventsCodeFile :: FilePath +eventsCodeFile = "./packages/simplex-chat-client/types/typescript/src/events.ts" + +typesCodeFile :: FilePath +typesCodeFile = "./packages/simplex-chat-client/types/typescript/src/types.ts" + +commandsCodeText :: Text +commandsCodeText = + ("// API Commands\n// " <> autoGenerated <> "\n") + <> "\nimport * as T from \"./types\"\n" + <> "\nimport {CR} from \"./responses\"\n" + <> foldMap commandCatCode chatCommandsDocs + where + commandCatCode CCCategory {categoryName, categoryDescr, commands} = + (T.pack $ "\n// " <> categoryName <> "\n// " <> categoryDescr <> "\n") + <> foldMap commandCode commands + where + commandCode CCDoc {commandType = ATUnionMember tag params, commandDescr, syntax, responses, network} = + ("\n// " <> commandDescr <> "\n") + <> ("// Network usage: " <> networkUsage network <> ".\n") + <> ("export interface " <> T.pack constrName <> " {\n") + <> fieldsCode "" "T." params + <> "}\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) + <> "}\n" + where + constrName = fstToUpper tag + +responsesCodeText :: Text +responsesCodeText = + ("// API Responses\n// " <> autoGenerated <> "\n") + <> "\nimport * as T from \"./types\"\n" + <> unionTypeCode "CR" "T." chatRespTypeDef chatRespConstrs "" + where + chatRespTypeDef = APITypeDef {typeName' = "ChatResponse", typeDef = ATDUnion chatRespConstrs} + chatRespConstrs = L.fromList $ map responseType chatResponsesDocs + +eventsCodeText :: Text +eventsCodeText = + ("// API Events\n// " <> autoGenerated <> "\n") + <> "\nimport * as T from \"./types\"\n" + <> unionTypeCode "CEvt" "T." chatEventTypeDef chatEventConstrs "" + where + chatEventTypeDef = APITypeDef {typeName' = "ChatEvent", typeDef = ATDUnion chatEventConstrs} + chatEventConstrs = L.fromList $ concatMap catEvents chatEventsDocs + catEvents CECategory {mainEvents, otherEvents} = map eventType $ mainEvents ++ otherEvents + +typesCodeText :: Text +typesCodeText = ("// API Types\n// " <> autoGenerated <> "\n") <> foldMap typeCode chatTypesDocs + where + typeCode CTDoc {typeDef = td@APITypeDef {typeName' = name, typeDef}, typeDescr, typeSyntax} = + (if T.null typeDescr then "" else "// " <> typeDescr <> "\n") + <> typeDefCode + -- <> (if typeSyntax == "" then "" else syntaxText (name, self : typeFields) typeSyntax) + where + name' = T.pack name + constrName tag = case name of + "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" + typeDefCode = case typeDef of + ATDRecord fields -> + ("\nexport interface " <> name' <> " {\n") + <> fieldsCode "" "" fields + <> "}\n" + <> (if typeSyntax == "" then "" else namespaceFuncCode) + ATDEnum cs -> + ("\nexport enum " <> name' <> " {\n") + <> foldMap (\m -> " " <> constrName m <> " = \"" <> T.pack m <> "\",\n") cs + <> "}\n" + <> (if typeSyntax == "" then "" else namespaceFuncCode) + ATDUnion cs -> unionTypeCode name' "" td cs typeSyntax + +unionTypeCode :: Text -> String -> APITypeDef -> L.NonEmpty ATUnionMember -> Expr -> Text +unionTypeCode unionNamespace typesNamespace td@APITypeDef {typeName' = name} cs cmdSyntax = + ("\nexport type " <> name' <> " = " <> constrsCode "" name' constrTypeRef (L.toList cs) <> "\n") + <> ("export namespace " <> unionNamespace <> " {\n") + <> (" 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) + <> "}\n" + where + name' = T.pack name + constrTypeRef (ATUnionMember tag _) = unionNamespace <> "." <> constrName tag + constrTag (ATUnionMember tag _) = T.pack $ "\"" <> tag <> "\"" + constrType c@(ATUnionMember tag fields) = + ("\n export interface " <> constrName tag <> " extends Interface {\n") + <> " type: " <> constrTag c <> "\n" + <> fieldsCode " " typesNamespace fields + <> " }\n" + constrName tag = T.replace "-" "_" (T.pack $ fstToUpper tag) + +constrsCode :: Text -> Text -> (ATUnionMember -> Text) -> [ATUnionMember] -> Text +constrsCode indent name' constr cs + | T.length (name' <> " = " <> line) <= 100 = line <> "\n" + | otherwise = "\n" <> foldMap (\c -> indent <> " | " <> c <> "\n") cs' + where + 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" + where + param = if hasParams cmdSyntax then "self" else "_self" + self = APIRecordField "self" (ATDef td) + typeFields = case typeDef of + ATDRecord fs -> fs + ATDUnion ms -> APIRecordField "type" tagType : concatMap (\(ATUnionMember _ fs) -> fs) ms + where + tagType = ATDef $ APITypeDef (name <> ".type") $ ATDEnum tags + tags = L.map (\(ATUnionMember tag _) -> tag) ms + ATDEnum _ -> [] + +fieldsCode :: Text -> String -> [APIRecordField] -> Text +fieldsCode indent namespace = foldMap $ (indent <>) . T.pack . fieldText + where + fieldText (APIRecordField name t) = " " <> name <> optional t <> ": " <> typeText t <> typeComment t <> "\n" + optional = \case + ATOptional _ -> "?" + _ -> "" + typeText = \case + ATPrim (PT t) -> typeName t + ATDef (APITypeDef t _) -> namespace <> t + ATRef t -> namespace <> t + ATOptional t -> typeText t + ATArray {elemType} -> typeText elemType <> "[]" + ATMap (PT t) valueType -> "{[key: " <> typeName t <> "]: " <> typeText valueType <> "}" + typeName = \case + TBool -> "boolean" + TInt -> "number" + TInt64 -> "number" + TWord32 -> "number" + TDouble -> "number" + TJSONObject -> "object" + TUTCTime -> "string" + t -> t + typeComment t = let c = typeComment' t in if null c then "" else " // " <> c + typeComment' = \case + ATPrim (PT t) -> typeComment_ t + ATOptional (ATPrim (PT t)) -> typeComment_ t + ATArray {elemType, nonEmpty} + | nonEmpty -> (if null c then "" else c <> ", ") <> "non-empty" + | otherwise -> c + where + c = typeComment' elemType + ATMap (PT k) v -> + let kc = typeComment_ k + vc = typeComment' v + tc t c = if null c then t else c + in if null kc && null vc then "" else tc (typeName k) kc <> " : " <> tc (typeText v) vc + _ -> "" + typeComment_ = \case + TInt -> "int" + TInt64 -> "int64" + TWord32 -> "word32" + TDouble -> "double" + TUTCTime -> "ISO-8601 timestamp" + _ -> "" diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 96f461e696..1c2e4baec3 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -88,6 +88,7 @@ chatResponsesDocsData = ("CRUserContactLinkUpdated", "User contact address updated"), ("CRUserDeletedMembers", "Members deleted"), ("CRUserProfileUpdated", "User profile updated"), + ("CRUserProfileNoChange", "User profile was not changed"), ("CRUsersList", "Users") -- ("CRApiChat", "Chat and messages"), -- ("CRApiChats", "Chats with the most recent messages"), @@ -195,7 +196,6 @@ undocumentedResponses = "CRUserPrivacy", "CRUserProfile", "CRUserProfileImage", - "CRUserProfileNoChange", "CRUserServers", "CRUserServersValidation", "CRVersionInfo", diff --git a/bots/src/API/Docs/Syntax.hs b/bots/src/API/Docs/Syntax.hs index df6e126ca4..83fa2bf6a2 100644 --- a/bots/src/API/Docs/Syntax.hs +++ b/bots/src/API/Docs/Syntax.hs @@ -99,8 +99,8 @@ withOptBoolParam r param p f = (ATOptional (ATPrim (PT TBool))) -> f True _ -> paramError r param p "is not [optional] boolean" -jsSyntaxText :: TypeAndFields -> Expr -> Text -jsSyntaxText r = T.replace "' + '" "" . T.pack . go Nothing True +jsSyntaxText :: Bool -> TypeAndFields -> Expr -> Text +jsSyntaxText useSelf r = T.replace "' + '" "" . T.pack . go Nothing True where go param top = \case Concat exs -> intercalate " + " $ map (go param False) $ L.toList exs @@ -109,14 +109,14 @@ jsSyntaxText r = T.replace "' + '" "" . T.pack . go Nothing True withParamType r param p $ \case ATDef td -> toStringSyntax td ATOptional (ATDef td) -> toStringSyntax td - _ -> paramName param p + _ -> paramName' useSelf param p where toStringSyntax (APITypeDef typeName _) - | typeHasSyntax typeName = paramName param p <> ".toString()" - | otherwise = paramName param p + | typeHasSyntax typeName = paramName' useSelf param p <> ".toString()" + | otherwise = paramName' useSelf param p Optional exN exJ p -> open <> n <> " ? " <> go (Just p) False exJ <> " : " <> nothing <> close where - n = paramName param p + n = paramName' useSelf param p nothing = if exN == "" then "''" else go param False exN Choice p opts else' -> withParamType r param p $ \case @@ -125,15 +125,15 @@ jsSyntaxText r = T.replace "' + '" "" . T.pack . go Nothing True _ -> paramError r param p "is not union type" where choiceSyntax = \case - APITypeDef _ (ATDUnion _) -> choices "type" + APITypeDef _ (ATDUnion _) -> choices $ (if useSelf then "self." else "") <> "type" APITypeDef _ (ATDEnum _) -> choices "self" _ -> paramError r param p "is not union type" choices var = open <> optsSyntax <> " : " <> go param top else' <> close where optsSyntax = intercalate " : " $ map (\(tag, ex) -> var <> " == '" <> tag <> "' ? " <> go param top ex) $ L.toList opts - Join c p -> paramName param p <> ".join('" <> [c] <> "')" - Json p -> "JSON.stringify(" <> paramName param p <> ")" - OnOff p -> open <> paramName param p <> " ? 'on' : 'off'" <> close + Join c p -> paramName' useSelf param p <> ".join('" <> [c] <> "')" + Json p -> "JSON.stringify(" <> paramName' useSelf param p <> ")" + OnOff p -> open <> paramName' useSelf param p <> " ? 'on' : 'off'" <> close OnOffParam name p def_ -> case def_ of Nothing -> withOptBoolParam r param p $ \optional -> @@ -141,13 +141,13 @@ jsSyntaxText r = T.replace "' + '" "" . T.pack . go Nothing True then "(typeof " <> n <> " == 'boolean' ? " <> res <> " : '')" else res where - n = paramName param p + n = paramName' useSelf param p res = "' " <> name <> "=' + (" <> n <> " ? 'on' : 'off')" Just def | def -> open <> "!" <> n <> " ? ' " <> name <> "=off' : ''" <> close | otherwise -> open <> n <> " ? ' " <> name <> "=on' : ''" <> close where - n = paramName param p + n = paramName' useSelf param p where open = if top then "" else "(" close = if top then "" else ")" @@ -215,6 +215,13 @@ pySyntaxText r = T.pack . go Nothing True close = if top then "" else ")" paramName :: Maybe ExprParam -> ExprParam -> String -paramName param_ p = case param_ of - Just param | p == "$0" -> param - _ -> p +paramName = paramName' False + +paramName' :: Bool -> Maybe ExprParam -> ExprParam -> String +paramName' useSelf param_ p + | useSelf && p' /= "self" = "self." <> p' + | otherwise = p' + where + p' = case param_ of + Just param | p == "$0" -> param + _ -> p diff --git a/bots/src/API/Docs/Syntax/Types.hs b/bots/src/API/Docs/Syntax/Types.hs index 211348d5a9..385c5e02c2 100644 --- a/bots/src/API/Docs/Syntax/Types.hs +++ b/bots/src/API/Docs/Syntax/Types.hs @@ -26,6 +26,18 @@ isConst = \case Const _ -> True _ -> False +hasParams :: Expr -> Bool +hasParams = \case + Concat es -> any (hasParams) es + Const _ -> False + Param _ -> True + Optional {} -> True + Choice {} -> True + Join {} -> True + Json _ -> True + OnOff _ -> True + OnOffParam {} -> True + instance IsString Expr where fromString = Const instance Semigroup Expr where diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index c0cf2867c2..758fc62d53 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -83,7 +83,7 @@ toTypeDef acc@(!visited, !typeDefs) (STI typeName allConstrs, jsonEncoding, cons [RecordTypeInfo {fieldInfos}] -> let fields = fromMaybe (error $ "Record type without fields: " <> typeName) $ L.nonEmpty fieldInfos ((visited', typeDefs'), fields') = mapAccumL (toAPIField_ typeName) (S.insert typeName visited, typeDefs) fields - td = APITypeDef typeName $ ATDRecord fields' + td = APITypeDef typeName $ ATDRecord $ L.toList fields' in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td) _ -> error $ "Record type with " <> show (length constrs) <> " constructors: " <> typeName STUnion -> if length constrs > 1 then toUnionType constrs else unionError constrs @@ -199,7 +199,7 @@ chatTypesDocsData = (sti @(ContactUserPreference SimplePreference), STRecord, "", [], "", ""), (sti @(CreatedConnLink 'CMContact), STRecord, "", [], Param "connFullLink" <> Optional "" (" " <> Param "$0") "connShortLink", ""), (sti @AddressSettings, STRecord, "", [], "", ""), - (sti @AgentCryptoError, STUnion, "", [], "", ""), + (sti @AgentCryptoError, STUnion, "", ["RATCHET_EARLIER", "RATCHET_SKIPPED"], "", ""), -- TODO add fields to types (sti @AgentErrorType, STUnion, "", [], "", ""), (sti @AutoAccept, STRecord, "", [], "", ""), (sti @BlockingInfo, STRecord, "", [], "", ""), @@ -217,7 +217,7 @@ chatTypesDocsData = (sti @ChatRef, STRecord, "", [], Param "chatType" <> Param "chatId" <> Optional "" (Param "$0") "chatScope", "Used in API commands. Chat scope can only be passed with groups."), (sti @ChatSettings, STRecord, "", [], "", ""), (sti @ChatStats, STRecord, "", [], "", ""), - (sti @ChatType, STEnum, "CT", ["CTContactRequest", "CTContactConnection"], Choice "self" [("contact", "@"), ("group", "#"), ("local", "*")] "", ""), + (sti @ChatType, STEnum, "CT", ["CTContactRequest", "CTContactConnection"], Choice "self" [("direct", "@"), ("group", "#"), ("local", "*")] "", ""), (sti @ChatWallpaper, STRecord, "", [], "", ""), (sti @ChatWallpaperScale, STEnum, "CWS", [], "", ""), (sti @CICallStatus, STEnum, "CISCall", [], "", ""), diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index 3d7269e766..df43374ffa 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -33,7 +33,7 @@ data APIType data APITypeDef = APITypeDef {typeName' :: String, typeDef :: APITypeDefinition} data APITypeDefinition - = ATDRecord (NonEmpty APIRecordField) + = ATDRecord [APIRecordField] | ATDUnion (NonEmpty ATUnionMember) | ATDEnum (NonEmpty String) @@ -45,10 +45,6 @@ data ATUnionMember = ATUnionMember {memberTag :: String, memberFields :: [APIRec newtype PrimitiveType = PT String --- data PrimitiveType = PTBool | PTString StringFormat | PTInt | PTInt64 | PTWord32 | PTDouble - -data StringFormat = SFTimestamp | SFBase64 | SFSimpleXLink | SFServerAddr | SFHexColor - pattern TBool :: String pattern TBool = "bool" @@ -61,8 +57,20 @@ pattern TInt = "int" pattern TInt64 :: String pattern TInt64 = "int64" +pattern TWord32 :: String +pattern TWord32 = "word32" + +pattern TDouble :: String +pattern TDouble = "double" + +pattern TJSONObject :: String +pattern TJSONObject = "JSONObject" + +pattern TUTCTime :: String +pattern TUTCTime = "UTCTime" + primitiveTypes :: [ConsName] -primitiveTypes = [TBool, TString, TInt, TInt64, "word32", "double", "JSONObject", "UTCTime"] +primitiveTypes = [TBool, TString, TInt, TInt64, TWord32, TDouble, TJSONObject, TUTCTime] data SumTypeInfo = STI {typeName :: String, recordTypes :: [RecordTypeInfo]} deriving (Show) diff --git a/packages/simplex-chat-client/types/typescript/.gitignore b/packages/simplex-chat-client/types/typescript/.gitignore new file mode 100644 index 0000000000..7481e30d5a --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/.gitignore @@ -0,0 +1,4 @@ +node_modules +package-lock.json +dist +coverage diff --git a/packages/simplex-chat-client/types/typescript/LICENSE b/packages/simplex-chat-client/types/typescript/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/simplex-chat-client/types/typescript/README.md b/packages/simplex-chat-client/types/typescript/README.md new file mode 100644 index 0000000000..ad13a5d76e --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/README.md @@ -0,0 +1,11 @@ +# SimpleX Chat bots API: typeScript types + +This TypeScript library provides auto-generated types for bots API: commands and responses, events and all types they use. + +It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) library that uses WebSockets interface of SimpleX Chat CLI. + +[API reference](https://github.com/simplex-chat/simplex-chat/tree/stable/bots). + +## License + +[AGPL v3](./LICENSE) diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json new file mode 100644 index 0000000000..1bf593b483 --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -0,0 +1,42 @@ +{ + "name": "@simplex-chat/types", + "version": "0.1.0", + "description": "TypeScript types for SimpleX Chat bot libraries", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc", + "prepublishOnly": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/simplex-chat/simplex-chat.git", + "directory": "/packages/simplex-chat-client/types/typescript" + }, + "keywords": [ + "simplex", + "messenger", + "chat", + "privacy", + "security", + "bots", + "types" + ], + "author": "SimpleX Chat", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/simplex-chat/simplex-chat/issues" + }, + "homepage": "https://github.com/simplex-chat/simplex-chat#readme", + "dependencies": { + "typescript": "^5.9.2" + } +} diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts new file mode 100644 index 0000000000..de6a7a7ce1 --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -0,0 +1,662 @@ +// API Commands +// This file is generated automatically. + +import * as T from "./types" + +import {CR} from "./responses" + +// Address commands +// Bots can use these commands to automatically check and create address when initialized + +// Create bot address. +// Network usage: interactive. +export interface APICreateMyAddress { + userId: number // int64 +} + +export namespace APICreateMyAddress { + export type Response = CR.UserContactLinkCreated | CR.ChatCmdError + + export function cmdString(self: APICreateMyAddress): string { + return '/_address ' + self.userId + } +} + +// Delete bot address. +// Network usage: background. +export interface APIDeleteMyAddress { + userId: number // int64 +} + +export namespace APIDeleteMyAddress { + export type Response = CR.UserContactLinkDeleted | CR.ChatCmdError + + export function cmdString(self: APIDeleteMyAddress): string { + return '/_delete_address ' + self.userId + } +} + +// Get bot address and settings. +// Network usage: no. +export interface APIShowMyAddress { + userId: number // int64 +} + +export namespace APIShowMyAddress { + export type Response = CR.UserContactLink | CR.ChatCmdError + + export function cmdString(self: APIShowMyAddress): string { + return '/_show_address ' + self.userId + } +} + +// Add address to bot profile. +// Network usage: interactive. +export interface APISetProfileAddress { + userId: number // int64 + enable: boolean +} + +export namespace APISetProfileAddress { + export type Response = CR.UserProfileUpdated | CR.ChatCmdError + + export function cmdString(self: APISetProfileAddress): string { + return '/_profile_address ' + self.userId + ' ' + (self.enable ? 'on' : 'off') + } +} + +// Set bot address settings. +// Network usage: interactive. +export interface APISetAddressSettings { + userId: number // int64 + settings: T.AddressSettings +} + +export namespace APISetAddressSettings { + export type Response = CR.UserContactLinkUpdated | CR.ChatCmdError + + export function cmdString(self: APISetAddressSettings): string { + return '/_address_settings ' + self.userId + ' ' + JSON.stringify(self.settings) + } +} + +// Message commands +// Commands to send, update, delete, moderate messages and set message reactions + +// Send messages. +// Network usage: background. +export interface APISendMessages { + sendRef: T.ChatRef + liveMessage: boolean + ttl?: number // int + composedMessages: T.ComposedMessage[] // non-empty +} + +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) + } +} + +// Update message. +// Network usage: background. +export interface APIUpdateChatItem { + chatRef: T.ChatRef + chatItemId: number // int64 + liveMessage: boolean + updatedMessage: T.UpdatedMessage +} + +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) + } +} + +// Delete message. +// Network usage: background. +export interface APIDeleteChatItem { + chatRef: T.ChatRef + chatItemIds: number[] // int64, non-empty + deleteMode: T.CIDeleteMode +} + +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 + } +} + +// Moderate message. Requires Moderator role (and higher than message author's). +// Network usage: background. +export interface APIDeleteMemberChatItem { + groupId: number // int64 + chatItemIds: number[] // int64, non-empty +} + +export namespace APIDeleteMemberChatItem { + export type Response = CR.ChatItemsDeleted | CR.ChatCmdError + + export function cmdString(self: APIDeleteMemberChatItem): string { + return '/_delete member item #' + self.groupId + ' ' + self.chatItemIds.join(',') + } +} + +// Add/remove message reaction. +// Network usage: background. +export interface APIChatItemReaction { + chatRef: T.ChatRef + chatItemId: number // int64 + add: boolean + reaction: T.MsgReaction +} + +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) + } +} + +// File commands +// Commands to receive and to cancel files. Files are sent as part of the message, there are no separate commands to send files. + +// Receive file. +// Network usage: no. +export interface ReceiveFile { + fileId: number // int64 + userApprovedRelays: boolean + storeEncrypted?: boolean + fileInline?: boolean + filePath?: string +} + +export namespace ReceiveFile { + export type Response = CR.RcvFileAccepted | CR.RcvFileAcceptedSndCancelled | CR.ChatCmdError + + export function cmdString(self: ReceiveFile): string { + return '/freceive ' + self.fileId + (self.userApprovedRelays ? ' approved_relays=on' : '') + (typeof self.storeEncrypted == 'boolean' ? ' encrypt=' + (self.storeEncrypted ? 'on' : 'off') : '') + (typeof self.fileInline == 'boolean' ? ' inline=' + (self.fileInline ? 'on' : 'off') : '') + (self.filePath ? ' ' + self.filePath : '') + } +} + +// Cancel file. +// Network usage: background. +export interface CancelFile { + fileId: number // int64 +} + +export namespace CancelFile { + export type Response = CR.SndFileCancelled | CR.RcvFileCancelled | CR.ChatCmdError + + export function cmdString(self: CancelFile): string { + return '/fcancel ' + self.fileId + } +} + +// Group commands +// Commands to manage and moderate groups. These commands can be used with business chats as well - they are groups. E.g., a common scenario would be to add human agents to business chat with the customer who connected via business address. + +// Add contact to group. Requires bot to have Admin role. +// Network usage: interactive. +export interface APIAddMember { + groupId: number // int64 + contactId: number // int64 + memberRole: T.GroupMemberRole +} + +export namespace APIAddMember { + export type Response = CR.SentGroupInvitation | CR.ChatCmdError + + export function cmdString(self: APIAddMember): string { + return '/_add #' + self.groupId + ' ' + self.contactId + ' ' + self.memberRole + } +} + +// Join group. +// Network usage: interactive. +export interface APIJoinGroup { + groupId: number // int64 +} + +export namespace APIJoinGroup { + export type Response = CR.UserAcceptedGroupSent | CR.ChatCmdError + + export function cmdString(self: APIJoinGroup): string { + return '/_join #' + self.groupId + } +} + +// Accept group member. Requires Admin role. +// Network usage: background. +export interface APIAcceptMember { + groupId: number // int64 + groupMemberId: number // int64 + memberRole: T.GroupMemberRole +} + +export namespace APIAcceptMember { + export type Response = CR.MemberAccepted | CR.ChatCmdError + + export function cmdString(self: APIAcceptMember): string { + return '/_accept member #' + self.groupId + ' ' + self.groupMemberId + ' ' + self.memberRole + } +} + +// Set members role. Requires Admin role. +// Network usage: background. +export interface APIMembersRole { + groupId: number // int64 + groupMemberIds: number[] // int64, non-empty + memberRole: T.GroupMemberRole +} + +export namespace APIMembersRole { + export type Response = CR.MembersRoleUser | CR.ChatCmdError + + export function cmdString(self: APIMembersRole): string { + return '/_member role #' + self.groupId + ' ' + self.groupMemberIds.join(',') + ' ' + self.memberRole + } +} + +// Block members. Requires Moderator role. +// Network usage: background. +export interface APIBlockMembersForAll { + groupId: number // int64 + groupMemberIds: number[] // int64, non-empty + blocked: boolean +} + +export namespace APIBlockMembersForAll { + export type Response = CR.MembersBlockedForAllUser | CR.ChatCmdError + + export function cmdString(self: APIBlockMembersForAll): string { + return '/_block #' + self.groupId + ' ' + self.groupMemberIds.join(',') + ' blocked=' + (self.blocked ? 'on' : 'off') + } +} + +// Remove members. Requires Admin role. +// Network usage: background. +export interface APIRemoveMembers { + groupId: number // int64 + groupMemberIds: number[] // int64, non-empty + withMessages: boolean +} + +export namespace APIRemoveMembers { + export type Response = CR.UserDeletedMembers | CR.ChatCmdError + + export function cmdString(self: APIRemoveMembers): string { + return '/_remove #' + self.groupId + ' ' + self.groupMemberIds.join(',') + (self.withMessages ? ' messages=on' : '') + } +} + +// Leave group. +// Network usage: background. +export interface APILeaveGroup { + groupId: number // int64 +} + +export namespace APILeaveGroup { + export type Response = CR.LeftMemberUser | CR.ChatCmdError + + export function cmdString(self: APILeaveGroup): string { + return '/_leave #' + self.groupId + } +} + +// Get group members. +// Network usage: no. +export interface APIListMembers { + groupId: number // int64 +} + +export namespace APIListMembers { + export type Response = CR.GroupMembers | CR.ChatCmdError + + export function cmdString(self: APIListMembers): string { + return '/_members #' + self.groupId + } +} + +// Create group. +// Network usage: no. +export interface APINewGroup { + userId: number // int64 + incognito: boolean + groupProfile: T.GroupProfile +} + +export namespace APINewGroup { + export type Response = CR.GroupCreated | CR.ChatCmdError + + export function cmdString(self: APINewGroup): string { + return '/_group ' + self.userId + (self.incognito ? ' incognito=on' : '') + ' ' + JSON.stringify(self.groupProfile) + } +} + +// Update group profile. +// Network usage: background. +export interface APIUpdateGroupProfile { + groupId: number // int64 + groupProfile: T.GroupProfile +} + +export namespace APIUpdateGroupProfile { + export type Response = CR.GroupUpdated | CR.ChatCmdError + + export function cmdString(self: APIUpdateGroupProfile): string { + return '/_group_profile #' + self.groupId + ' ' + JSON.stringify(self.groupProfile) + } +} + +// Group link commands +// These commands can be used by bots that manage multiple public groups + +// Create group link. +// Network usage: interactive. +export interface APICreateGroupLink { + groupId: number // int64 + memberRole: T.GroupMemberRole +} + +export namespace APICreateGroupLink { + export type Response = CR.GroupLinkCreated | CR.ChatCmdError + + export function cmdString(self: APICreateGroupLink): string { + return '/_create link #' + self.groupId + ' ' + self.memberRole + } +} + +// Set member role for group link. +// Network usage: no. +export interface APIGroupLinkMemberRole { + groupId: number // int64 + memberRole: T.GroupMemberRole +} + +export namespace APIGroupLinkMemberRole { + export type Response = CR.GroupLink | CR.ChatCmdError + + export function cmdString(self: APIGroupLinkMemberRole): string { + return '/_set link role #' + self.groupId + ' ' + self.memberRole + } +} + +// Delete group link. +// Network usage: background. +export interface APIDeleteGroupLink { + groupId: number // int64 +} + +export namespace APIDeleteGroupLink { + export type Response = CR.GroupLinkDeleted | CR.ChatCmdError + + export function cmdString(self: APIDeleteGroupLink): string { + return '/_delete link #' + self.groupId + } +} + +// Get group link. +// Network usage: no. +export interface APIGetGroupLink { + groupId: number // int64 +} + +export namespace APIGetGroupLink { + export type Response = CR.GroupLink | CR.ChatCmdError + + export function cmdString(self: APIGetGroupLink): string { + return '/_get link #' + self.groupId + } +} + +// Connection commands +// 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. + +// Create 1-time invitation link. +// Network usage: interactive. +export interface APIAddContact { + userId: number // int64 + incognito: boolean +} + +export namespace APIAddContact { + export type Response = CR.Invitation | CR.ChatCmdError + + export function cmdString(self: APIAddContact): string { + return '/_connect ' + self.userId + (self.incognito ? ' incognito=on' : '') + } +} + +// Determine SimpleX link type and if the bot is already connected via this link. +// Network usage: interactive. +export interface APIConnectPlan { + userId: number // int64 + connectionLink?: string +} + +export namespace APIConnectPlan { + export type Response = CR.ConnectionPlan | CR.ChatCmdError + + export function cmdString(self: APIConnectPlan): string { + return '/_connect plan ' + self.userId + ' ' + self.connectionLink + } +} + +// 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 + incognito: boolean + preparedLink_?: T.CreatedConnLink +} + +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() : '') + } +} + +// Connect via SimpleX link as string in the active user profile. +// Network usage: interactive. +export interface Connect { + incognito: boolean + connLink_?: string +} + +export namespace Connect { + export type Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError + + export function cmdString(self: Connect): string { + return '/connect' + (self.connLink_ ? ' ' + self.connLink_ : '') + } +} + +// Accept contact request. +// Network usage: interactive. +export interface APIAcceptContact { + contactReqId: number // int64 +} + +export namespace APIAcceptContact { + export type Response = CR.AcceptingContactRequest | CR.ChatCmdError + + export function cmdString(self: APIAcceptContact): string { + return '/_accept ' + self.contactReqId + } +} + +// Reject contact request. The user who sent the request is **not notified**. +// Network usage: no. +export interface APIRejectContact { + contactReqId: number // int64 +} + +export namespace APIRejectContact { + export type Response = CR.ContactRequestRejected | CR.ChatCmdError + + export function cmdString(self: APIRejectContact): string { + return '/_reject ' + self.contactReqId + } +} + +// Chat commands +// Commands to list and delete conversations. + +// Get contacts. +// Network usage: no. +export interface APIListContacts { + userId: number // int64 +} + +export namespace APIListContacts { + export type Response = CR.ContactsList | CR.ChatCmdError + + export function cmdString(self: APIListContacts): string { + return '/_contacts ' + self.userId + } +} + +// Get groups. +// Network usage: no. +export interface APIListGroups { + userId: number // int64 + contactId_?: number // int64 + search?: string +} + +export namespace APIListGroups { + export type Response = CR.GroupsList | CR.ChatCmdError + + export function cmdString(self: APIListGroups): string { + return '/_groups ' + self.userId + (self.contactId_ ? ' @' + self.contactId_ : '') + (self.search ? ' ' + self.search : '') + } +} + +// Delete chat. +// Network usage: background. +export interface APIDeleteChat { + chatRef: T.ChatRef + chatDeleteMode: T.ChatDeleteMode +} + +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() + } +} + +// 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 +// Network usage: no. +export interface ShowActiveUser { +} + +export namespace ShowActiveUser { + export type Response = CR.ActiveUser | CR.ChatCmdError + + export function cmdString(_self: ShowActiveUser): string { + return '/user' + } +} + +// Create new user profile +// Network usage: no. +export interface CreateActiveUser { + newUser: T.NewUser +} + +export namespace CreateActiveUser { + export type Response = CR.ActiveUser | CR.ChatCmdError + + export function cmdString(self: CreateActiveUser): string { + return '/_create user ' + JSON.stringify(self.newUser) + } +} + +// Get all user profiles +// Network usage: no. +export interface ListUsers { +} + +export namespace ListUsers { + export type Response = CR.UsersList | CR.ChatCmdError + + export function cmdString(_self: ListUsers): string { + return '/users' + } +} + +// Set active user profile +// Network usage: no. +export interface APISetActiveUser { + userId: number // int64 + viewPwd?: string +} + +export namespace APISetActiveUser { + export type Response = CR.ActiveUser | CR.ChatCmdError + + export function cmdString(self: APISetActiveUser): string { + return '/_user ' + self.userId + (self.viewPwd ? ' ' + JSON.stringify(self.viewPwd) : '') + } +} + +// Delete user profile. +// Network usage: background. +export interface APIDeleteUser { + userId: number // int64 + delSMPQueues: boolean + viewPwd?: string +} + +export namespace APIDeleteUser { + export type Response = CR.CmdOk | CR.ChatCmdError + + export function cmdString(self: APIDeleteUser): string { + return '/_delete user ' + self.userId + ' del_smp=' + (self.delSMPQueues ? 'on' : 'off') + (self.viewPwd ? ' ' + JSON.stringify(self.viewPwd) : '') + } +} + +// Update user profile. +// Network usage: background. +export interface APIUpdateProfile { + userId: number // int64 + profile: T.Profile +} + +export namespace APIUpdateProfile { + export type Response = CR.UserProfileUpdated | CR.UserProfileNoChange | CR.ChatCmdError + + export function cmdString(self: APIUpdateProfile): string { + return '/_profile ' + self.userId + ' ' + JSON.stringify(self.profile) + } +} + +// Configure chat preference overrides for the contact. +// Network usage: background. +export interface APISetContactPrefs { + contactId: number // int64 + preferences: T.Preferences +} + +export namespace APISetContactPrefs { + export type Response = CR.ContactPrefsUpdated | CR.ChatCmdError + + export function cmdString(self: APISetContactPrefs): string { + return '/_set prefs @' + self.contactId + ' ' + JSON.stringify(self.preferences) + } +} diff --git a/packages/simplex-chat-client/types/typescript/src/events.ts b/packages/simplex-chat-client/types/typescript/src/events.ts new file mode 100644 index 0000000000..d7a0419bbe --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/src/events.ts @@ -0,0 +1,430 @@ +// API Events +// This file is generated automatically. + +import * as T from "./types" + +export type ChatEvent = + | CEvt.ContactConnected + | CEvt.ContactUpdated + | CEvt.ContactDeletedByContact + | CEvt.ReceivedContactRequest + | CEvt.NewMemberContactReceivedInv + | CEvt.ContactSndReady + | CEvt.NewChatItems + | CEvt.ChatItemReaction + | CEvt.ChatItemsDeleted + | CEvt.ChatItemUpdated + | CEvt.GroupChatItemsDeleted + | CEvt.ChatItemsStatusesUpdated + | CEvt.ReceivedGroupInvitation + | CEvt.UserJoinedGroup + | CEvt.GroupUpdated + | CEvt.JoinedGroupMember + | CEvt.MemberRole + | CEvt.DeletedMember + | CEvt.LeftMember + | CEvt.DeletedMemberUser + | CEvt.GroupDeleted + | CEvt.ConnectedToGroupMember + | CEvt.MemberAcceptedByOther + | CEvt.MemberBlockedForAll + | CEvt.GroupMemberUpdated + | CEvt.RcvFileDescrReady + | CEvt.RcvFileComplete + | CEvt.SndFileCompleteXFTP + | CEvt.RcvFileStart + | CEvt.RcvFileSndCancelled + | CEvt.RcvFileAccepted + | CEvt.RcvFileError + | CEvt.RcvFileWarning + | CEvt.SndFileError + | CEvt.SndFileWarning + | CEvt.AcceptingContactRequest + | CEvt.AcceptingBusinessRequest + | CEvt.ContactConnecting + | CEvt.BusinessLinkConnecting + | CEvt.JoinedGroupMemberConnecting + | CEvt.SentGroupInvitation + | CEvt.GroupLinkConnecting + | CEvt.MessageError + | CEvt.ChatError + | CEvt.ChatErrors + +export namespace CEvt { + export type Tag = + | "contactConnected" + | "contactUpdated" + | "contactDeletedByContact" + | "receivedContactRequest" + | "newMemberContactReceivedInv" + | "contactSndReady" + | "newChatItems" + | "chatItemReaction" + | "chatItemsDeleted" + | "chatItemUpdated" + | "groupChatItemsDeleted" + | "chatItemsStatusesUpdated" + | "receivedGroupInvitation" + | "userJoinedGroup" + | "groupUpdated" + | "joinedGroupMember" + | "memberRole" + | "deletedMember" + | "leftMember" + | "deletedMemberUser" + | "groupDeleted" + | "connectedToGroupMember" + | "memberAcceptedByOther" + | "memberBlockedForAll" + | "groupMemberUpdated" + | "rcvFileDescrReady" + | "rcvFileComplete" + | "sndFileCompleteXFTP" + | "rcvFileStart" + | "rcvFileSndCancelled" + | "rcvFileAccepted" + | "rcvFileError" + | "rcvFileWarning" + | "sndFileError" + | "sndFileWarning" + | "acceptingContactRequest" + | "acceptingBusinessRequest" + | "contactConnecting" + | "businessLinkConnecting" + | "joinedGroupMemberConnecting" + | "sentGroupInvitation" + | "groupLinkConnecting" + | "messageError" + | "chatError" + | "chatErrors" + + interface Interface { + type: Tag + } + + export interface ContactConnected extends Interface { + type: "contactConnected" + user: T.User + contact: T.Contact + userCustomProfile?: T.Profile + } + + export interface ContactUpdated extends Interface { + type: "contactUpdated" + user: T.User + fromContact: T.Contact + toContact: T.Contact + } + + export interface ContactDeletedByContact extends Interface { + type: "contactDeletedByContact" + user: T.User + contact: T.Contact + } + + export interface ReceivedContactRequest extends Interface { + type: "receivedContactRequest" + user: T.User + contactRequest: T.UserContactRequest + chat_?: T.AChat + } + + export interface NewMemberContactReceivedInv extends Interface { + type: "newMemberContactReceivedInv" + user: T.User + contact: T.Contact + groupInfo: T.GroupInfo + member: T.GroupMember + } + + export interface ContactSndReady extends Interface { + type: "contactSndReady" + user: T.User + contact: T.Contact + } + + export interface NewChatItems extends Interface { + type: "newChatItems" + user: T.User + chatItems: T.AChatItem[] + } + + export interface ChatItemReaction extends Interface { + type: "chatItemReaction" + user: T.User + added: boolean + reaction: T.ACIReaction + } + + export interface ChatItemsDeleted extends Interface { + type: "chatItemsDeleted" + user: T.User + chatItemDeletions: T.ChatItemDeletion[] + byUser: boolean + timed: boolean + } + + export interface ChatItemUpdated extends Interface { + type: "chatItemUpdated" + user: T.User + chatItem: T.AChatItem + } + + export interface GroupChatItemsDeleted extends Interface { + type: "groupChatItemsDeleted" + user: T.User + groupInfo: T.GroupInfo + chatItemIDs: number[] // int64 + byUser: boolean + member_?: T.GroupMember + } + + export interface ChatItemsStatusesUpdated extends Interface { + type: "chatItemsStatusesUpdated" + user: T.User + chatItems: T.AChatItem[] + } + + export interface ReceivedGroupInvitation extends Interface { + type: "receivedGroupInvitation" + user: T.User + groupInfo: T.GroupInfo + contact: T.Contact + fromMemberRole: T.GroupMemberRole + memberRole: T.GroupMemberRole + } + + export interface UserJoinedGroup extends Interface { + type: "userJoinedGroup" + user: T.User + groupInfo: T.GroupInfo + hostMember: T.GroupMember + } + + export interface GroupUpdated extends Interface { + type: "groupUpdated" + user: T.User + fromGroup: T.GroupInfo + toGroup: T.GroupInfo + member_?: T.GroupMember + } + + export interface JoinedGroupMember extends Interface { + type: "joinedGroupMember" + user: T.User + groupInfo: T.GroupInfo + member: T.GroupMember + } + + export interface MemberRole extends Interface { + type: "memberRole" + user: T.User + groupInfo: T.GroupInfo + byMember: T.GroupMember + member: T.GroupMember + fromRole: T.GroupMemberRole + toRole: T.GroupMemberRole + } + + export interface DeletedMember extends Interface { + type: "deletedMember" + user: T.User + groupInfo: T.GroupInfo + byMember: T.GroupMember + deletedMember: T.GroupMember + withMessages: boolean + } + + export interface LeftMember extends Interface { + type: "leftMember" + user: T.User + groupInfo: T.GroupInfo + member: T.GroupMember + } + + export interface DeletedMemberUser extends Interface { + type: "deletedMemberUser" + user: T.User + groupInfo: T.GroupInfo + member: T.GroupMember + withMessages: boolean + } + + export interface GroupDeleted extends Interface { + type: "groupDeleted" + user: T.User + groupInfo: T.GroupInfo + member: T.GroupMember + } + + export interface ConnectedToGroupMember extends Interface { + type: "connectedToGroupMember" + user: T.User + groupInfo: T.GroupInfo + member: T.GroupMember + memberContact?: T.Contact + } + + export interface MemberAcceptedByOther extends Interface { + type: "memberAcceptedByOther" + user: T.User + groupInfo: T.GroupInfo + acceptingMember: T.GroupMember + member: T.GroupMember + } + + export interface MemberBlockedForAll extends Interface { + type: "memberBlockedForAll" + user: T.User + groupInfo: T.GroupInfo + byMember: T.GroupMember + member: T.GroupMember + blocked: boolean + } + + export interface GroupMemberUpdated extends Interface { + type: "groupMemberUpdated" + user: T.User + groupInfo: T.GroupInfo + fromMember: T.GroupMember + toMember: T.GroupMember + } + + export interface RcvFileDescrReady extends Interface { + type: "rcvFileDescrReady" + user: T.User + chatItem: T.AChatItem + rcvFileTransfer: T.RcvFileTransfer + rcvFileDescr: T.RcvFileDescr + } + + export interface RcvFileComplete extends Interface { + type: "rcvFileComplete" + user: T.User + chatItem: T.AChatItem + } + + export interface SndFileCompleteXFTP extends Interface { + type: "sndFileCompleteXFTP" + user: T.User + chatItem: T.AChatItem + fileTransferMeta: T.FileTransferMeta + } + + export interface RcvFileStart extends Interface { + type: "rcvFileStart" + user: T.User + chatItem: T.AChatItem + } + + export interface RcvFileSndCancelled extends Interface { + type: "rcvFileSndCancelled" + user: T.User + chatItem: T.AChatItem + rcvFileTransfer: T.RcvFileTransfer + } + + export interface RcvFileAccepted extends Interface { + type: "rcvFileAccepted" + user: T.User + chatItem: T.AChatItem + } + + export interface RcvFileError extends Interface { + type: "rcvFileError" + user: T.User + chatItem_?: T.AChatItem + agentError: T.AgentErrorType + rcvFileTransfer: T.RcvFileTransfer + } + + export interface RcvFileWarning extends Interface { + type: "rcvFileWarning" + user: T.User + chatItem_?: T.AChatItem + agentError: T.AgentErrorType + rcvFileTransfer: T.RcvFileTransfer + } + + export interface SndFileError extends Interface { + type: "sndFileError" + user: T.User + chatItem_?: T.AChatItem + fileTransferMeta: T.FileTransferMeta + errorMessage: string + } + + export interface SndFileWarning extends Interface { + type: "sndFileWarning" + user: T.User + chatItem_?: T.AChatItem + fileTransferMeta: T.FileTransferMeta + errorMessage: string + } + + export interface AcceptingContactRequest extends Interface { + type: "acceptingContactRequest" + user: T.User + contact: T.Contact + } + + export interface AcceptingBusinessRequest extends Interface { + type: "acceptingBusinessRequest" + user: T.User + groupInfo: T.GroupInfo + } + + export interface ContactConnecting extends Interface { + type: "contactConnecting" + user: T.User + contact: T.Contact + } + + export interface BusinessLinkConnecting extends Interface { + type: "businessLinkConnecting" + user: T.User + groupInfo: T.GroupInfo + hostMember: T.GroupMember + fromContact: T.Contact + } + + export interface JoinedGroupMemberConnecting extends Interface { + type: "joinedGroupMemberConnecting" + user: T.User + groupInfo: T.GroupInfo + hostMember: T.GroupMember + member: T.GroupMember + } + + export interface SentGroupInvitation extends Interface { + type: "sentGroupInvitation" + user: T.User + groupInfo: T.GroupInfo + contact: T.Contact + member: T.GroupMember + } + + export interface GroupLinkConnecting extends Interface { + type: "groupLinkConnecting" + user: T.User + groupInfo: T.GroupInfo + hostMember: T.GroupMember + } + + export interface MessageError extends Interface { + type: "messageError" + user: T.User + severity: string + errorMessage: string + } + + export interface ChatError extends Interface { + type: "chatError" + chatError: T.ChatError + } + + export interface ChatErrors extends Interface { + type: "chatErrors" + chatErrors: T.ChatError[] + } +} diff --git a/packages/simplex-chat-client/types/typescript/src/index.ts b/packages/simplex-chat-client/types/typescript/src/index.ts new file mode 100644 index 0000000000..9a92af29a0 --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/src/index.ts @@ -0,0 +1,4 @@ +export * as CC from "./commands" +export {ChatResponse, CR} from "./responses" +export {ChatEvent, CEvt} from "./events" +export * as T from "./types" diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts new file mode 100644 index 0000000000..2ea67432f5 --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -0,0 +1,396 @@ +// API Responses +// This file is generated automatically. + +import * as T from "./types" + +export type ChatResponse = + | CR.AcceptingContactRequest + | CR.ActiveUser + | CR.ChatItemNotChanged + | CR.ChatItemReaction + | CR.ChatItemUpdated + | CR.ChatItemsDeleted + | CR.CmdOk + | CR.ChatCmdError + | CR.ConnectionPlan + | CR.ContactAlreadyExists + | CR.ContactConnectionDeleted + | CR.ContactDeleted + | CR.ContactPrefsUpdated + | CR.ContactRequestRejected + | CR.ContactsList + | CR.GroupDeletedUser + | CR.GroupLink + | CR.GroupLinkCreated + | CR.GroupLinkDeleted + | CR.GroupCreated + | CR.GroupMembers + | CR.GroupUpdated + | CR.GroupsList + | CR.Invitation + | CR.LeftMemberUser + | CR.MemberAccepted + | CR.MembersBlockedForAllUser + | CR.MembersRoleUser + | CR.NewChatItems + | CR.RcvFileAccepted + | CR.RcvFileAcceptedSndCancelled + | CR.RcvFileCancelled + | CR.SentConfirmation + | CR.SentGroupInvitation + | CR.SentInvitation + | CR.SndFileCancelled + | CR.UserAcceptedGroupSent + | CR.UserContactLink + | CR.UserContactLinkCreated + | CR.UserContactLinkDeleted + | CR.UserContactLinkUpdated + | CR.UserDeletedMembers + | CR.UserProfileUpdated + | CR.UserProfileNoChange + | CR.UsersList + +export namespace CR { + export type Tag = + | "acceptingContactRequest" + | "activeUser" + | "chatItemNotChanged" + | "chatItemReaction" + | "chatItemUpdated" + | "chatItemsDeleted" + | "cmdOk" + | "chatCmdError" + | "connectionPlan" + | "contactAlreadyExists" + | "contactConnectionDeleted" + | "contactDeleted" + | "contactPrefsUpdated" + | "contactRequestRejected" + | "contactsList" + | "groupDeletedUser" + | "groupLink" + | "groupLinkCreated" + | "groupLinkDeleted" + | "groupCreated" + | "groupMembers" + | "groupUpdated" + | "groupsList" + | "invitation" + | "leftMemberUser" + | "memberAccepted" + | "membersBlockedForAllUser" + | "membersRoleUser" + | "newChatItems" + | "rcvFileAccepted" + | "rcvFileAcceptedSndCancelled" + | "rcvFileCancelled" + | "sentConfirmation" + | "sentGroupInvitation" + | "sentInvitation" + | "sndFileCancelled" + | "userAcceptedGroupSent" + | "userContactLink" + | "userContactLinkCreated" + | "userContactLinkDeleted" + | "userContactLinkUpdated" + | "userDeletedMembers" + | "userProfileUpdated" + | "userProfileNoChange" + | "usersList" + + interface Interface { + type: Tag + } + + export interface AcceptingContactRequest extends Interface { + type: "acceptingContactRequest" + user: T.User + contact: T.Contact + } + + export interface ActiveUser extends Interface { + type: "activeUser" + user: T.User + } + + export interface ChatItemNotChanged extends Interface { + type: "chatItemNotChanged" + user: T.User + chatItem: T.AChatItem + } + + export interface ChatItemReaction extends Interface { + type: "chatItemReaction" + user: T.User + added: boolean + reaction: T.ACIReaction + } + + export interface ChatItemUpdated extends Interface { + type: "chatItemUpdated" + user: T.User + chatItem: T.AChatItem + } + + export interface ChatItemsDeleted extends Interface { + type: "chatItemsDeleted" + user: T.User + chatItemDeletions: T.ChatItemDeletion[] + byUser: boolean + timed: boolean + } + + export interface CmdOk extends Interface { + type: "cmdOk" + user_?: T.User + } + + export interface ChatCmdError extends Interface { + type: "chatCmdError" + chatError: T.ChatError + } + + export interface ConnectionPlan extends Interface { + type: "connectionPlan" + user: T.User + connLink: T.CreatedConnLink + connectionPlan: T.ConnectionPlan + } + + export interface ContactAlreadyExists extends Interface { + type: "contactAlreadyExists" + user: T.User + contact: T.Contact + } + + export interface ContactConnectionDeleted extends Interface { + type: "contactConnectionDeleted" + user: T.User + connection: T.PendingContactConnection + } + + export interface ContactDeleted extends Interface { + type: "contactDeleted" + user: T.User + contact: T.Contact + } + + export interface ContactPrefsUpdated extends Interface { + type: "contactPrefsUpdated" + user: T.User + fromContact: T.Contact + toContact: T.Contact + } + + export interface ContactRequestRejected extends Interface { + type: "contactRequestRejected" + user: T.User + contactRequest: T.UserContactRequest + contact_?: T.Contact + } + + export interface ContactsList extends Interface { + type: "contactsList" + user: T.User + contacts: T.Contact[] + } + + export interface GroupDeletedUser extends Interface { + type: "groupDeletedUser" + user: T.User + groupInfo: T.GroupInfo + } + + export interface GroupLink extends Interface { + type: "groupLink" + user: T.User + groupInfo: T.GroupInfo + groupLink: T.GroupLink + } + + export interface GroupLinkCreated extends Interface { + type: "groupLinkCreated" + user: T.User + groupInfo: T.GroupInfo + groupLink: T.GroupLink + } + + export interface GroupLinkDeleted extends Interface { + type: "groupLinkDeleted" + user: T.User + groupInfo: T.GroupInfo + } + + export interface GroupCreated extends Interface { + type: "groupCreated" + user: T.User + groupInfo: T.GroupInfo + } + + export interface GroupMembers extends Interface { + type: "groupMembers" + user: T.User + group: T.Group + } + + export interface GroupUpdated extends Interface { + type: "groupUpdated" + user: T.User + fromGroup: T.GroupInfo + toGroup: T.GroupInfo + member_?: T.GroupMember + } + + export interface GroupsList extends Interface { + type: "groupsList" + user: T.User + groups: T.GroupInfoSummary[] + } + + export interface Invitation extends Interface { + type: "invitation" + user: T.User + connLinkInvitation: T.CreatedConnLink + connection: T.PendingContactConnection + } + + export interface LeftMemberUser extends Interface { + type: "leftMemberUser" + user: T.User + groupInfo: T.GroupInfo + } + + export interface MemberAccepted extends Interface { + type: "memberAccepted" + user: T.User + groupInfo: T.GroupInfo + member: T.GroupMember + } + + export interface MembersBlockedForAllUser extends Interface { + type: "membersBlockedForAllUser" + user: T.User + groupInfo: T.GroupInfo + members: T.GroupMember[] + blocked: boolean + } + + export interface MembersRoleUser extends Interface { + type: "membersRoleUser" + user: T.User + groupInfo: T.GroupInfo + members: T.GroupMember[] + toRole: T.GroupMemberRole + } + + export interface NewChatItems extends Interface { + type: "newChatItems" + user: T.User + chatItems: T.AChatItem[] + } + + export interface RcvFileAccepted extends Interface { + type: "rcvFileAccepted" + user: T.User + chatItem: T.AChatItem + } + + export interface RcvFileAcceptedSndCancelled extends Interface { + type: "rcvFileAcceptedSndCancelled" + user: T.User + rcvFileTransfer: T.RcvFileTransfer + } + + export interface RcvFileCancelled extends Interface { + type: "rcvFileCancelled" + user: T.User + chatItem_?: T.AChatItem + rcvFileTransfer: T.RcvFileTransfer + } + + export interface SentConfirmation extends Interface { + type: "sentConfirmation" + user: T.User + connection: T.PendingContactConnection + customUserProfile?: T.Profile + } + + export interface SentGroupInvitation extends Interface { + type: "sentGroupInvitation" + user: T.User + groupInfo: T.GroupInfo + contact: T.Contact + member: T.GroupMember + } + + export interface SentInvitation extends Interface { + type: "sentInvitation" + user: T.User + connection: T.PendingContactConnection + customUserProfile?: T.Profile + } + + export interface SndFileCancelled extends Interface { + type: "sndFileCancelled" + user: T.User + chatItem_?: T.AChatItem + fileTransferMeta: T.FileTransferMeta + sndFileTransfers: T.SndFileTransfer[] + } + + export interface UserAcceptedGroupSent extends Interface { + type: "userAcceptedGroupSent" + user: T.User + groupInfo: T.GroupInfo + hostContact?: T.Contact + } + + export interface UserContactLink extends Interface { + type: "userContactLink" + user: T.User + contactLink: T.UserContactLink + } + + export interface UserContactLinkCreated extends Interface { + type: "userContactLinkCreated" + user: T.User + connLinkContact: T.CreatedConnLink + } + + export interface UserContactLinkDeleted extends Interface { + type: "userContactLinkDeleted" + user: T.User + } + + export interface UserContactLinkUpdated extends Interface { + type: "userContactLinkUpdated" + user: T.User + contactLink: T.UserContactLink + } + + export interface UserDeletedMembers extends Interface { + type: "userDeletedMembers" + user: T.User + groupInfo: T.GroupInfo + members: T.GroupMember[] + withMessages: boolean + } + + export interface UserProfileUpdated extends Interface { + type: "userProfileUpdated" + user: T.User + fromProfile: T.Profile + toProfile: T.Profile + updateSummary: T.UserProfileUpdateSummary + } + + export interface UserProfileNoChange extends Interface { + type: "userProfileNoChange" + user: T.User + } + + export interface UsersList extends Interface { + type: "usersList" + users: T.UserInfo[] + } +} diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts new file mode 100644 index 0000000000..1511c3851f --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -0,0 +1,4509 @@ +// API Types +// This file is generated automatically. + +export interface ACIReaction { + chatInfo: ChatInfo + chatReaction: CIReaction +} + +export interface AChat { + chatInfo: ChatInfo + chatItems: ChatItem[] + chatStats: ChatStats +} + +export interface AChatItem { + chatInfo: ChatInfo + chatItem: ChatItem +} + +export interface AddressSettings { + businessAddress: boolean + autoAccept?: AutoAccept + autoReply?: MsgContent +} + +export type AgentCryptoError = + | AgentCryptoError.DECRYPT_AES + | AgentCryptoError.DECRYPT_CB + | AgentCryptoError.RATCHET_HEADER + | AgentCryptoError.RATCHET_SYNC + +export namespace AgentCryptoError { + export type Tag = "DECRYPT_AES" | "DECRYPT_CB" | "RATCHET_HEADER" | "RATCHET_SYNC" + + interface Interface { + type: Tag + } + + export interface DECRYPT_AES extends Interface { + type: "DECRYPT_AES" + } + + export interface DECRYPT_CB extends Interface { + type: "DECRYPT_CB" + } + + export interface RATCHET_HEADER extends Interface { + type: "RATCHET_HEADER" + } + + export interface RATCHET_SYNC extends Interface { + type: "RATCHET_SYNC" + } +} + +export type AgentErrorType = + | AgentErrorType.CMD + | AgentErrorType.CONN + | AgentErrorType.NO_USER + | AgentErrorType.SMP + | AgentErrorType.NTF + | AgentErrorType.XFTP + | AgentErrorType.FILE + | AgentErrorType.PROXY + | AgentErrorType.RCP + | AgentErrorType.BROKER + | AgentErrorType.AGENT + | AgentErrorType.INTERNAL + | AgentErrorType.CRITICAL + | AgentErrorType.INACTIVE + +export namespace AgentErrorType { + export type Tag = + | "CMD" + | "CONN" + | "NO_USER" + | "SMP" + | "NTF" + | "XFTP" + | "FILE" + | "PROXY" + | "RCP" + | "BROKER" + | "AGENT" + | "INTERNAL" + | "CRITICAL" + | "INACTIVE" + + interface Interface { + type: Tag + } + + export interface CMD extends Interface { + type: "CMD" + cmdErr: CommandErrorType + errContext: string + } + + export interface CONN extends Interface { + type: "CONN" + connErr: ConnectionErrorType + errContext: string + } + + export interface NO_USER extends Interface { + type: "NO_USER" + } + + export interface SMP extends Interface { + type: "SMP" + serverAddress: string + smpErr: ErrorType + } + + export interface NTF extends Interface { + type: "NTF" + serverAddress: string + ntfErr: ErrorType + } + + export interface XFTP extends Interface { + type: "XFTP" + serverAddress: string + xftpErr: XFTPErrorType + } + + export interface FILE extends Interface { + type: "FILE" + fileErr: FileErrorType + } + + export interface PROXY extends Interface { + type: "PROXY" + proxyServer: string + relayServer: string + proxyErr: ProxyClientError + } + + export interface RCP extends Interface { + type: "RCP" + rcpErr: RCErrorType + } + + export interface BROKER extends Interface { + type: "BROKER" + brokerAddress: string + brokerErr: BrokerErrorType + } + + export interface AGENT extends Interface { + type: "AGENT" + agentErr: SMPAgentError + } + + export interface INTERNAL extends Interface { + type: "INTERNAL" + internalErr: string + } + + export interface CRITICAL extends Interface { + type: "CRITICAL" + offerRestart: boolean + criticalErr: string + } + + export interface INACTIVE extends Interface { + type: "INACTIVE" + } +} + +export interface AutoAccept { + acceptIncognito: boolean +} + +export interface BlockingInfo { + reason: BlockingReason +} + +export enum BlockingReason { + Spam = "spam", + Content = "content", +} + +export type BrokerErrorType = + | BrokerErrorType.RESPONSE + | BrokerErrorType.UNEXPECTED + | BrokerErrorType.NETWORK + | BrokerErrorType.HOST + | BrokerErrorType.NO_SERVICE + | BrokerErrorType.TRANSPORT + | BrokerErrorType.TIMEOUT + +export namespace BrokerErrorType { + export type Tag = + | "RESPONSE" + | "UNEXPECTED" + | "NETWORK" + | "HOST" + | "NO_SERVICE" + | "TRANSPORT" + | "TIMEOUT" + + interface Interface { + type: Tag + } + + export interface RESPONSE extends Interface { + type: "RESPONSE" + respErr: string + } + + export interface UNEXPECTED extends Interface { + type: "UNEXPECTED" + respErr: string + } + + export interface NETWORK extends Interface { + type: "NETWORK" + } + + export interface HOST extends Interface { + type: "HOST" + } + + export interface NO_SERVICE extends Interface { + type: "NO_SERVICE" + } + + export interface TRANSPORT extends Interface { + type: "TRANSPORT" + transportErr: TransportError + } + + export interface TIMEOUT extends Interface { + type: "TIMEOUT" + } +} + +export interface BusinessChatInfo { + chatType: BusinessChatType + businessId: string + customerId: string +} + +export enum BusinessChatType { + Business = "business", + Customer = "customer", +} + +export enum CICallStatus { + Pending = "pending", + Missed = "missed", + Rejected = "rejected", + Accepted = "accepted", + Negotiated = "negotiated", + Progress = "progress", + Ended = "ended", + Error = "error", +} + +export type CIContent = + | CIContent.SndMsgContent + | CIContent.RcvMsgContent + | CIContent.SndDeleted + | CIContent.RcvDeleted + | CIContent.SndCall + | CIContent.RcvCall + | CIContent.RcvIntegrityError + | CIContent.RcvDecryptionError + | CIContent.RcvGroupInvitation + | CIContent.SndGroupInvitation + | CIContent.RcvDirectEvent + | CIContent.RcvGroupEvent + | CIContent.SndGroupEvent + | CIContent.RcvConnEvent + | CIContent.SndConnEvent + | CIContent.RcvChatFeature + | CIContent.SndChatFeature + | CIContent.RcvChatPreference + | CIContent.SndChatPreference + | CIContent.RcvGroupFeature + | CIContent.SndGroupFeature + | CIContent.RcvChatFeatureRejected + | CIContent.RcvGroupFeatureRejected + | CIContent.SndModerated + | CIContent.RcvModerated + | CIContent.RcvBlocked + | CIContent.SndDirectE2EEInfo + | CIContent.RcvDirectE2EEInfo + | CIContent.SndGroupE2EEInfo + | CIContent.RcvGroupE2EEInfo + | CIContent.ChatBanner + +export namespace CIContent { + export type Tag = + | "sndMsgContent" + | "rcvMsgContent" + | "sndDeleted" + | "rcvDeleted" + | "sndCall" + | "rcvCall" + | "rcvIntegrityError" + | "rcvDecryptionError" + | "rcvGroupInvitation" + | "sndGroupInvitation" + | "rcvDirectEvent" + | "rcvGroupEvent" + | "sndGroupEvent" + | "rcvConnEvent" + | "sndConnEvent" + | "rcvChatFeature" + | "sndChatFeature" + | "rcvChatPreference" + | "sndChatPreference" + | "rcvGroupFeature" + | "sndGroupFeature" + | "rcvChatFeatureRejected" + | "rcvGroupFeatureRejected" + | "sndModerated" + | "rcvModerated" + | "rcvBlocked" + | "sndDirectE2EEInfo" + | "rcvDirectE2EEInfo" + | "sndGroupE2EEInfo" + | "rcvGroupE2EEInfo" + | "chatBanner" + + interface Interface { + type: Tag + } + + export interface SndMsgContent extends Interface { + type: "sndMsgContent" + msgContent: MsgContent + } + + export interface RcvMsgContent extends Interface { + type: "rcvMsgContent" + msgContent: MsgContent + } + + export interface SndDeleted extends Interface { + type: "sndDeleted" + deleteMode: CIDeleteMode + } + + export interface RcvDeleted extends Interface { + type: "rcvDeleted" + deleteMode: CIDeleteMode + } + + export interface SndCall extends Interface { + type: "sndCall" + status: CICallStatus + duration: number // int + } + + export interface RcvCall extends Interface { + type: "rcvCall" + status: CICallStatus + duration: number // int + } + + export interface RcvIntegrityError extends Interface { + type: "rcvIntegrityError" + msgError: MsgErrorType + } + + export interface RcvDecryptionError extends Interface { + type: "rcvDecryptionError" + msgDecryptError: MsgDecryptError + msgCount: number // word32 + } + + export interface RcvGroupInvitation extends Interface { + type: "rcvGroupInvitation" + groupInvitation: CIGroupInvitation + memberRole: GroupMemberRole + } + + export interface SndGroupInvitation extends Interface { + type: "sndGroupInvitation" + groupInvitation: CIGroupInvitation + memberRole: GroupMemberRole + } + + export interface RcvDirectEvent extends Interface { + type: "rcvDirectEvent" + rcvDirectEvent: RcvDirectEvent + } + + export interface RcvGroupEvent extends Interface { + type: "rcvGroupEvent" + rcvGroupEvent: RcvGroupEvent + } + + export interface SndGroupEvent extends Interface { + type: "sndGroupEvent" + sndGroupEvent: SndGroupEvent + } + + export interface RcvConnEvent extends Interface { + type: "rcvConnEvent" + rcvConnEvent: RcvConnEvent + } + + export interface SndConnEvent extends Interface { + type: "sndConnEvent" + sndConnEvent: SndConnEvent + } + + export interface RcvChatFeature extends Interface { + type: "rcvChatFeature" + feature: ChatFeature + enabled: PrefEnabled + param?: number // int + } + + export interface SndChatFeature extends Interface { + type: "sndChatFeature" + feature: ChatFeature + enabled: PrefEnabled + param?: number // int + } + + export interface RcvChatPreference extends Interface { + type: "rcvChatPreference" + feature: ChatFeature + allowed: FeatureAllowed + param?: number // int + } + + export interface SndChatPreference extends Interface { + type: "sndChatPreference" + feature: ChatFeature + allowed: FeatureAllowed + param?: number // int + } + + export interface RcvGroupFeature extends Interface { + type: "rcvGroupFeature" + groupFeature: GroupFeature + preference: GroupPreference + param?: number // int + memberRole_?: GroupMemberRole + } + + export interface SndGroupFeature extends Interface { + type: "sndGroupFeature" + groupFeature: GroupFeature + preference: GroupPreference + param?: number // int + memberRole_?: GroupMemberRole + } + + export interface RcvChatFeatureRejected extends Interface { + type: "rcvChatFeatureRejected" + feature: ChatFeature + } + + export interface RcvGroupFeatureRejected extends Interface { + type: "rcvGroupFeatureRejected" + groupFeature: GroupFeature + } + + export interface SndModerated extends Interface { + type: "sndModerated" + } + + export interface RcvModerated extends Interface { + type: "rcvModerated" + } + + export interface RcvBlocked extends Interface { + type: "rcvBlocked" + } + + export interface SndDirectE2EEInfo extends Interface { + type: "sndDirectE2EEInfo" + e2eeInfo: E2EInfo + } + + export interface RcvDirectE2EEInfo extends Interface { + type: "rcvDirectE2EEInfo" + e2eeInfo: E2EInfo + } + + export interface SndGroupE2EEInfo extends Interface { + type: "sndGroupE2EEInfo" + e2eeInfo: E2EInfo + } + + export interface RcvGroupE2EEInfo extends Interface { + type: "rcvGroupE2EEInfo" + e2eeInfo: E2EInfo + } + + export interface ChatBanner extends Interface { + type: "chatBanner" + } +} + +export enum CIDeleteMode { + Broadcast = "broadcast", + Internal = "internal", + InternalMark = "internalMark", +} + +export type CIDeleted = CIDeleted.Deleted | CIDeleted.Blocked | CIDeleted.BlockedByAdmin | CIDeleted.Moderated + +export namespace CIDeleted { + export type Tag = "deleted" | "blocked" | "blockedByAdmin" | "moderated" + + interface Interface { + type: Tag + } + + export interface Deleted extends Interface { + type: "deleted" + deletedTs?: string // ISO-8601 timestamp + chatType: ChatType + } + + export interface Blocked extends Interface { + type: "blocked" + deletedTs?: string // ISO-8601 timestamp + } + + export interface BlockedByAdmin extends Interface { + type: "blockedByAdmin" + deletedTs?: string // ISO-8601 timestamp + } + + export interface Moderated extends Interface { + type: "moderated" + deletedTs?: string // ISO-8601 timestamp + byGroupMember: GroupMember + } +} + +export type CIDirection = + | CIDirection.DirectSnd + | CIDirection.DirectRcv + | CIDirection.GroupSnd + | CIDirection.GroupRcv + | CIDirection.LocalSnd + | CIDirection.LocalRcv + +export namespace CIDirection { + export type Tag = "directSnd" | "directRcv" | "groupSnd" | "groupRcv" | "localSnd" | "localRcv" + + interface Interface { + type: Tag + } + + export interface DirectSnd extends Interface { + type: "directSnd" + } + + export interface DirectRcv extends Interface { + type: "directRcv" + } + + export interface GroupSnd extends Interface { + type: "groupSnd" + } + + export interface GroupRcv extends Interface { + type: "groupRcv" + groupMember: GroupMember + } + + export interface LocalSnd extends Interface { + type: "localSnd" + } + + export interface LocalRcv extends Interface { + type: "localRcv" + } +} + +export interface CIFile { + fileId: number // int64 + fileName: string + fileSize: number // int64 + fileSource?: CryptoFile + fileStatus: CIFileStatus + fileProtocol: FileProtocol +} + +export type CIFileStatus = + | CIFileStatus.SndStored + | CIFileStatus.SndTransfer + | CIFileStatus.SndCancelled + | CIFileStatus.SndComplete + | CIFileStatus.SndError + | CIFileStatus.SndWarning + | CIFileStatus.RcvInvitation + | CIFileStatus.RcvAccepted + | CIFileStatus.RcvTransfer + | CIFileStatus.RcvAborted + | CIFileStatus.RcvComplete + | CIFileStatus.RcvCancelled + | CIFileStatus.RcvError + | CIFileStatus.RcvWarning + | CIFileStatus.Invalid + +export namespace CIFileStatus { + export type Tag = + | "sndStored" + | "sndTransfer" + | "sndCancelled" + | "sndComplete" + | "sndError" + | "sndWarning" + | "rcvInvitation" + | "rcvAccepted" + | "rcvTransfer" + | "rcvAborted" + | "rcvComplete" + | "rcvCancelled" + | "rcvError" + | "rcvWarning" + | "invalid" + + interface Interface { + type: Tag + } + + export interface SndStored extends Interface { + type: "sndStored" + } + + export interface SndTransfer extends Interface { + type: "sndTransfer" + sndProgress: number // int64 + sndTotal: number // int64 + } + + export interface SndCancelled extends Interface { + type: "sndCancelled" + } + + export interface SndComplete extends Interface { + type: "sndComplete" + } + + export interface SndError extends Interface { + type: "sndError" + sndFileError: FileError + } + + export interface SndWarning extends Interface { + type: "sndWarning" + sndFileError: FileError + } + + export interface RcvInvitation extends Interface { + type: "rcvInvitation" + } + + export interface RcvAccepted extends Interface { + type: "rcvAccepted" + } + + export interface RcvTransfer extends Interface { + type: "rcvTransfer" + rcvProgress: number // int64 + rcvTotal: number // int64 + } + + export interface RcvAborted extends Interface { + type: "rcvAborted" + } + + export interface RcvComplete extends Interface { + type: "rcvComplete" + } + + export interface RcvCancelled extends Interface { + type: "rcvCancelled" + } + + export interface RcvError extends Interface { + type: "rcvError" + rcvFileError: FileError + } + + export interface RcvWarning extends Interface { + type: "rcvWarning" + rcvFileError: FileError + } + + export interface Invalid extends Interface { + type: "invalid" + text: string + } +} + +export type CIForwardedFrom = CIForwardedFrom.Unknown | CIForwardedFrom.Contact | CIForwardedFrom.Group + +export namespace CIForwardedFrom { + export type Tag = "unknown" | "contact" | "group" + + interface Interface { + type: Tag + } + + export interface Unknown extends Interface { + type: "unknown" + } + + export interface Contact extends Interface { + type: "contact" + chatName: string + msgDir: MsgDirection + contactId?: number // int64 + chatItemId?: number // int64 + } + + export interface Group extends Interface { + type: "group" + chatName: string + msgDir: MsgDirection + groupId?: number // int64 + chatItemId?: number // int64 + } +} + +export interface CIGroupInvitation { + groupId: number // int64 + groupMemberId: number // int64 + localDisplayName: string + groupProfile: GroupProfile + status: CIGroupInvitationStatus +} + +export enum CIGroupInvitationStatus { + Pending = "pending", + Accepted = "accepted", + Rejected = "rejected", + Expired = "expired", +} + +export interface CIMention { + memberId: string + memberRef?: CIMentionMember +} + +export interface CIMentionMember { + groupMemberId: number // int64 + displayName: string + localAlias?: string + memberRole: GroupMemberRole +} + +export interface CIMeta { + itemId: number // int64 + itemTs: string // ISO-8601 timestamp + itemText: string + itemStatus: CIStatus + sentViaProxy?: boolean + itemSharedMsgId?: string + itemForwarded?: CIForwardedFrom + itemDeleted?: CIDeleted + itemEdited: boolean + itemTimed?: CITimed + itemLive?: boolean + userMention: boolean + deletable: boolean + editable: boolean + forwardedByMember?: number // int64 + showGroupAsSender: boolean + createdAt: string // ISO-8601 timestamp + updatedAt: string // ISO-8601 timestamp +} + +export interface CIQuote { + chatDir?: CIDirection + itemId?: number // int64 + sharedMsgId?: string + sentAt: string // ISO-8601 timestamp + content: MsgContent + formattedText?: FormattedText[] +} + +export interface CIReaction { + chatDir: CIDirection + chatItem: ChatItem + sentAt: string // ISO-8601 timestamp + reaction: MsgReaction +} + +export interface CIReactionCount { + reaction: MsgReaction + userReacted: boolean + totalReacted: number // int +} + +export type CIStatus = + | CIStatus.SndNew + | CIStatus.SndSent + | CIStatus.SndRcvd + | CIStatus.SndErrorAuth + | CIStatus.SndError + | CIStatus.SndWarning + | CIStatus.RcvNew + | CIStatus.RcvRead + | CIStatus.Invalid + +export namespace CIStatus { + export type Tag = + | "sndNew" + | "sndSent" + | "sndRcvd" + | "sndErrorAuth" + | "sndError" + | "sndWarning" + | "rcvNew" + | "rcvRead" + | "invalid" + + interface Interface { + type: Tag + } + + export interface SndNew extends Interface { + type: "sndNew" + } + + export interface SndSent extends Interface { + type: "sndSent" + sndProgress: SndCIStatusProgress + } + + export interface SndRcvd extends Interface { + type: "sndRcvd" + msgRcptStatus: MsgReceiptStatus + sndProgress: SndCIStatusProgress + } + + export interface SndErrorAuth extends Interface { + type: "sndErrorAuth" + } + + export interface SndError extends Interface { + type: "sndError" + agentError: SndError + } + + export interface SndWarning extends Interface { + type: "sndWarning" + agentError: SndError + } + + export interface RcvNew extends Interface { + type: "rcvNew" + } + + export interface RcvRead extends Interface { + type: "rcvRead" + } + + export interface Invalid extends Interface { + type: "invalid" + text: string + } +} + +export interface CITimed { + ttl: number // int + deleteAt?: string // ISO-8601 timestamp +} + +export type ChatBotCommand = ChatBotCommand.Command | ChatBotCommand.Menu + +export namespace ChatBotCommand { + export type Tag = "command" | "menu" + + interface Interface { + type: Tag + } + + export interface Command extends Interface { + type: "command" + keyword: string + label: string + params?: string + } + + export interface Menu extends Interface { + type: "menu" + label: string + commands: ChatBotCommand[] + } +} + +export type ChatDeleteMode = ChatDeleteMode.Full | ChatDeleteMode.Entity | ChatDeleteMode.Messages + +export namespace ChatDeleteMode { + export type Tag = "full" | "entity" | "messages" + + interface Interface { + type: Tag + } + + export interface Full extends Interface { + type: "full" + notify: boolean + } + + export interface Entity extends Interface { + type: "entity" + notify: boolean + } + + export interface Messages extends Interface { + type: "messages" + } + + export function cmdString(self: ChatDeleteMode): string { + return self.type + (self.type == 'messages' ? '' : (!self.notify ? ' notify=off' : '')) + } +} + +export type ChatError = ChatError.Error | ChatError.ErrorAgent | ChatError.ErrorStore + +export namespace ChatError { + export type Tag = "error" | "errorAgent" | "errorStore" + + interface Interface { + type: Tag + } + + export interface Error extends Interface { + type: "error" + errorType: ChatErrorType + } + + export interface ErrorAgent extends Interface { + type: "errorAgent" + agentError: AgentErrorType + connectionEntity_?: ConnectionEntity + } + + export interface ErrorStore extends Interface { + type: "errorStore" + storeError: StoreError + } +} + +export type ChatErrorType = + | ChatErrorType.NoActiveUser + | ChatErrorType.NoConnectionUser + | ChatErrorType.NoSndFileUser + | ChatErrorType.NoRcvFileUser + | ChatErrorType.UserUnknown + | ChatErrorType.ActiveUserExists + | ChatErrorType.UserExists + | ChatErrorType.DifferentActiveUser + | ChatErrorType.CantDeleteActiveUser + | ChatErrorType.CantDeleteLastUser + | ChatErrorType.CantHideLastUser + | ChatErrorType.HiddenUserAlwaysMuted + | ChatErrorType.EmptyUserPassword + | ChatErrorType.UserAlreadyHidden + | ChatErrorType.UserNotHidden + | ChatErrorType.InvalidDisplayName + | ChatErrorType.ChatNotStarted + | ChatErrorType.ChatNotStopped + | ChatErrorType.ChatStoreChanged + | ChatErrorType.InvalidConnReq + | ChatErrorType.UnsupportedConnReq + | ChatErrorType.ConnReqMessageProhibited + | ChatErrorType.ContactNotReady + | ChatErrorType.ContactNotActive + | ChatErrorType.ContactDisabled + | ChatErrorType.ConnectionDisabled + | ChatErrorType.GroupUserRole + | ChatErrorType.GroupMemberInitialRole + | ChatErrorType.ContactIncognitoCantInvite + | ChatErrorType.GroupIncognitoCantInvite + | ChatErrorType.GroupContactRole + | ChatErrorType.GroupDuplicateMember + | ChatErrorType.GroupDuplicateMemberId + | ChatErrorType.GroupNotJoined + | ChatErrorType.GroupMemberNotActive + | ChatErrorType.CantBlockMemberForSelf + | ChatErrorType.GroupMemberUserRemoved + | ChatErrorType.GroupMemberNotFound + | ChatErrorType.GroupCantResendInvitation + | ChatErrorType.GroupInternal + | ChatErrorType.FileNotFound + | ChatErrorType.FileSize + | ChatErrorType.FileAlreadyReceiving + | ChatErrorType.FileCancelled + | ChatErrorType.FileCancel + | ChatErrorType.FileAlreadyExists + | ChatErrorType.FileRead + | ChatErrorType.FileWrite + | ChatErrorType.FileSend + | ChatErrorType.FileRcvChunk + | ChatErrorType.FileInternal + | ChatErrorType.FileImageType + | ChatErrorType.FileImageSize + | ChatErrorType.FileNotReceived + | ChatErrorType.FileNotApproved + | ChatErrorType.FallbackToSMPProhibited + | ChatErrorType.InlineFileProhibited + | ChatErrorType.InvalidForward + | ChatErrorType.InvalidChatItemUpdate + | ChatErrorType.InvalidChatItemDelete + | ChatErrorType.HasCurrentCall + | ChatErrorType.NoCurrentCall + | ChatErrorType.CallContact + | ChatErrorType.DirectMessagesProhibited + | ChatErrorType.AgentVersion + | ChatErrorType.AgentNoSubResult + | ChatErrorType.CommandError + | ChatErrorType.AgentCommandError + | ChatErrorType.InvalidFileDescription + | ChatErrorType.ConnectionIncognitoChangeProhibited + | ChatErrorType.ConnectionUserChangeProhibited + | ChatErrorType.PeerChatVRangeIncompatible + | ChatErrorType.InternalError + | ChatErrorType.Exception + +export namespace ChatErrorType { + export type Tag = + | "noActiveUser" + | "noConnectionUser" + | "noSndFileUser" + | "noRcvFileUser" + | "userUnknown" + | "activeUserExists" + | "userExists" + | "differentActiveUser" + | "cantDeleteActiveUser" + | "cantDeleteLastUser" + | "cantHideLastUser" + | "hiddenUserAlwaysMuted" + | "emptyUserPassword" + | "userAlreadyHidden" + | "userNotHidden" + | "invalidDisplayName" + | "chatNotStarted" + | "chatNotStopped" + | "chatStoreChanged" + | "invalidConnReq" + | "unsupportedConnReq" + | "connReqMessageProhibited" + | "contactNotReady" + | "contactNotActive" + | "contactDisabled" + | "connectionDisabled" + | "groupUserRole" + | "groupMemberInitialRole" + | "contactIncognitoCantInvite" + | "groupIncognitoCantInvite" + | "groupContactRole" + | "groupDuplicateMember" + | "groupDuplicateMemberId" + | "groupNotJoined" + | "groupMemberNotActive" + | "cantBlockMemberForSelf" + | "groupMemberUserRemoved" + | "groupMemberNotFound" + | "groupCantResendInvitation" + | "groupInternal" + | "fileNotFound" + | "fileSize" + | "fileAlreadyReceiving" + | "fileCancelled" + | "fileCancel" + | "fileAlreadyExists" + | "fileRead" + | "fileWrite" + | "fileSend" + | "fileRcvChunk" + | "fileInternal" + | "fileImageType" + | "fileImageSize" + | "fileNotReceived" + | "fileNotApproved" + | "fallbackToSMPProhibited" + | "inlineFileProhibited" + | "invalidForward" + | "invalidChatItemUpdate" + | "invalidChatItemDelete" + | "hasCurrentCall" + | "noCurrentCall" + | "callContact" + | "directMessagesProhibited" + | "agentVersion" + | "agentNoSubResult" + | "commandError" + | "agentCommandError" + | "invalidFileDescription" + | "connectionIncognitoChangeProhibited" + | "connectionUserChangeProhibited" + | "peerChatVRangeIncompatible" + | "internalError" + | "exception" + + interface Interface { + type: Tag + } + + export interface NoActiveUser extends Interface { + type: "noActiveUser" + } + + export interface NoConnectionUser extends Interface { + type: "noConnectionUser" + agentConnId: string + } + + export interface NoSndFileUser extends Interface { + type: "noSndFileUser" + agentSndFileId: string + } + + export interface NoRcvFileUser extends Interface { + type: "noRcvFileUser" + agentRcvFileId: string + } + + export interface UserUnknown extends Interface { + type: "userUnknown" + } + + export interface ActiveUserExists extends Interface { + type: "activeUserExists" + } + + export interface UserExists extends Interface { + type: "userExists" + contactName: string + } + + export interface DifferentActiveUser extends Interface { + type: "differentActiveUser" + commandUserId: number // int64 + activeUserId: number // int64 + } + + export interface CantDeleteActiveUser extends Interface { + type: "cantDeleteActiveUser" + userId: number // int64 + } + + export interface CantDeleteLastUser extends Interface { + type: "cantDeleteLastUser" + userId: number // int64 + } + + export interface CantHideLastUser extends Interface { + type: "cantHideLastUser" + userId: number // int64 + } + + export interface HiddenUserAlwaysMuted extends Interface { + type: "hiddenUserAlwaysMuted" + userId: number // int64 + } + + export interface EmptyUserPassword extends Interface { + type: "emptyUserPassword" + userId: number // int64 + } + + export interface UserAlreadyHidden extends Interface { + type: "userAlreadyHidden" + userId: number // int64 + } + + export interface UserNotHidden extends Interface { + type: "userNotHidden" + userId: number // int64 + } + + export interface InvalidDisplayName extends Interface { + type: "invalidDisplayName" + displayName: string + validName: string + } + + export interface ChatNotStarted extends Interface { + type: "chatNotStarted" + } + + export interface ChatNotStopped extends Interface { + type: "chatNotStopped" + } + + export interface ChatStoreChanged extends Interface { + type: "chatStoreChanged" + } + + export interface InvalidConnReq extends Interface { + type: "invalidConnReq" + } + + export interface UnsupportedConnReq extends Interface { + type: "unsupportedConnReq" + } + + export interface ConnReqMessageProhibited extends Interface { + type: "connReqMessageProhibited" + } + + export interface ContactNotReady extends Interface { + type: "contactNotReady" + contact: Contact + } + + export interface ContactNotActive extends Interface { + type: "contactNotActive" + contact: Contact + } + + export interface ContactDisabled extends Interface { + type: "contactDisabled" + contact: Contact + } + + export interface ConnectionDisabled extends Interface { + type: "connectionDisabled" + connection: Connection + } + + export interface GroupUserRole extends Interface { + type: "groupUserRole" + groupInfo: GroupInfo + requiredRole: GroupMemberRole + } + + export interface GroupMemberInitialRole extends Interface { + type: "groupMemberInitialRole" + groupInfo: GroupInfo + initialRole: GroupMemberRole + } + + export interface ContactIncognitoCantInvite extends Interface { + type: "contactIncognitoCantInvite" + } + + export interface GroupIncognitoCantInvite extends Interface { + type: "groupIncognitoCantInvite" + } + + export interface GroupContactRole extends Interface { + type: "groupContactRole" + contactName: string + } + + export interface GroupDuplicateMember extends Interface { + type: "groupDuplicateMember" + contactName: string + } + + export interface GroupDuplicateMemberId extends Interface { + type: "groupDuplicateMemberId" + } + + export interface GroupNotJoined extends Interface { + type: "groupNotJoined" + groupInfo: GroupInfo + } + + export interface GroupMemberNotActive extends Interface { + type: "groupMemberNotActive" + } + + export interface CantBlockMemberForSelf extends Interface { + type: "cantBlockMemberForSelf" + groupInfo: GroupInfo + member: GroupMember + setShowMessages: boolean + } + + export interface GroupMemberUserRemoved extends Interface { + type: "groupMemberUserRemoved" + } + + export interface GroupMemberNotFound extends Interface { + type: "groupMemberNotFound" + } + + export interface GroupCantResendInvitation extends Interface { + type: "groupCantResendInvitation" + groupInfo: GroupInfo + contactName: string + } + + export interface GroupInternal extends Interface { + type: "groupInternal" + message: string + } + + export interface FileNotFound extends Interface { + type: "fileNotFound" + message: string + } + + export interface FileSize extends Interface { + type: "fileSize" + filePath: string + } + + export interface FileAlreadyReceiving extends Interface { + type: "fileAlreadyReceiving" + message: string + } + + export interface FileCancelled extends Interface { + type: "fileCancelled" + message: string + } + + export interface FileCancel extends Interface { + type: "fileCancel" + fileId: number // int64 + message: string + } + + export interface FileAlreadyExists extends Interface { + type: "fileAlreadyExists" + filePath: string + } + + export interface FileRead extends Interface { + type: "fileRead" + filePath: string + message: string + } + + export interface FileWrite extends Interface { + type: "fileWrite" + filePath: string + message: string + } + + export interface FileSend extends Interface { + type: "fileSend" + fileId: number // int64 + agentError: AgentErrorType + } + + export interface FileRcvChunk extends Interface { + type: "fileRcvChunk" + message: string + } + + export interface FileInternal extends Interface { + type: "fileInternal" + message: string + } + + export interface FileImageType extends Interface { + type: "fileImageType" + filePath: string + } + + export interface FileImageSize extends Interface { + type: "fileImageSize" + filePath: string + } + + export interface FileNotReceived extends Interface { + type: "fileNotReceived" + fileId: number // int64 + } + + export interface FileNotApproved extends Interface { + type: "fileNotApproved" + fileId: number // int64 + unknownServers: string[] + } + + export interface FallbackToSMPProhibited extends Interface { + type: "fallbackToSMPProhibited" + fileId: number // int64 + } + + export interface InlineFileProhibited extends Interface { + type: "inlineFileProhibited" + fileId: number // int64 + } + + export interface InvalidForward extends Interface { + type: "invalidForward" + } + + export interface InvalidChatItemUpdate extends Interface { + type: "invalidChatItemUpdate" + } + + export interface InvalidChatItemDelete extends Interface { + type: "invalidChatItemDelete" + } + + export interface HasCurrentCall extends Interface { + type: "hasCurrentCall" + } + + export interface NoCurrentCall extends Interface { + type: "noCurrentCall" + } + + export interface CallContact extends Interface { + type: "callContact" + contactId: number // int64 + } + + export interface DirectMessagesProhibited extends Interface { + type: "directMessagesProhibited" + direction: MsgDirection + contact: Contact + } + + export interface AgentVersion extends Interface { + type: "agentVersion" + } + + export interface AgentNoSubResult extends Interface { + type: "agentNoSubResult" + agentConnId: string + } + + export interface CommandError extends Interface { + type: "commandError" + message: string + } + + export interface AgentCommandError extends Interface { + type: "agentCommandError" + message: string + } + + export interface InvalidFileDescription extends Interface { + type: "invalidFileDescription" + message: string + } + + export interface ConnectionIncognitoChangeProhibited extends Interface { + type: "connectionIncognitoChangeProhibited" + } + + export interface ConnectionUserChangeProhibited extends Interface { + type: "connectionUserChangeProhibited" + } + + export interface PeerChatVRangeIncompatible extends Interface { + type: "peerChatVRangeIncompatible" + } + + export interface InternalError extends Interface { + type: "internalError" + message: string + } + + export interface Exception extends Interface { + type: "exception" + message: string + } +} + +export enum ChatFeature { + TimedMessages = "timedMessages", + FullDelete = "fullDelete", + Reactions = "reactions", + Voice = "voice", + Files = "files", + Calls = "calls", + Sessions = "sessions", +} + +export type ChatInfo = + | ChatInfo.Direct + | ChatInfo.Group + | ChatInfo.Local + | ChatInfo.ContactRequest + | ChatInfo.ContactConnection + +export namespace ChatInfo { + export type Tag = "direct" | "group" | "local" | "contactRequest" | "contactConnection" + + interface Interface { + type: Tag + } + + export interface Direct extends Interface { + type: "direct" + contact: Contact + } + + export interface Group extends Interface { + type: "group" + groupInfo: GroupInfo + groupChatScope?: GroupChatScopeInfo + } + + export interface Local extends Interface { + type: "local" + noteFolder: NoteFolder + } + + export interface ContactRequest extends Interface { + type: "contactRequest" + contactRequest: UserContactRequest + } + + export interface ContactConnection extends Interface { + type: "contactConnection" + contactConnection: PendingContactConnection + } +} + +export interface ChatItem { + chatDir: CIDirection + meta: CIMeta + content: CIContent + mentions: {[key: string]: CIMention} + formattedText?: FormattedText[] + quotedItem?: CIQuote + reactions: CIReactionCount[] + file?: CIFile +} +// Message deletion result. + +export interface ChatItemDeletion { + deletedChatItem: AChatItem + toChatItem?: AChatItem +} + +export enum ChatPeerType { + Human = "human", + Bot = "bot", +} +// Used in API commands. Chat scope can only be passed with groups. + +export interface ChatRef { + chatType: ChatType + chatId: number // int64 + chatScope?: GroupChatScope +} + +export namespace ChatRef { + export function cmdString(self: ChatRef): string { + return self.chatType.toString() + self.chatId + (self.chatScope ? self.chatScope.toString() : '') + } +} + +export interface ChatSettings { + enableNtfs: MsgFilter + sendRcpts?: boolean + favorite: boolean +} + +export interface ChatStats { + unreadCount: number // int + unreadMentions: number // int + reportsCount: number // int + minUnreadItemId: number // int64 + unreadChat: boolean +} + +export enum ChatType { + Direct = "direct", + Group = "group", + Local = "local", +} + +export namespace ChatType { + export function cmdString(self: ChatType): string { + return self == 'direct' ? '@' : self == 'group' ? '#' : self == 'local' ? '*' : '' + } +} + +export interface ChatWallpaper { + preset?: string + imageFile?: string + background?: string + tint?: string + scaleType?: ChatWallpaperScale + scale?: number // double +} + +export enum ChatWallpaperScale { + Fill = "fill", + Fit = "fit", + Repeat = "repeat", +} + +export enum Color { + Black = "black", + Red = "red", + Green = "green", + Yellow = "yellow", + Blue = "blue", + Magenta = "magenta", + Cyan = "cyan", + White = "white", +} + +export type CommandError = + | CommandError.UNKNOWN + | CommandError.SYNTAX + | CommandError.PROHIBITED + | CommandError.NO_AUTH + | CommandError.HAS_AUTH + | CommandError.NO_ENTITY + +export namespace CommandError { + export type Tag = "UNKNOWN" | "SYNTAX" | "PROHIBITED" | "NO_AUTH" | "HAS_AUTH" | "NO_ENTITY" + + interface Interface { + type: Tag + } + + export interface UNKNOWN extends Interface { + type: "UNKNOWN" + } + + export interface SYNTAX extends Interface { + type: "SYNTAX" + } + + export interface PROHIBITED extends Interface { + type: "PROHIBITED" + } + + export interface NO_AUTH extends Interface { + type: "NO_AUTH" + } + + export interface HAS_AUTH extends Interface { + type: "HAS_AUTH" + } + + export interface NO_ENTITY extends Interface { + type: "NO_ENTITY" + } +} + +export type CommandErrorType = + | CommandErrorType.PROHIBITED + | CommandErrorType.SYNTAX + | CommandErrorType.NO_CONN + | CommandErrorType.SIZE + | CommandErrorType.LARGE + +export namespace CommandErrorType { + export type Tag = "PROHIBITED" | "SYNTAX" | "NO_CONN" | "SIZE" | "LARGE" + + interface Interface { + type: Tag + } + + export interface PROHIBITED extends Interface { + type: "PROHIBITED" + } + + export interface SYNTAX extends Interface { + type: "SYNTAX" + } + + export interface NO_CONN extends Interface { + type: "NO_CONN" + } + + export interface SIZE extends Interface { + type: "SIZE" + } + + export interface LARGE extends Interface { + type: "LARGE" + } +} + +export interface ComposedMessage { + fileSource?: CryptoFile + quotedItemId?: number // int64 + msgContent: MsgContent + mentions: {[key: string]: number} // string : int64 +} + +export enum ConnStatus { + New = "new", + Prepared = "prepared", + Joined = "joined", + Requested = "requested", + Accepted = "accepted", + Snd_ready = "snd-ready", + Ready = "ready", + Deleted = "deleted", +} + +export enum ConnType { + Contact = "contact", + Member = "member", + User_contact = "user_contact", +} + +export interface Connection { + connId: number // int64 + agentConnId: string + connChatVersion: number // int + peerChatVRange: VersionRange + connLevel: number // int + viaContact?: number // int64 + viaUserContactLink?: number // int64 + viaGroupLink: boolean + groupLinkId?: string + xContactId?: string + customUserProfileId?: number // int64 + connType: ConnType + connStatus: ConnStatus + contactConnInitiated: boolean + localAlias: string + entityId?: number // int64 + connectionCode?: SecurityCode + pqSupport: boolean + pqEncryption: boolean + pqSndEnabled?: boolean + pqRcvEnabled?: boolean + authErrCounter: number // int + quotaErrCounter: number // int + createdAt: string // ISO-8601 timestamp +} + +export type ConnectionEntity = + | ConnectionEntity.RcvDirectMsgConnection + | ConnectionEntity.RcvGroupMsgConnection + | ConnectionEntity.SndFileConnection + | ConnectionEntity.RcvFileConnection + | ConnectionEntity.UserContactConnection + +export namespace ConnectionEntity { + export type Tag = + | "rcvDirectMsgConnection" + | "rcvGroupMsgConnection" + | "sndFileConnection" + | "rcvFileConnection" + | "userContactConnection" + + interface Interface { + type: Tag + } + + export interface RcvDirectMsgConnection extends Interface { + type: "rcvDirectMsgConnection" + entityConnection: Connection + contact?: Contact + } + + export interface RcvGroupMsgConnection extends Interface { + type: "rcvGroupMsgConnection" + entityConnection: Connection + groupInfo: GroupInfo + groupMember: GroupMember + } + + export interface SndFileConnection extends Interface { + type: "sndFileConnection" + entityConnection: Connection + sndFileTransfer: SndFileTransfer + } + + export interface RcvFileConnection extends Interface { + type: "rcvFileConnection" + entityConnection: Connection + rcvFileTransfer: RcvFileTransfer + } + + export interface UserContactConnection extends Interface { + type: "userContactConnection" + entityConnection: Connection + userContact: UserContact + } +} + +export type ConnectionErrorType = + | ConnectionErrorType.NOT_FOUND + | ConnectionErrorType.DUPLICATE + | ConnectionErrorType.SIMPLEX + | ConnectionErrorType.NOT_ACCEPTED + | ConnectionErrorType.NOT_AVAILABLE + +export namespace ConnectionErrorType { + export type Tag = "NOT_FOUND" | "DUPLICATE" | "SIMPLEX" | "NOT_ACCEPTED" | "NOT_AVAILABLE" + + interface Interface { + type: Tag + } + + export interface NOT_FOUND extends Interface { + type: "NOT_FOUND" + } + + export interface DUPLICATE extends Interface { + type: "DUPLICATE" + } + + export interface SIMPLEX extends Interface { + type: "SIMPLEX" + } + + export interface NOT_ACCEPTED extends Interface { + type: "NOT_ACCEPTED" + } + + export interface NOT_AVAILABLE extends Interface { + type: "NOT_AVAILABLE" + } +} + +export enum ConnectionMode { + INV = "inv", + CON = "con", +} + +export type ConnectionPlan = + | ConnectionPlan.InvitationLink + | ConnectionPlan.ContactAddress + | ConnectionPlan.GroupLink + | ConnectionPlan.Error + +export namespace ConnectionPlan { + export type Tag = "invitationLink" | "contactAddress" | "groupLink" | "error" + + interface Interface { + type: Tag + } + + export interface InvitationLink extends Interface { + type: "invitationLink" + invitationLinkPlan: InvitationLinkPlan + } + + export interface ContactAddress extends Interface { + type: "contactAddress" + contactAddressPlan: ContactAddressPlan + } + + export interface GroupLink extends Interface { + type: "groupLink" + groupLinkPlan: GroupLinkPlan + } + + export interface Error extends Interface { + type: "error" + chatError: ChatError + } +} + +export interface Contact { + contactId: number // int64 + localDisplayName: string + profile: LocalProfile + activeConn?: Connection + viaGroup?: number // int64 + contactUsed: boolean + contactStatus: ContactStatus + chatSettings: ChatSettings + userPreferences: Preferences + mergedPreferences: ContactUserPreferences + createdAt: string // ISO-8601 timestamp + updatedAt: string // ISO-8601 timestamp + chatTs?: string // ISO-8601 timestamp + preparedContact?: PreparedContact + contactRequestId?: number // int64 + contactGroupMemberId?: number // int64 + contactGrpInvSent: boolean + groupDirectInv?: GroupDirectInvitation + chatTags: number[] // int64 + chatItemTTL?: number // int64 + uiThemes?: UIThemeEntityOverrides + chatDeleted: boolean + customData?: object +} + +export type ContactAddressPlan = + | ContactAddressPlan.Ok + | ContactAddressPlan.OwnLink + | ContactAddressPlan.ConnectingConfirmReconnect + | ContactAddressPlan.ConnectingProhibit + | ContactAddressPlan.Known + | ContactAddressPlan.ContactViaAddress + +export namespace ContactAddressPlan { + export type Tag = + | "ok" + | "ownLink" + | "connectingConfirmReconnect" + | "connectingProhibit" + | "known" + | "contactViaAddress" + + interface Interface { + type: Tag + } + + export interface Ok extends Interface { + type: "ok" + contactSLinkData_?: ContactShortLinkData + } + + export interface OwnLink extends Interface { + type: "ownLink" + } + + export interface ConnectingConfirmReconnect extends Interface { + type: "connectingConfirmReconnect" + } + + export interface ConnectingProhibit extends Interface { + type: "connectingProhibit" + contact: Contact + } + + export interface Known extends Interface { + type: "known" + contact: Contact + } + + export interface ContactViaAddress extends Interface { + type: "contactViaAddress" + contact: Contact + } +} + +export interface ContactShortLinkData { + profile: Profile + message?: MsgContent + business: boolean +} + +export enum ContactStatus { + Active = "active", + Deleted = "deleted", + DeletedByUser = "deletedByUser", +} + +export type ContactUserPref = ContactUserPref.Contact | ContactUserPref.User + +export namespace ContactUserPref { + export type Tag = "contact" | "user" + + interface Interface { + type: Tag + } + + export interface Contact extends Interface { + type: "contact" + preference: SimplePreference + } + + export interface User extends Interface { + type: "user" + preference: SimplePreference + } +} + +export interface ContactUserPreference { + enabled: PrefEnabled + userPreference: ContactUserPref + contactPreference: SimplePreference +} + +export interface ContactUserPreferences { + timedMessages: ContactUserPreference + fullDelete: ContactUserPreference + reactions: ContactUserPreference + voice: ContactUserPreference + files: ContactUserPreference + calls: ContactUserPreference + sessions: ContactUserPreference + commands?: ChatBotCommand[] +} + +export interface CreatedConnLink { + connFullLink: string + connShortLink?: string +} + +export namespace CreatedConnLink { + export function cmdString(self: CreatedConnLink): string { + return self.connFullLink + (self.connShortLink ? ' ' + self.connShortLink : '') + } +} + +export interface CryptoFile { + filePath: string + cryptoArgs?: CryptoFileArgs +} + +export interface CryptoFileArgs { + fileKey: string + fileNonce: string +} + +export interface E2EInfo { + pqEnabled?: boolean +} + +export type ErrorType = + | ErrorType.BLOCK + | ErrorType.SESSION + | ErrorType.CMD + | ErrorType.PROXY + | ErrorType.AUTH + | ErrorType.BLOCKED + | ErrorType.SERVICE + | ErrorType.CRYPTO + | ErrorType.QUOTA + | ErrorType.STORE + | ErrorType.NO_MSG + | ErrorType.LARGE_MSG + | ErrorType.EXPIRED + | ErrorType.INTERNAL + | ErrorType.DUPLICATE_ + +export namespace ErrorType { + export type Tag = + | "BLOCK" + | "SESSION" + | "CMD" + | "PROXY" + | "AUTH" + | "BLOCKED" + | "SERVICE" + | "CRYPTO" + | "QUOTA" + | "STORE" + | "NO_MSG" + | "LARGE_MSG" + | "EXPIRED" + | "INTERNAL" + | "DUPLICATE_" + + interface Interface { + type: Tag + } + + export interface BLOCK extends Interface { + type: "BLOCK" + } + + export interface SESSION extends Interface { + type: "SESSION" + } + + export interface CMD extends Interface { + type: "CMD" + cmdErr: CommandError + } + + export interface PROXY extends Interface { + type: "PROXY" + proxyErr: ProxyError + } + + export interface AUTH extends Interface { + type: "AUTH" + } + + export interface BLOCKED extends Interface { + type: "BLOCKED" + blockInfo: BlockingInfo + } + + export interface SERVICE extends Interface { + type: "SERVICE" + } + + export interface CRYPTO extends Interface { + type: "CRYPTO" + } + + export interface QUOTA extends Interface { + type: "QUOTA" + } + + export interface STORE extends Interface { + type: "STORE" + storeErr: string + } + + export interface NO_MSG extends Interface { + type: "NO_MSG" + } + + export interface LARGE_MSG extends Interface { + type: "LARGE_MSG" + } + + export interface EXPIRED extends Interface { + type: "EXPIRED" + } + + export interface INTERNAL extends Interface { + type: "INTERNAL" + } + + export interface DUPLICATE_ extends Interface { + type: "DUPLICATE_" + } +} + +export enum FeatureAllowed { + Always = "always", + Yes = "yes", + No = "no", +} + +export interface FileDescr { + fileDescrText: string + fileDescrPartNo: number // int + fileDescrComplete: boolean +} + +export type FileError = + | FileError.Auth + | FileError.Blocked + | FileError.NoFile + | FileError.Relay + | FileError.Other + +export namespace FileError { + export type Tag = "auth" | "blocked" | "noFile" | "relay" | "other" + + interface Interface { + type: Tag + } + + export interface Auth extends Interface { + type: "auth" + } + + export interface Blocked extends Interface { + type: "blocked" + server: string + blockInfo: BlockingInfo + } + + export interface NoFile extends Interface { + type: "noFile" + } + + export interface Relay extends Interface { + type: "relay" + srvError: SrvError + } + + export interface Other extends Interface { + type: "other" + fileError: string + } +} + +export type FileErrorType = + | FileErrorType.NOT_APPROVED + | FileErrorType.SIZE + | FileErrorType.REDIRECT + | FileErrorType.FILE_IO + | FileErrorType.NO_FILE + +export namespace FileErrorType { + export type Tag = "NOT_APPROVED" | "SIZE" | "REDIRECT" | "FILE_IO" | "NO_FILE" + + interface Interface { + type: Tag + } + + export interface NOT_APPROVED extends Interface { + type: "NOT_APPROVED" + } + + export interface SIZE extends Interface { + type: "SIZE" + } + + export interface REDIRECT extends Interface { + type: "REDIRECT" + redirectError: string + } + + export interface FILE_IO extends Interface { + type: "FILE_IO" + fileIOError: string + } + + export interface NO_FILE extends Interface { + type: "NO_FILE" + } +} + +export interface FileInvitation { + fileName: string + fileSize: number // int64 + fileDigest?: string + fileConnReq?: string + fileInline?: InlineFileMode + fileDescr?: FileDescr +} + +export enum FileProtocol { + SMP = "smp", + XFTP = "xftp", + LOCAL = "local", +} + +export enum FileStatus { + New = "new", + Accepted = "accepted", + Connected = "connected", + Complete = "complete", + Cancelled = "cancelled", +} + +export interface FileTransferMeta { + fileId: number // int64 + xftpSndFile?: XFTPSndFile + xftpRedirectFor?: number // int64 + fileName: string + filePath: string + fileSize: number // int64 + fileInline?: InlineFileMode + chunkSize: number // int64 + cancelled: boolean +} + +export type Format = + | Format.Bold + | Format.Italic + | Format.StrikeThrough + | Format.Snippet + | Format.Secret + | Format.Colored + | Format.Uri + | Format.HyperLink + | Format.SimplexLink + | Format.Command + | Format.Mention + | Format.Email + | Format.Phone + +export namespace Format { + export type Tag = + | "bold" + | "italic" + | "strikeThrough" + | "snippet" + | "secret" + | "colored" + | "uri" + | "hyperLink" + | "simplexLink" + | "command" + | "mention" + | "email" + | "phone" + + interface Interface { + type: Tag + } + + export interface Bold extends Interface { + type: "bold" + } + + export interface Italic extends Interface { + type: "italic" + } + + export interface StrikeThrough extends Interface { + type: "strikeThrough" + } + + export interface Snippet extends Interface { + type: "snippet" + } + + export interface Secret extends Interface { + type: "secret" + } + + export interface Colored extends Interface { + type: "colored" + color: Color + } + + export interface Uri extends Interface { + type: "uri" + } + + export interface HyperLink extends Interface { + type: "hyperLink" + showText?: string + linkUri: string + } + + export interface SimplexLink extends Interface { + type: "simplexLink" + showText?: string + linkType: SimplexLinkType + simplexUri: string + smpHosts: string[] // non-empty + } + + export interface Command extends Interface { + type: "command" + commandStr: string + } + + export interface Mention extends Interface { + type: "mention" + memberName: string + } + + export interface Email extends Interface { + type: "email" + } + + export interface Phone extends Interface { + type: "phone" + } +} + +export interface FormattedText { + format?: Format + text: string +} + +export interface FullGroupPreferences { + timedMessages: TimedMessagesGroupPreference + directMessages: RoleGroupPreference + fullDelete: GroupPreference + reactions: GroupPreference + voice: RoleGroupPreference + files: RoleGroupPreference + simplexLinks: RoleGroupPreference + reports: GroupPreference + history: GroupPreference + sessions: RoleGroupPreference + commands: ChatBotCommand[] +} + +export interface FullPreferences { + timedMessages: TimedMessagesPreference + fullDelete: SimplePreference + reactions: SimplePreference + voice: SimplePreference + files: SimplePreference + calls: SimplePreference + sessions: SimplePreference + commands: ChatBotCommand[] +} + +export interface Group { + groupInfo: GroupInfo + members: GroupMember[] +} + +export type GroupChatScope = GroupChatScope.MemberSupport + +export namespace GroupChatScope { + export type Tag = "memberSupport" + + interface Interface { + type: Tag + } + + export interface MemberSupport extends Interface { + type: "memberSupport" + groupMemberId_?: number // int64 + } + + export function cmdString(self: GroupChatScope): string { + return '(_support' + (self.groupMemberId_ ? ':' + self.groupMemberId_ : '') + ')' + } +} + +export type GroupChatScopeInfo = GroupChatScopeInfo.MemberSupport + +export namespace GroupChatScopeInfo { + export type Tag = "memberSupport" + + interface Interface { + type: Tag + } + + export interface MemberSupport extends Interface { + type: "memberSupport" + groupMember_?: GroupMember + } +} + +export interface GroupDirectInvitation { + groupDirectInvLink: string + fromGroupId_?: number // int64 + fromGroupMemberId_?: number // int64 + fromGroupMemberConnId_?: number // int64 + groupDirectInvStartedConnection: boolean +} + +export enum GroupFeature { + TimedMessages = "timedMessages", + DirectMessages = "directMessages", + FullDelete = "fullDelete", + Reactions = "reactions", + Voice = "voice", + Files = "files", + SimplexLinks = "simplexLinks", + Reports = "reports", + History = "history", + Sessions = "sessions", +} + +export enum GroupFeatureEnabled { + On = "on", + Off = "off", +} + +export interface GroupInfo { + groupId: number // int64 + localDisplayName: string + groupProfile: GroupProfile + localAlias: string + businessChat?: BusinessChatInfo + fullGroupPreferences: FullGroupPreferences + membership: GroupMember + chatSettings: ChatSettings + createdAt: string // ISO-8601 timestamp + updatedAt: string // ISO-8601 timestamp + chatTs?: string // ISO-8601 timestamp + userMemberProfileSentAt?: string // ISO-8601 timestamp + preparedGroup?: PreparedGroup + chatTags: number[] // int64 + chatItemTTL?: number // int64 + uiThemes?: UIThemeEntityOverrides + customData?: object + membersRequireAttention: number // int + viaGroupLinkUri?: string +} + +export interface GroupInfoSummary { + groupInfo: GroupInfo + groupSummary: GroupSummary +} + +export interface GroupLink { + userContactLinkId: number // int64 + connLinkContact: CreatedConnLink + shortLinkDataSet: boolean + shortLinkLargeDataSet: boolean + groupLinkId: string + acceptMemberRole: GroupMemberRole +} + +export type GroupLinkPlan = + | GroupLinkPlan.Ok + | GroupLinkPlan.OwnLink + | GroupLinkPlan.ConnectingConfirmReconnect + | GroupLinkPlan.ConnectingProhibit + | GroupLinkPlan.Known + +export namespace GroupLinkPlan { + export type Tag = "ok" | "ownLink" | "connectingConfirmReconnect" | "connectingProhibit" | "known" + + interface Interface { + type: Tag + } + + export interface Ok extends Interface { + type: "ok" + groupSLinkData_?: GroupShortLinkData + } + + export interface OwnLink extends Interface { + type: "ownLink" + groupInfo: GroupInfo + } + + export interface ConnectingConfirmReconnect extends Interface { + type: "connectingConfirmReconnect" + } + + export interface ConnectingProhibit extends Interface { + type: "connectingProhibit" + groupInfo_?: GroupInfo + } + + export interface Known extends Interface { + type: "known" + groupInfo: GroupInfo + } +} + +export interface GroupMember { + groupMemberId: number // int64 + groupId: number // int64 + memberId: string + memberRole: GroupMemberRole + memberCategory: GroupMemberCategory + memberStatus: GroupMemberStatus + memberSettings: GroupMemberSettings + blockedByAdmin: boolean + invitedBy: InvitedBy + invitedByGroupMemberId?: number // int64 + localDisplayName: string + memberProfile: LocalProfile + memberContactId?: number // int64 + memberContactProfileId: number // int64 + activeConn?: Connection + memberChatVRange: VersionRange + createdAt: string // ISO-8601 timestamp + updatedAt: string // ISO-8601 timestamp + supportChat?: GroupSupportChat +} + +export interface GroupMemberAdmission { + review?: MemberCriteria +} + +export enum GroupMemberCategory { + User = "user", + Invitee = "invitee", + Host = "host", + Pre = "pre", + Post = "post", +} + +export interface GroupMemberRef { + groupMemberId: number // int64 + profile: Profile +} + +export enum GroupMemberRole { + Observer = "observer", + Author = "author", + Member = "member", + Moderator = "moderator", + Admin = "admin", + Owner = "owner", +} + +export interface GroupMemberSettings { + showMessages: boolean +} + +export enum GroupMemberStatus { + Rejected = "rejected", + Removed = "removed", + Left = "left", + Deleted = "deleted", + Unknown = "unknown", + Invited = "invited", + Pending_approval = "pending_approval", + Pending_review = "pending_review", + Introduced = "introduced", + Intro_inv = "intro-inv", + Accepted = "accepted", + Announced = "announced", + Connected = "connected", + Complete = "complete", + Creator = "creator", +} + +export interface GroupPreference { + enable: GroupFeatureEnabled +} + +export interface GroupPreferences { + timedMessages?: TimedMessagesGroupPreference + directMessages?: RoleGroupPreference + fullDelete?: GroupPreference + reactions?: GroupPreference + voice?: RoleGroupPreference + files?: RoleGroupPreference + simplexLinks?: RoleGroupPreference + reports?: GroupPreference + history?: GroupPreference + sessions?: RoleGroupPreference + commands?: ChatBotCommand[] +} + +export interface GroupProfile { + displayName: string + fullName: string + shortDescr?: string + description?: string + image?: string + groupPreferences?: GroupPreferences + memberAdmission?: GroupMemberAdmission +} + +export interface GroupShortLinkData { + groupProfile: GroupProfile +} + +export interface GroupSummary { + currentMembers: number // int +} + +export interface GroupSupportChat { + chatTs: string // ISO-8601 timestamp + unread: number // int64 + memberAttention: number // int64 + mentions: number // int64 + lastMsgFromMemberTs?: string // ISO-8601 timestamp +} + +export enum HandshakeError { + PARSE = "PARSE", + IDENTITY = "IDENTITY", + BAD_AUTH = "BAD_AUTH", + BAD_SERVICE = "BAD_SERVICE", +} + +export enum InlineFileMode { + Offer = "offer", + Sent = "sent", +} + +export type InvitationLinkPlan = + | InvitationLinkPlan.Ok + | InvitationLinkPlan.OwnLink + | InvitationLinkPlan.Connecting + | InvitationLinkPlan.Known + +export namespace InvitationLinkPlan { + export type Tag = "ok" | "ownLink" | "connecting" | "known" + + interface Interface { + type: Tag + } + + export interface Ok extends Interface { + type: "ok" + contactSLinkData_?: ContactShortLinkData + } + + export interface OwnLink extends Interface { + type: "ownLink" + } + + export interface Connecting extends Interface { + type: "connecting" + contact_?: Contact + } + + export interface Known extends Interface { + type: "known" + contact: Contact + } +} + +export type InvitedBy = InvitedBy.Contact | InvitedBy.User | InvitedBy.Unknown + +export namespace InvitedBy { + export type Tag = "contact" | "user" | "unknown" + + interface Interface { + type: Tag + } + + export interface Contact extends Interface { + type: "contact" + byContactId: number // int64 + } + + export interface User extends Interface { + type: "user" + } + + export interface Unknown extends Interface { + type: "unknown" + } +} + +export type LinkContent = LinkContent.Page | LinkContent.Image | LinkContent.Video | LinkContent.Unknown + +export namespace LinkContent { + export type Tag = "page" | "image" | "video" | "unknown" + + interface Interface { + type: Tag + } + + export interface Page extends Interface { + type: "page" + } + + export interface Image extends Interface { + type: "image" + } + + export interface Video extends Interface { + type: "video" + duration?: number // int + } + + export interface Unknown extends Interface { + type: "unknown" + tag: string + json: object + } +} + +export interface LinkPreview { + uri: string + title: string + description: string + image: string + content?: LinkContent +} + +export interface LocalProfile { + profileId: number // int64 + displayName: string + fullName: string + shortDescr?: string + image?: string + contactLink?: string + preferences?: Preferences + peerType?: ChatPeerType + localAlias: string +} + +export enum MemberCriteria { + All = "all", +} +// Connection link sent in a message - only short links are allowed. + +export type MsgChatLink = MsgChatLink.Contact | MsgChatLink.Invitation | MsgChatLink.Group + +export namespace MsgChatLink { + export type Tag = "contact" | "invitation" | "group" + + interface Interface { + type: Tag + } + + export interface Contact extends Interface { + type: "contact" + connLink: string + profile: Profile + business: boolean + } + + export interface Invitation extends Interface { + type: "invitation" + invLink: string + profile: Profile + } + + export interface Group extends Interface { + type: "group" + connLink: string + groupProfile: GroupProfile + } +} + +export type MsgContent = + | MsgContent.Text + | MsgContent.Link + | MsgContent.Image + | MsgContent.Video + | MsgContent.Voice + | MsgContent.File + | MsgContent.Report + | MsgContent.Chat + | MsgContent.Unknown + +export namespace MsgContent { + export type Tag = "text" | "link" | "image" | "video" | "voice" | "file" | "report" | "chat" | "unknown" + + interface Interface { + type: Tag + } + + export interface Text extends Interface { + type: "text" + text: string + } + + export interface Link extends Interface { + type: "link" + text: string + preview: LinkPreview + } + + export interface Image extends Interface { + type: "image" + text: string + image: string + } + + export interface Video extends Interface { + type: "video" + text: string + image: string + duration: number // int + } + + export interface Voice extends Interface { + type: "voice" + text: string + duration: number // int + } + + export interface File extends Interface { + type: "file" + text: string + } + + export interface Report extends Interface { + type: "report" + text: string + reason: ReportReason + } + + export interface Chat extends Interface { + type: "chat" + text: string + chatLink: MsgChatLink + } + + export interface Unknown extends Interface { + type: "unknown" + tag: string + text: string + json: object + } +} + +export enum MsgDecryptError { + RatchetHeader = "ratchetHeader", + TooManySkipped = "tooManySkipped", + RatchetEarlier = "ratchetEarlier", + Other = "other", + RatchetSync = "ratchetSync", +} + +export enum MsgDirection { + Rcv = "rcv", + Snd = "snd", +} + +export type MsgErrorType = + | MsgErrorType.MsgSkipped + | MsgErrorType.MsgBadId + | MsgErrorType.MsgBadHash + | MsgErrorType.MsgDuplicate + +export namespace MsgErrorType { + export type Tag = "msgSkipped" | "msgBadId" | "msgBadHash" | "msgDuplicate" + + interface Interface { + type: Tag + } + + export interface MsgSkipped extends Interface { + type: "msgSkipped" + fromMsgId: number // int64 + toMsgId: number // int64 + } + + export interface MsgBadId extends Interface { + type: "msgBadId" + msgId: number // int64 + } + + export interface MsgBadHash extends Interface { + type: "msgBadHash" + } + + export interface MsgDuplicate extends Interface { + type: "msgDuplicate" + } +} + +export enum MsgFilter { + None = "none", + All = "all", + Mentions = "mentions", +} + +export type MsgReaction = MsgReaction.Emoji | MsgReaction.Unknown + +export namespace MsgReaction { + export type Tag = "emoji" | "unknown" + + interface Interface { + type: Tag + } + + export interface Emoji extends Interface { + type: "emoji" + emoji: string + } + + export interface Unknown extends Interface { + type: "unknown" + tag: string + json: object + } +} + +export enum MsgReceiptStatus { + Ok = "ok", + BadMsgHash = "badMsgHash", +} + +export interface NewUser { + profile?: Profile + pastTimestamp: boolean +} + +export interface NoteFolder { + noteFolderId: number // int64 + userId: number // int64 + createdAt: string // ISO-8601 timestamp + updatedAt: string // ISO-8601 timestamp + chatTs: string // ISO-8601 timestamp + favorite: boolean + unread: boolean +} + +export interface PendingContactConnection { + pccConnId: number // int64 + pccAgentConnId: string + pccConnStatus: ConnStatus + viaContactUri: boolean + viaUserContactLink?: number // int64 + groupLinkId?: string + customUserProfileId?: number // int64 + connLinkInv?: CreatedConnLink + localAlias: string + createdAt: string // ISO-8601 timestamp + updatedAt: string // ISO-8601 timestamp +} + +export interface PrefEnabled { + forUser: boolean + forContact: boolean +} + +export interface Preferences { + timedMessages?: TimedMessagesPreference + fullDelete?: SimplePreference + reactions?: SimplePreference + voice?: SimplePreference + files?: SimplePreference + calls?: SimplePreference + sessions?: SimplePreference + commands?: ChatBotCommand[] +} + +export interface PreparedContact { + connLinkToConnect: CreatedConnLink + uiConnLinkType: ConnectionMode + welcomeSharedMsgId?: string + requestSharedMsgId?: string +} + +export interface PreparedGroup { + connLinkToConnect: CreatedConnLink + connLinkPreparedConnection: boolean + connLinkStartedConnection: boolean + welcomeSharedMsgId?: string + requestSharedMsgId?: string +} + +export interface Profile { + displayName: string + fullName: string + shortDescr?: string + image?: string + contactLink?: string + preferences?: Preferences + peerType?: ChatPeerType +} + +export type ProxyClientError = + | ProxyClientError.ProtocolError + | ProxyClientError.UnexpectedResponse + | ProxyClientError.ResponseError + +export namespace ProxyClientError { + export type Tag = "protocolError" | "unexpectedResponse" | "responseError" + + interface Interface { + type: Tag + } + + export interface ProtocolError extends Interface { + type: "protocolError" + protocolErr: ErrorType + } + + export interface UnexpectedResponse extends Interface { + type: "unexpectedResponse" + responseStr: string + } + + export interface ResponseError extends Interface { + type: "responseError" + responseErr: ErrorType + } +} + +export type ProxyError = ProxyError.PROTOCOL | ProxyError.BROKER | ProxyError.BASIC_AUTH | ProxyError.NO_SESSION + +export namespace ProxyError { + export type Tag = "PROTOCOL" | "BROKER" | "BASIC_AUTH" | "NO_SESSION" + + interface Interface { + type: Tag + } + + export interface PROTOCOL extends Interface { + type: "PROTOCOL" + protocolErr: ErrorType + } + + export interface BROKER extends Interface { + type: "BROKER" + brokerErr: BrokerErrorType + } + + export interface BASIC_AUTH extends Interface { + type: "BASIC_AUTH" + } + + export interface NO_SESSION extends Interface { + type: "NO_SESSION" + } +} + +export type RCErrorType = + | RCErrorType.Internal + | RCErrorType.Identity + | RCErrorType.NoLocalAddress + | RCErrorType.NewController + | RCErrorType.NotDiscovered + | RCErrorType.TLSStartFailed + | RCErrorType.Exception + | RCErrorType.CtrlAuth + | RCErrorType.CtrlNotFound + | RCErrorType.CtrlError + | RCErrorType.Invitation + | RCErrorType.Version + | RCErrorType.Encrypt + | RCErrorType.Decrypt + | RCErrorType.BlockSize + | RCErrorType.Syntax + +export namespace RCErrorType { + export type Tag = + | "internal" + | "identity" + | "noLocalAddress" + | "newController" + | "notDiscovered" + | "tLSStartFailed" + | "exception" + | "ctrlAuth" + | "ctrlNotFound" + | "ctrlError" + | "invitation" + | "version" + | "encrypt" + | "decrypt" + | "blockSize" + | "syntax" + + interface Interface { + type: Tag + } + + export interface Internal extends Interface { + type: "internal" + internalErr: string + } + + export interface Identity extends Interface { + type: "identity" + } + + export interface NoLocalAddress extends Interface { + type: "noLocalAddress" + } + + export interface NewController extends Interface { + type: "newController" + } + + export interface NotDiscovered extends Interface { + type: "notDiscovered" + } + + export interface TLSStartFailed extends Interface { + type: "tLSStartFailed" + } + + export interface Exception extends Interface { + type: "exception" + exception: string + } + + export interface CtrlAuth extends Interface { + type: "ctrlAuth" + } + + export interface CtrlNotFound extends Interface { + type: "ctrlNotFound" + } + + export interface CtrlError extends Interface { + type: "ctrlError" + ctrlErr: string + } + + export interface Invitation extends Interface { + type: "invitation" + } + + export interface Version extends Interface { + type: "version" + } + + export interface Encrypt extends Interface { + type: "encrypt" + } + + export interface Decrypt extends Interface { + type: "decrypt" + } + + export interface BlockSize extends Interface { + type: "blockSize" + } + + export interface Syntax extends Interface { + type: "syntax" + syntaxErr: string + } +} + +export enum RatchetSyncState { + Ok = "ok", + Allowed = "allowed", + Required = "required", + Started = "started", + Agreed = "agreed", +} + +export type RcvConnEvent = + | RcvConnEvent.SwitchQueue + | RcvConnEvent.RatchetSync + | RcvConnEvent.VerificationCodeReset + | RcvConnEvent.PqEnabled + +export namespace RcvConnEvent { + export type Tag = "switchQueue" | "ratchetSync" | "verificationCodeReset" | "pqEnabled" + + interface Interface { + type: Tag + } + + export interface SwitchQueue extends Interface { + type: "switchQueue" + phase: SwitchPhase + } + + export interface RatchetSync extends Interface { + type: "ratchetSync" + syncStatus: RatchetSyncState + } + + export interface VerificationCodeReset extends Interface { + type: "verificationCodeReset" + } + + export interface PqEnabled extends Interface { + type: "pqEnabled" + enabled: boolean + } +} + +export type RcvDirectEvent = + | RcvDirectEvent.ContactDeleted + | RcvDirectEvent.ProfileUpdated + | RcvDirectEvent.GroupInvLinkReceived + +export namespace RcvDirectEvent { + export type Tag = "contactDeleted" | "profileUpdated" | "groupInvLinkReceived" + + interface Interface { + type: Tag + } + + export interface ContactDeleted extends Interface { + type: "contactDeleted" + } + + export interface ProfileUpdated extends Interface { + type: "profileUpdated" + fromProfile: Profile + toProfile: Profile + } + + export interface GroupInvLinkReceived extends Interface { + type: "groupInvLinkReceived" + groupProfile: GroupProfile + } +} + +export interface RcvFileDescr { + fileDescrId: number // int64 + fileDescrText: string + fileDescrPartNo: number // int + fileDescrComplete: boolean +} + +export interface RcvFileInfo { + filePath: string + connId?: number // int64 + agentConnId?: string +} + +export type RcvFileStatus = + | RcvFileStatus.New + | RcvFileStatus.Accepted + | RcvFileStatus.Connected + | RcvFileStatus.Complete + | RcvFileStatus.Cancelled + +export namespace RcvFileStatus { + export type Tag = "new" | "accepted" | "connected" | "complete" | "cancelled" + + interface Interface { + type: Tag + } + + export interface New extends Interface { + type: "new" + } + + export interface Accepted extends Interface { + type: "accepted" + fileInfo: RcvFileInfo + } + + export interface Connected extends Interface { + type: "connected" + fileInfo: RcvFileInfo + } + + export interface Complete extends Interface { + type: "complete" + fileInfo: RcvFileInfo + } + + export interface Cancelled extends Interface { + type: "cancelled" + fileInfo_?: RcvFileInfo + } +} + +export interface RcvFileTransfer { + fileId: number // int64 + xftpRcvFile?: XFTPRcvFile + fileInvitation: FileInvitation + fileStatus: RcvFileStatus + rcvFileInline?: InlineFileMode + senderDisplayName: string + chunkSize: number // int64 + cancelled: boolean + grpMemberId?: number // int64 + cryptoArgs?: CryptoFileArgs +} + +export type RcvGroupEvent = + | RcvGroupEvent.MemberAdded + | RcvGroupEvent.MemberConnected + | RcvGroupEvent.MemberAccepted + | RcvGroupEvent.UserAccepted + | RcvGroupEvent.MemberLeft + | RcvGroupEvent.MemberRole + | RcvGroupEvent.MemberBlocked + | RcvGroupEvent.UserRole + | RcvGroupEvent.MemberDeleted + | RcvGroupEvent.UserDeleted + | RcvGroupEvent.GroupDeleted + | RcvGroupEvent.GroupUpdated + | RcvGroupEvent.InvitedViaGroupLink + | RcvGroupEvent.MemberCreatedContact + | RcvGroupEvent.MemberProfileUpdated + | RcvGroupEvent.NewMemberPendingReview + +export namespace RcvGroupEvent { + export type Tag = + | "memberAdded" + | "memberConnected" + | "memberAccepted" + | "userAccepted" + | "memberLeft" + | "memberRole" + | "memberBlocked" + | "userRole" + | "memberDeleted" + | "userDeleted" + | "groupDeleted" + | "groupUpdated" + | "invitedViaGroupLink" + | "memberCreatedContact" + | "memberProfileUpdated" + | "newMemberPendingReview" + + interface Interface { + type: Tag + } + + export interface MemberAdded extends Interface { + type: "memberAdded" + groupMemberId: number // int64 + profile: Profile + } + + export interface MemberConnected extends Interface { + type: "memberConnected" + } + + export interface MemberAccepted extends Interface { + type: "memberAccepted" + groupMemberId: number // int64 + profile: Profile + } + + export interface UserAccepted extends Interface { + type: "userAccepted" + } + + export interface MemberLeft extends Interface { + type: "memberLeft" + } + + export interface MemberRole extends Interface { + type: "memberRole" + groupMemberId: number // int64 + profile: Profile + role: GroupMemberRole + } + + export interface MemberBlocked extends Interface { + type: "memberBlocked" + groupMemberId: number // int64 + profile: Profile + blocked: boolean + } + + export interface UserRole extends Interface { + type: "userRole" + role: GroupMemberRole + } + + export interface MemberDeleted extends Interface { + type: "memberDeleted" + groupMemberId: number // int64 + profile: Profile + } + + export interface UserDeleted extends Interface { + type: "userDeleted" + } + + export interface GroupDeleted extends Interface { + type: "groupDeleted" + } + + export interface GroupUpdated extends Interface { + type: "groupUpdated" + groupProfile: GroupProfile + } + + export interface InvitedViaGroupLink extends Interface { + type: "invitedViaGroupLink" + } + + export interface MemberCreatedContact extends Interface { + type: "memberCreatedContact" + } + + export interface MemberProfileUpdated extends Interface { + type: "memberProfileUpdated" + fromProfile: Profile + toProfile: Profile + } + + export interface NewMemberPendingReview extends Interface { + type: "newMemberPendingReview" + } +} + +export enum ReportReason { + Spam = "spam", + Content = "content", + Community = "community", + Profile = "profile", + Other = "other", +} + +export interface RoleGroupPreference { + enable: GroupFeatureEnabled + role?: GroupMemberRole +} + +export type SMPAgentError = + | SMPAgentError.A_MESSAGE + | SMPAgentError.A_PROHIBITED + | SMPAgentError.A_VERSION + | SMPAgentError.A_LINK + | SMPAgentError.A_CRYPTO + | SMPAgentError.A_DUPLICATE + | SMPAgentError.A_QUEUE + +export namespace SMPAgentError { + export type Tag = + | "A_MESSAGE" + | "A_PROHIBITED" + | "A_VERSION" + | "A_LINK" + | "A_CRYPTO" + | "A_DUPLICATE" + | "A_QUEUE" + + interface Interface { + type: Tag + } + + export interface A_MESSAGE extends Interface { + type: "A_MESSAGE" + } + + export interface A_PROHIBITED extends Interface { + type: "A_PROHIBITED" + prohibitedErr: string + } + + export interface A_VERSION extends Interface { + type: "A_VERSION" + } + + export interface A_LINK extends Interface { + type: "A_LINK" + linkErr: string + } + + export interface A_CRYPTO extends Interface { + type: "A_CRYPTO" + cryptoErr: AgentCryptoError + } + + export interface A_DUPLICATE extends Interface { + type: "A_DUPLICATE" + } + + export interface A_QUEUE extends Interface { + type: "A_QUEUE" + queueErr: string + } +} + +export interface SecurityCode { + securityCode: string + verifiedAt: string // ISO-8601 timestamp +} + +export interface SimplePreference { + allow: FeatureAllowed +} + +export enum SimplexLinkType { + Contact = "contact", + Invitation = "invitation", + Group = "group", + Channel = "channel", + Relay = "relay", +} + +export enum SndCIStatusProgress { + Partial = "partial", + Complete = "complete", +} + +export type SndConnEvent = SndConnEvent.SwitchQueue | SndConnEvent.RatchetSync | SndConnEvent.PqEnabled + +export namespace SndConnEvent { + export type Tag = "switchQueue" | "ratchetSync" | "pqEnabled" + + interface Interface { + type: Tag + } + + export interface SwitchQueue extends Interface { + type: "switchQueue" + phase: SwitchPhase + member?: GroupMemberRef + } + + export interface RatchetSync extends Interface { + type: "ratchetSync" + syncStatus: RatchetSyncState + member?: GroupMemberRef + } + + export interface PqEnabled extends Interface { + type: "pqEnabled" + enabled: boolean + } +} + +export type SndError = + | SndError.Auth + | SndError.Quota + | SndError.Expired + | SndError.Relay + | SndError.Proxy + | SndError.ProxyRelay + | SndError.Other + +export namespace SndError { + export type Tag = "auth" | "quota" | "expired" | "relay" | "proxy" | "proxyRelay" | "other" + + interface Interface { + type: Tag + } + + export interface Auth extends Interface { + type: "auth" + } + + export interface Quota extends Interface { + type: "quota" + } + + export interface Expired extends Interface { + type: "expired" + } + + export interface Relay extends Interface { + type: "relay" + srvError: SrvError + } + + export interface Proxy extends Interface { + type: "proxy" + proxyServer: string + srvError: SrvError + } + + export interface ProxyRelay extends Interface { + type: "proxyRelay" + proxyServer: string + srvError: SrvError + } + + export interface Other extends Interface { + type: "other" + sndError: string + } +} + +export interface SndFileTransfer { + fileId: number // int64 + fileName: string + filePath: string + fileSize: number // int64 + chunkSize: number // int64 + recipientDisplayName: string + connId: number // int64 + agentConnId: string + groupMemberId?: number // int64 + fileStatus: FileStatus + fileDescrId?: number // int64 + fileInline?: InlineFileMode +} + +export type SndGroupEvent = + | SndGroupEvent.MemberRole + | SndGroupEvent.MemberBlocked + | SndGroupEvent.UserRole + | SndGroupEvent.MemberDeleted + | SndGroupEvent.UserLeft + | SndGroupEvent.GroupUpdated + | SndGroupEvent.MemberAccepted + | SndGroupEvent.UserPendingReview + +export namespace SndGroupEvent { + export type Tag = + | "memberRole" + | "memberBlocked" + | "userRole" + | "memberDeleted" + | "userLeft" + | "groupUpdated" + | "memberAccepted" + | "userPendingReview" + + interface Interface { + type: Tag + } + + export interface MemberRole extends Interface { + type: "memberRole" + groupMemberId: number // int64 + profile: Profile + role: GroupMemberRole + } + + export interface MemberBlocked extends Interface { + type: "memberBlocked" + groupMemberId: number // int64 + profile: Profile + blocked: boolean + } + + export interface UserRole extends Interface { + type: "userRole" + role: GroupMemberRole + } + + export interface MemberDeleted extends Interface { + type: "memberDeleted" + groupMemberId: number // int64 + profile: Profile + } + + export interface UserLeft extends Interface { + type: "userLeft" + } + + export interface GroupUpdated extends Interface { + type: "groupUpdated" + groupProfile: GroupProfile + } + + export interface MemberAccepted extends Interface { + type: "memberAccepted" + groupMemberId: number // int64 + profile: Profile + } + + export interface UserPendingReview extends Interface { + type: "userPendingReview" + } +} + +export type SrvError = SrvError.Host | SrvError.Version | SrvError.Other + +export namespace SrvError { + export type Tag = "host" | "version" | "other" + + interface Interface { + type: Tag + } + + export interface Host extends Interface { + type: "host" + } + + export interface Version extends Interface { + type: "version" + } + + export interface Other extends Interface { + type: "other" + srvError: string + } +} + +export type StoreError = + | StoreError.DuplicateName + | StoreError.UserNotFound + | StoreError.UserNotFoundByName + | StoreError.UserNotFoundByContactId + | StoreError.UserNotFoundByGroupId + | StoreError.UserNotFoundByFileId + | StoreError.UserNotFoundByContactRequestId + | StoreError.ContactNotFound + | StoreError.ContactNotFoundByName + | StoreError.ContactNotFoundByMemberId + | StoreError.ContactNotReady + | StoreError.DuplicateContactLink + | StoreError.UserContactLinkNotFound + | StoreError.ContactRequestNotFound + | StoreError.ContactRequestNotFoundByName + | StoreError.InvalidContactRequestEntity + | StoreError.InvalidBusinessChatContactRequest + | StoreError.GroupNotFound + | StoreError.GroupNotFoundByName + | StoreError.GroupMemberNameNotFound + | StoreError.GroupMemberNotFound + | StoreError.GroupHostMemberNotFound + | StoreError.GroupMemberNotFoundByMemberId + | StoreError.MemberContactGroupMemberNotFound + | StoreError.GroupWithoutUser + | StoreError.DuplicateGroupMember + | StoreError.GroupAlreadyJoined + | StoreError.GroupInvitationNotFound + | StoreError.NoteFolderAlreadyExists + | StoreError.NoteFolderNotFound + | StoreError.UserNoteFolderNotFound + | StoreError.SndFileNotFound + | StoreError.SndFileInvalid + | StoreError.RcvFileNotFound + | StoreError.RcvFileDescrNotFound + | StoreError.FileNotFound + | StoreError.RcvFileInvalid + | StoreError.RcvFileInvalidDescrPart + | StoreError.LocalFileNoTransfer + | StoreError.SharedMsgIdNotFoundByFileId + | StoreError.FileIdNotFoundBySharedMsgId + | StoreError.SndFileNotFoundXFTP + | StoreError.RcvFileNotFoundXFTP + | StoreError.ConnectionNotFound + | StoreError.ConnectionNotFoundById + | StoreError.ConnectionNotFoundByMemberId + | StoreError.PendingConnectionNotFound + | StoreError.IntroNotFound + | StoreError.UniqueID + | StoreError.LargeMsg + | StoreError.InternalError + | StoreError.DBException + | StoreError.DBBusyError + | StoreError.BadChatItem + | StoreError.ChatItemNotFound + | StoreError.ChatItemNotFoundByText + | StoreError.ChatItemSharedMsgIdNotFound + | StoreError.ChatItemNotFoundByFileId + | StoreError.ChatItemNotFoundByContactId + | StoreError.ChatItemNotFoundByGroupId + | StoreError.ProfileNotFound + | StoreError.DuplicateGroupLink + | StoreError.GroupLinkNotFound + | StoreError.HostMemberIdNotFound + | StoreError.ContactNotFoundByFileId + | StoreError.NoGroupSndStatus + | StoreError.DuplicateGroupMessage + | StoreError.RemoteHostNotFound + | StoreError.RemoteHostUnknown + | StoreError.RemoteHostDuplicateCA + | StoreError.RemoteCtrlNotFound + | StoreError.RemoteCtrlDuplicateCA + | StoreError.ProhibitedDeleteUser + | StoreError.OperatorNotFound + | StoreError.UsageConditionsNotFound + | StoreError.InvalidQuote + | StoreError.InvalidMention + +export namespace StoreError { + export type Tag = + | "duplicateName" + | "userNotFound" + | "userNotFoundByName" + | "userNotFoundByContactId" + | "userNotFoundByGroupId" + | "userNotFoundByFileId" + | "userNotFoundByContactRequestId" + | "contactNotFound" + | "contactNotFoundByName" + | "contactNotFoundByMemberId" + | "contactNotReady" + | "duplicateContactLink" + | "userContactLinkNotFound" + | "contactRequestNotFound" + | "contactRequestNotFoundByName" + | "invalidContactRequestEntity" + | "invalidBusinessChatContactRequest" + | "groupNotFound" + | "groupNotFoundByName" + | "groupMemberNameNotFound" + | "groupMemberNotFound" + | "groupHostMemberNotFound" + | "groupMemberNotFoundByMemberId" + | "memberContactGroupMemberNotFound" + | "groupWithoutUser" + | "duplicateGroupMember" + | "groupAlreadyJoined" + | "groupInvitationNotFound" + | "noteFolderAlreadyExists" + | "noteFolderNotFound" + | "userNoteFolderNotFound" + | "sndFileNotFound" + | "sndFileInvalid" + | "rcvFileNotFound" + | "rcvFileDescrNotFound" + | "fileNotFound" + | "rcvFileInvalid" + | "rcvFileInvalidDescrPart" + | "localFileNoTransfer" + | "sharedMsgIdNotFoundByFileId" + | "fileIdNotFoundBySharedMsgId" + | "sndFileNotFoundXFTP" + | "rcvFileNotFoundXFTP" + | "connectionNotFound" + | "connectionNotFoundById" + | "connectionNotFoundByMemberId" + | "pendingConnectionNotFound" + | "introNotFound" + | "uniqueID" + | "largeMsg" + | "internalError" + | "dBException" + | "dBBusyError" + | "badChatItem" + | "chatItemNotFound" + | "chatItemNotFoundByText" + | "chatItemSharedMsgIdNotFound" + | "chatItemNotFoundByFileId" + | "chatItemNotFoundByContactId" + | "chatItemNotFoundByGroupId" + | "profileNotFound" + | "duplicateGroupLink" + | "groupLinkNotFound" + | "hostMemberIdNotFound" + | "contactNotFoundByFileId" + | "noGroupSndStatus" + | "duplicateGroupMessage" + | "remoteHostNotFound" + | "remoteHostUnknown" + | "remoteHostDuplicateCA" + | "remoteCtrlNotFound" + | "remoteCtrlDuplicateCA" + | "prohibitedDeleteUser" + | "operatorNotFound" + | "usageConditionsNotFound" + | "invalidQuote" + | "invalidMention" + + interface Interface { + type: Tag + } + + export interface DuplicateName extends Interface { + type: "duplicateName" + } + + export interface UserNotFound extends Interface { + type: "userNotFound" + userId: number // int64 + } + + export interface UserNotFoundByName extends Interface { + type: "userNotFoundByName" + contactName: string + } + + export interface UserNotFoundByContactId extends Interface { + type: "userNotFoundByContactId" + contactId: number // int64 + } + + export interface UserNotFoundByGroupId extends Interface { + type: "userNotFoundByGroupId" + groupId: number // int64 + } + + export interface UserNotFoundByFileId extends Interface { + type: "userNotFoundByFileId" + fileId: number // int64 + } + + export interface UserNotFoundByContactRequestId extends Interface { + type: "userNotFoundByContactRequestId" + contactRequestId: number // int64 + } + + export interface ContactNotFound extends Interface { + type: "contactNotFound" + contactId: number // int64 + } + + export interface ContactNotFoundByName extends Interface { + type: "contactNotFoundByName" + contactName: string + } + + export interface ContactNotFoundByMemberId extends Interface { + type: "contactNotFoundByMemberId" + groupMemberId: number // int64 + } + + export interface ContactNotReady extends Interface { + type: "contactNotReady" + contactName: string + } + + export interface DuplicateContactLink extends Interface { + type: "duplicateContactLink" + } + + export interface UserContactLinkNotFound extends Interface { + type: "userContactLinkNotFound" + } + + export interface ContactRequestNotFound extends Interface { + type: "contactRequestNotFound" + contactRequestId: number // int64 + } + + export interface ContactRequestNotFoundByName extends Interface { + type: "contactRequestNotFoundByName" + contactName: string + } + + export interface InvalidContactRequestEntity extends Interface { + type: "invalidContactRequestEntity" + contactRequestId: number // int64 + } + + export interface InvalidBusinessChatContactRequest extends Interface { + type: "invalidBusinessChatContactRequest" + } + + export interface GroupNotFound extends Interface { + type: "groupNotFound" + groupId: number // int64 + } + + export interface GroupNotFoundByName extends Interface { + type: "groupNotFoundByName" + groupName: string + } + + export interface GroupMemberNameNotFound extends Interface { + type: "groupMemberNameNotFound" + groupId: number // int64 + groupMemberName: string + } + + export interface GroupMemberNotFound extends Interface { + type: "groupMemberNotFound" + groupMemberId: number // int64 + } + + export interface GroupHostMemberNotFound extends Interface { + type: "groupHostMemberNotFound" + groupId: number // int64 + } + + export interface GroupMemberNotFoundByMemberId extends Interface { + type: "groupMemberNotFoundByMemberId" + memberId: string + } + + export interface MemberContactGroupMemberNotFound extends Interface { + type: "memberContactGroupMemberNotFound" + contactId: number // int64 + } + + export interface GroupWithoutUser extends Interface { + type: "groupWithoutUser" + } + + export interface DuplicateGroupMember extends Interface { + type: "duplicateGroupMember" + } + + export interface GroupAlreadyJoined extends Interface { + type: "groupAlreadyJoined" + } + + export interface GroupInvitationNotFound extends Interface { + type: "groupInvitationNotFound" + } + + export interface NoteFolderAlreadyExists extends Interface { + type: "noteFolderAlreadyExists" + noteFolderId: number // int64 + } + + export interface NoteFolderNotFound extends Interface { + type: "noteFolderNotFound" + noteFolderId: number // int64 + } + + export interface UserNoteFolderNotFound extends Interface { + type: "userNoteFolderNotFound" + } + + export interface SndFileNotFound extends Interface { + type: "sndFileNotFound" + fileId: number // int64 + } + + export interface SndFileInvalid extends Interface { + type: "sndFileInvalid" + fileId: number // int64 + } + + export interface RcvFileNotFound extends Interface { + type: "rcvFileNotFound" + fileId: number // int64 + } + + export interface RcvFileDescrNotFound extends Interface { + type: "rcvFileDescrNotFound" + fileId: number // int64 + } + + export interface FileNotFound extends Interface { + type: "fileNotFound" + fileId: number // int64 + } + + export interface RcvFileInvalid extends Interface { + type: "rcvFileInvalid" + fileId: number // int64 + } + + export interface RcvFileInvalidDescrPart extends Interface { + type: "rcvFileInvalidDescrPart" + } + + export interface LocalFileNoTransfer extends Interface { + type: "localFileNoTransfer" + fileId: number // int64 + } + + export interface SharedMsgIdNotFoundByFileId extends Interface { + type: "sharedMsgIdNotFoundByFileId" + fileId: number // int64 + } + + export interface FileIdNotFoundBySharedMsgId extends Interface { + type: "fileIdNotFoundBySharedMsgId" + sharedMsgId: string + } + + export interface SndFileNotFoundXFTP extends Interface { + type: "sndFileNotFoundXFTP" + agentSndFileId: string + } + + export interface RcvFileNotFoundXFTP extends Interface { + type: "rcvFileNotFoundXFTP" + agentRcvFileId: string + } + + export interface ConnectionNotFound extends Interface { + type: "connectionNotFound" + agentConnId: string + } + + export interface ConnectionNotFoundById extends Interface { + type: "connectionNotFoundById" + connId: number // int64 + } + + export interface ConnectionNotFoundByMemberId extends Interface { + type: "connectionNotFoundByMemberId" + groupMemberId: number // int64 + } + + export interface PendingConnectionNotFound extends Interface { + type: "pendingConnectionNotFound" + connId: number // int64 + } + + export interface IntroNotFound extends Interface { + type: "introNotFound" + } + + export interface UniqueID extends Interface { + type: "uniqueID" + } + + export interface LargeMsg extends Interface { + type: "largeMsg" + } + + export interface InternalError extends Interface { + type: "internalError" + message: string + } + + export interface DBException extends Interface { + type: "dBException" + message: string + } + + export interface DBBusyError extends Interface { + type: "dBBusyError" + message: string + } + + export interface BadChatItem extends Interface { + type: "badChatItem" + itemId: number // int64 + itemTs?: string // ISO-8601 timestamp + } + + export interface ChatItemNotFound extends Interface { + type: "chatItemNotFound" + itemId: number // int64 + } + + export interface ChatItemNotFoundByText extends Interface { + type: "chatItemNotFoundByText" + text: string + } + + export interface ChatItemSharedMsgIdNotFound extends Interface { + type: "chatItemSharedMsgIdNotFound" + sharedMsgId: string + } + + export interface ChatItemNotFoundByFileId extends Interface { + type: "chatItemNotFoundByFileId" + fileId: number // int64 + } + + export interface ChatItemNotFoundByContactId extends Interface { + type: "chatItemNotFoundByContactId" + contactId: number // int64 + } + + export interface ChatItemNotFoundByGroupId extends Interface { + type: "chatItemNotFoundByGroupId" + groupId: number // int64 + } + + export interface ProfileNotFound extends Interface { + type: "profileNotFound" + profileId: number // int64 + } + + export interface DuplicateGroupLink extends Interface { + type: "duplicateGroupLink" + groupInfo: GroupInfo + } + + export interface GroupLinkNotFound extends Interface { + type: "groupLinkNotFound" + groupInfo: GroupInfo + } + + export interface HostMemberIdNotFound extends Interface { + type: "hostMemberIdNotFound" + groupId: number // int64 + } + + export interface ContactNotFoundByFileId extends Interface { + type: "contactNotFoundByFileId" + fileId: number // int64 + } + + export interface NoGroupSndStatus extends Interface { + type: "noGroupSndStatus" + itemId: number // int64 + groupMemberId: number // int64 + } + + export interface DuplicateGroupMessage extends Interface { + type: "duplicateGroupMessage" + groupId: number // int64 + sharedMsgId: string + authorGroupMemberId?: number // int64 + forwardedByGroupMemberId?: number // int64 + } + + export interface RemoteHostNotFound extends Interface { + type: "remoteHostNotFound" + remoteHostId: number // int64 + } + + export interface RemoteHostUnknown extends Interface { + type: "remoteHostUnknown" + } + + export interface RemoteHostDuplicateCA extends Interface { + type: "remoteHostDuplicateCA" + } + + export interface RemoteCtrlNotFound extends Interface { + type: "remoteCtrlNotFound" + remoteCtrlId: number // int64 + } + + export interface RemoteCtrlDuplicateCA extends Interface { + type: "remoteCtrlDuplicateCA" + } + + export interface ProhibitedDeleteUser extends Interface { + type: "prohibitedDeleteUser" + userId: number // int64 + contactId: number // int64 + } + + export interface OperatorNotFound extends Interface { + type: "operatorNotFound" + serverOperatorId: number // int64 + } + + export interface UsageConditionsNotFound extends Interface { + type: "usageConditionsNotFound" + } + + export interface InvalidQuote extends Interface { + type: "invalidQuote" + } + + export interface InvalidMention extends Interface { + type: "invalidMention" + } +} + +export enum SwitchPhase { + Started = "started", + Confirmed = "confirmed", + Secured = "secured", + Completed = "completed", +} + +export interface TimedMessagesGroupPreference { + enable: GroupFeatureEnabled + ttl?: number // int +} + +export interface TimedMessagesPreference { + allow: FeatureAllowed + ttl?: number // int +} + +export type TransportError = + | TransportError.BadBlock + | TransportError.Version + | TransportError.LargeMsg + | TransportError.BadSession + | TransportError.NoServerAuth + | TransportError.Handshake + +export namespace TransportError { + export type Tag = "badBlock" | "version" | "largeMsg" | "badSession" | "noServerAuth" | "handshake" + + interface Interface { + type: Tag + } + + export interface BadBlock extends Interface { + type: "badBlock" + } + + export interface Version extends Interface { + type: "version" + } + + export interface LargeMsg extends Interface { + type: "largeMsg" + } + + export interface BadSession extends Interface { + type: "badSession" + } + + export interface NoServerAuth extends Interface { + type: "noServerAuth" + } + + export interface Handshake extends Interface { + type: "handshake" + handshakeErr: HandshakeError + } +} + +export enum UIColorMode { + Light = "light", + Dark = "dark", +} + +export interface UIColors { + accent?: string + accentVariant?: string + secondary?: string + secondaryVariant?: string + background?: string + menus?: string + title?: string + accentVariant2?: string + sentMessage?: string + sentReply?: string + receivedMessage?: string + receivedReply?: string +} + +export interface UIThemeEntityOverride { + mode: UIColorMode + wallpaper?: ChatWallpaper + colors: UIColors +} + +export interface UIThemeEntityOverrides { + light?: UIThemeEntityOverride + dark?: UIThemeEntityOverride +} + +export interface UpdatedMessage { + msgContent: MsgContent + mentions: {[key: string]: number} // string : int64 +} + +export interface User { + userId: number // int64 + agentUserId: number // int64 + userContactId: number // int64 + localDisplayName: string + profile: LocalProfile + fullPreferences: FullPreferences + activeUser: boolean + activeOrder: number // int64 + viewPwdHash?: UserPwdHash + showNtfs: boolean + sendRcptsContacts: boolean + sendRcptsSmallGroups: boolean + autoAcceptMemberContacts: boolean + userMemberProfileUpdatedAt?: string // ISO-8601 timestamp + uiThemes?: UIThemeEntityOverrides +} + +export interface UserContact { + userContactLinkId: number // int64 + connReqContact: string + groupId?: number // int64 +} + +export interface UserContactLink { + userContactLinkId: number // int64 + connLinkContact: CreatedConnLink + shortLinkDataSet: boolean + shortLinkLargeDataSet: boolean + addressSettings: AddressSettings +} + +export interface UserContactRequest { + contactRequestId: number // int64 + agentInvitationId: string + contactId_?: number // int64 + businessGroupId_?: number // int64 + userContactLinkId_?: number // int64 + cReqChatVRange: VersionRange + localDisplayName: string + profileId: number // int64 + profile: Profile + createdAt: string // ISO-8601 timestamp + updatedAt: string // ISO-8601 timestamp + xContactId?: string + pqSupport: boolean + welcomeSharedMsgId?: string + requestSharedMsgId?: string +} + +export interface UserInfo { + user: User + unreadCount: number // int +} + +export interface UserProfileUpdateSummary { + updateSuccesses: number // int + updateFailures: number // int + changedContacts: Contact[] +} + +export interface UserPwdHash { + hash: string + salt: string +} + +export interface VersionRange { + minVersion: number // int + maxVersion: number // int +} + +export type XFTPErrorType = + | XFTPErrorType.BLOCK + | XFTPErrorType.SESSION + | XFTPErrorType.HANDSHAKE + | XFTPErrorType.CMD + | XFTPErrorType.AUTH + | XFTPErrorType.BLOCKED + | XFTPErrorType.SIZE + | XFTPErrorType.QUOTA + | XFTPErrorType.DIGEST + | XFTPErrorType.CRYPTO + | XFTPErrorType.NO_FILE + | XFTPErrorType.HAS_FILE + | XFTPErrorType.FILE_IO + | XFTPErrorType.TIMEOUT + | XFTPErrorType.INTERNAL + | XFTPErrorType.DUPLICATE_ + +export namespace XFTPErrorType { + export type Tag = + | "BLOCK" + | "SESSION" + | "HANDSHAKE" + | "CMD" + | "AUTH" + | "BLOCKED" + | "SIZE" + | "QUOTA" + | "DIGEST" + | "CRYPTO" + | "NO_FILE" + | "HAS_FILE" + | "FILE_IO" + | "TIMEOUT" + | "INTERNAL" + | "DUPLICATE_" + + interface Interface { + type: Tag + } + + export interface BLOCK extends Interface { + type: "BLOCK" + } + + export interface SESSION extends Interface { + type: "SESSION" + } + + export interface HANDSHAKE extends Interface { + type: "HANDSHAKE" + } + + export interface CMD extends Interface { + type: "CMD" + cmdErr: CommandError + } + + export interface AUTH extends Interface { + type: "AUTH" + } + + export interface BLOCKED extends Interface { + type: "BLOCKED" + blockInfo: BlockingInfo + } + + export interface SIZE extends Interface { + type: "SIZE" + } + + export interface QUOTA extends Interface { + type: "QUOTA" + } + + export interface DIGEST extends Interface { + type: "DIGEST" + } + + export interface CRYPTO extends Interface { + type: "CRYPTO" + } + + export interface NO_FILE extends Interface { + type: "NO_FILE" + } + + export interface HAS_FILE extends Interface { + type: "HAS_FILE" + } + + export interface FILE_IO extends Interface { + type: "FILE_IO" + } + + export interface TIMEOUT extends Interface { + type: "TIMEOUT" + } + + export interface INTERNAL extends Interface { + type: "INTERNAL" + } + + export interface DUPLICATE_ extends Interface { + type: "DUPLICATE_" + } +} + +export interface XFTPRcvFile { + rcvFileDescription: RcvFileDescr + agentRcvFileId?: string + agentRcvFileDeleted: boolean + userApprovedRelays: boolean +} + +export interface XFTPSndFile { + agentSndFileId: string + privateSndFileDescr?: string + agentSndFileDeleted: boolean + cryptoArgs?: CryptoFileArgs +} diff --git a/packages/simplex-chat-client/types/typescript/tsconfig.json b/packages/simplex-chat-client/types/typescript/tsconfig.json new file mode 100644 index 0000000000..d39db52454 --- /dev/null +++ b/packages/simplex-chat-client/types/typescript/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["src"], + "compilerOptions": { + "declaration": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2018", "DOM"], + "module": "CommonJS", + "moduleResolution": "Node", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noEmitOnError": true, + "outDir": "dist", + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "target": "ES2018" + } + } diff --git a/packages/simplex-chat-client/typescript/README.md b/packages/simplex-chat-client/typescript/README.md index 4940eb0f7c..c1756dc82c 100644 --- a/packages/simplex-chat-client/typescript/README.md +++ b/packages/simplex-chat-client/typescript/README.md @@ -38,6 +38,8 @@ See the example of a simple chat bot in [squaring-bot.js](./examples/squaring-bo Please refer to the available client API in [client.ts](./src/client.ts). +This library uses [@simplex-chat/types](https://www.npmjs.com/package/@simplex-chat/types) package with auto-generated [bot API types](https://github.com/simplex-chat/simplex-chat/tree/stable/bots) + ## License [AGPL v3](./LICENSE) diff --git a/packages/simplex-chat-client/typescript/package.json b/packages/simplex-chat-client/typescript/package.json index bb2fdda702..b721dbcce2 100644 --- a/packages/simplex-chat-client/typescript/package.json +++ b/packages/simplex-chat-client/typescript/package.json @@ -1,12 +1,16 @@ { "name": "simplex-chat", - "version": "0.2.1", + "version": "0.3.0", "description": "SimpleX Chat client", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist" ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, "scripts": { "test": "npm run prettier:check && npm run eslint && jest --coverage", "build": "npm run prettier:write && npm run eslint && tsc && ./copy && npm run bundle", @@ -17,13 +21,17 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/simplex-chat/simplex-chat.git" + "url": "git+https://github.com/simplex-chat/simplex-chat.git", + "directory": "/packages/simplex-chat-client/typescript" }, "keywords": [ + "simplex", "messenger", "chat", "privacy", - "security" + "security", + "bots", + "websockets" ], "author": "SimpleX Chat", "license": "AGPL-3.0", @@ -35,6 +43,7 @@ "isomorphic-ws": "^4.0.1" }, "devDependencies": { + "@simplex-chat/types": "^0.1.0", "@types/jest": "^27.5.1", "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.23.0", diff --git a/packages/simplex-chat-client/typescript/src/client.ts b/packages/simplex-chat-client/typescript/src/client.ts index f908e6a7e3..12fa150fc4 100644 --- a/packages/simplex-chat-client/typescript/src/client.ts +++ b/packages/simplex-chat-client/typescript/src/client.ts @@ -1,9 +1,6 @@ import {ABQueue} from "./queue" -import {ChatTransport, ChatServer, ChatSrvRequest, ChatSrvResponse, ChatResponseError, localServer, noop} from "./transport" -import {ChatCommand, ChatType, Profile} from "./command" -import {ChatResponse, ChatInfo} from "./response" -import * as CC from "./command" -import * as CR from "./response" +import {ChatTransport, ChatServer, ChatSrvRequest, ChatSrvResponse, ChatResponseError, localServer, noop, ChatSrvEvent} from "./transport" +import {CC, ChatResponse, CR, ChatEvent, T} from "@simplex-chat/types" export interface ChatClientConfig { readonly qSize: number @@ -12,7 +9,7 @@ export interface ChatClientConfig { export interface Request { readonly resolve: (resp: ChatResponse) => void - readonly reject: (err?: ChatResponseError | CR.CRChatCmdError) => void + readonly reject: (err?: ChatResponseError | CR.ChatCmdError) => void } export class ChatCommandError extends Error { @@ -39,44 +36,42 @@ export class ChatClient { private constructor( readonly server: ChatServer | string, readonly config: ChatClientConfig, - readonly msgQ: ABQueue, + readonly msgQ: ABQueue, readonly client: Promise, private readonly transport: ChatTransport ) {} static async create(server: ChatServer | string = localServer, cfg: ChatClientConfig = ChatClient.defaultConfig): Promise { const transport = await ChatTransport.connect(server, cfg.tcpTimeout, cfg.qSize) - const msgQ = new ABQueue(cfg.qSize) + const msgQ = new ABQueue(cfg.qSize) const client = runClient().then(noop, noop) const c = new ChatClient(server, cfg, msgQ, client, transport) return c async function runClient(): Promise { for await (const t of transport) { - const apiResp = (t instanceof Promise ? await t : t) as ChatSrvResponse | ChatResponseError + const apiResp = (t instanceof Promise ? await t : t) as ChatSrvResponse | ChatSrvEvent | ChatResponseError if (apiResp instanceof ChatResponseError) { console.log("chat response error: ", apiResp) - } else { + } else if ("corrId" in apiResp) { const {corrId, resp} = apiResp - if (corrId) { - const req = c.sentCommands.get(corrId) - if (req) { - c.sentCommands.delete(corrId) - req.resolve(resp) - } else { - // TODO send error to errQ? - console.log("no command sent for chat response: ", apiResp) - } + const req = c.sentCommands.get(corrId) + if (req) { + c.sentCommands.delete(corrId) + req.resolve(resp) } else { - await msgQ.enqueue(resp) + // TODO send error to errQ? + console.log("no command sent for chat response: ", apiResp) } + } else { + await msgQ.enqueue(apiResp.resp) } } c._connected = false } } - sendChatCmdStr(cmd: string): Promise { + sendChatCmd(cmd: string): Promise { const corrId = `${++this.clientCorrId}` const t: ChatSrvRequest = {corrId, cmd} const p = new Promise((resolve, reject) => this.sentCommands.set(corrId, {resolve, reject})) @@ -84,17 +79,13 @@ export class ChatClient { return p } - sendChatCommand(command: ChatCommand): Promise { - return this.sendChatCmdStr(CC.cmdString(command)) - } - async disconnect(): Promise { await this.transport.close() await this.client } - async apiGetActiveUser(): Promise { - const r = await this.sendChatCommand({type: "showActiveUser"}) + async apiGetActiveUser(): Promise { + const r = await this.sendChatCmd(CC.ShowActiveUser.cmdString({})) switch (r.type) { case "activeUser": return r.user @@ -106,90 +97,66 @@ export class ChatClient { } } - async apiCreateActiveUser(profile?: Profile, sameServers = true, pastTimestamp = false): Promise { - const r = await this.sendChatCommand({type: "createActiveUser", profile, sameServers, pastTimestamp}) + async apiCreateActiveUser(profile?: T.Profile): Promise { + const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false}})) if (r.type === "activeUser") return r.user throw new ChatCommandError("unexpected response", r) } - async apiStartChat(): Promise { - const r = await this.sendChatCommand({type: "startChat"}) - if (r.type !== "chatStarted" && r.type !== "chatRunning") { - throw new ChatCommandError("error starting chat", r) - } - } - - async apiStopChat(): Promise { - const r = await this.sendChatCommand({type: "apiStopChat"}) - if (r.type !== "chatStopped") { - throw new ChatCommandError("error stopping chat", r) - } - } - - apiSetIncognito(incognito: boolean): Promise { - return this.okChatCommand({type: "setIncognito", incognito}) - } - - async enableAddressAutoAccept(acceptIncognito = false, autoReply?: CC.MsgContent): Promise { - const r = await this.sendChatCommand({type: "addressAutoAccept", autoAccept: {acceptIncognito, autoReply}}) + async enableAddressAutoAccept(userId: number, autoReply?: T.MsgContent, businessAddress = false): Promise { + const r = await this.sendChatCmd( + CC.APISetAddressSettings.cmdString({userId, settings: {businessAddress, autoAccept: {acceptIncognito: false}, autoReply}}) + ) if (r.type !== "userContactLinkUpdated") { throw new ChatCommandError("error changing user contact address mode", r) } } - async disableAddressAutoAccept(): Promise { - const r = await this.sendChatCommand({type: "addressAutoAccept"}) + async disableAddressAutoAccept(userId: number, businessAddress = false): Promise { + const r = await this.sendChatCmd(CC.APISetAddressSettings.cmdString({userId, settings: {businessAddress}})) if (r.type !== "userContactLinkUpdated") { throw new ChatCommandError("error changing user contact address mode", r) } } - async apiGetChats(userId: number): Promise { - const r = await this.sendChatCommand({type: "apiGetChats", userId}) - if (r.type === "apiChats") return r.chats - throw new ChatCommandError("error loading chats", r) - } - - async apiGetChat( - chatType: ChatType, - chatId: number, - pagination: CC.ChatPagination = {count: 100}, - search: string | undefined = undefined - ): Promise { - const r = await this.sendChatCommand({type: "apiGetChat", chatType, chatId, pagination, search}) - if (r.type === "apiChat") return r.chat - throw new ChatCommandError("error loading chat", r) - } - - async apiSendMessages(chatType: ChatType, chatId: number, messages: CC.ComposedMessage[]): Promise { - const r = await this.sendChatCommand({type: "apiSendMessage", chatType, chatId, messages}) + async apiSendMessages(chatType: T.ChatType, chatId: number, messages: T.ComposedMessage[]): Promise { + const r = await this.sendChatCmd( + CC.APISendMessages.cmdString({sendRef: {chatType, chatId}, composedMessages: messages, liveMessage: false}) + ) if (r.type === "newChatItems") return r.chatItems throw new ChatCommandError("unexpected response", r) } - async apiSendTextMessage(chatType: ChatType, chatId: number, text: string): Promise { - return this.apiSendMessages(chatType, chatId, [{msgContent: {type: "text", text}}]) + async apiSendTextMessage(chatType: T.ChatType, chatId: number, text: string): Promise { + return this.apiSendMessages(chatType, chatId, [{msgContent: {type: "text", text}, mentions: {}}]) } - async apiUpdateChatItem(chatType: ChatType, chatId: number, chatItemId: CC.ChatItemId, msgContent: CC.MsgContent): Promise { - const r = await this.sendChatCommand({type: "apiUpdateChatItem", chatType, chatId, chatItemId, msgContent}) + async apiUpdateChatItem(chatType: T.ChatType, chatId: number, chatItemId: number, msgContent: T.MsgContent): Promise { + const r = await this.sendChatCmd( + CC.APIUpdateChatItem.cmdString({ + chatRef: {chatType, chatId}, + chatItemId, + liveMessage: false, + updatedMessage: {msgContent, mentions: {}}, + }) + ) if (r.type === "chatItemUpdated") return r.chatItem.chatItem throw new ChatCommandError("error updating chat item", r) } - async apiDeleteChatItem( - chatType: ChatType, + async apiDeleteChatItems( + chatType: T.ChatType, chatId: number, - chatItemId: number, - deleteMode: CC.DeleteMode - ): Promise { - const r = await this.sendChatCommand({type: "apiDeleteChatItem", chatType, chatId, chatItemId, deleteMode}) - if (r.type === "chatItemDeleted") return r.toChatItem?.chatItem + chatItemIds: number[], + deleteMode: T.CIDeleteMode + ): Promise { + const r = await this.sendChatCmd(CC.APIDeleteChatItem.cmdString({chatRef: {chatType, chatId}, chatItemIds, deleteMode})) + if (r.type === "chatItemsDeleted") return r.chatItemDeletions throw new ChatCommandError("error deleting chat item", r) } - async apiCreateLink(): Promise { - const r = await this.sendChatCommand({type: "addContact"}) + async apiCreateLink(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIAddContact.cmdString({userId, incognito: false})) if (r.type === "invitation") { const link = r.connLinkInvitation return link.connShortLink || link.connFullLink @@ -197,8 +164,8 @@ export class ChatClient { throw new ChatCommandError("error creating link", r) } - async apiConnect(connReq: string): Promise { - const r = await this.sendChatCommand({type: "connect", connReq}) + async apiConnectActiveUser(connLink: string): Promise { + const r = await this.sendChatCmd(CC.Connect.cmdString({incognito: false, connLink_: connLink})) switch (r.type) { case "sentConfirmation": return ConnReqType.Invitation @@ -209,30 +176,21 @@ export class ChatClient { } } - async apiDeleteChat(chatType: ChatType, chatId: number): Promise { - const r = await this.sendChatCommand({type: "apiDeleteChat", chatType, chatId}) + async apiDeleteChat(chatType: T.ChatType, chatId: number, deleteMode: T.ChatDeleteMode = {type: "full", notify: true}): Promise { + const r = await this.sendChatCmd(CC.APIDeleteChat.cmdString({chatRef: {chatType, chatId}, chatDeleteMode: deleteMode})) switch (chatType) { - case ChatType.Direct: + case T.ChatType.Direct: if (r.type === "contactDeleted") return break - case ChatType.Group: + case T.ChatType.Group: if (r.type === "groupDeletedUser") return break - case ChatType.ContactRequest: - if (r.type === "contactConnectionDeleted") return - break } throw new ChatCommandError("error deleting chat", r) } - async apiClearChat(chatType: ChatType, chatId: number): Promise { - const r = await this.sendChatCommand({type: "apiClearChat", chatType, chatId}) - if (r.type === "chatCleared") return r.chatInfo - throw new ChatCommandError("error clearing chat", r) - } - - async apiUpdateProfile(userId: number, profile: CC.Profile): Promise { - const r = await this.sendChatCommand({type: "apiUpdateProfile", userId, profile}) + async apiUpdateProfile(userId: number, profile: T.Profile): Promise { + const r = await this.sendChatCmd(CC.APIUpdateProfile.cmdString({userId, profile})) switch (r.type) { case "userProfileNoChange": return undefined @@ -243,14 +201,8 @@ export class ChatClient { } } - async apiSetContactAlias(contactId: number, localAlias: string): Promise { - const r = await this.sendChatCommand({type: "apiSetContactAlias", contactId, localAlias}) - if (r.type === "contactAliasUpdated") return r.toContact - throw new ChatCommandError("error updating contact alias", r) - } - - async apiCreateUserAddress(): Promise { - const r = await this.sendChatCommand({type: "createMyAddress"}) + async apiCreateUserAddress(userId: number): Promise { + const r = await this.sendChatCmd(CC.APICreateMyAddress.cmdString({userId})) if (r.type === "userContactLinkCreated") { const link = r.connLinkContact return link.connShortLink || link.connFullLink @@ -258,14 +210,14 @@ export class ChatClient { throw new ChatCommandError("error creating user address", r) } - async apiDeleteUserAddress(): Promise { - const r = await this.sendChatCommand({type: "deleteMyAddress"}) + async apiDeleteUserAddress(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIDeleteMyAddress.cmdString({userId})) if (r.type === "userContactLinkDeleted") return throw new ChatCommandError("error deleting user address", r) } - async apiGetUserAddress(): Promise { - const r = await this.sendChatCommand({type: "showMyAddress"}) + async apiGetUserAddress(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIShowMyAddress.cmdString({userId})) switch (r.type) { case "userContactLink": { const link = r.contactLink.connLinkContact @@ -279,87 +231,66 @@ export class ChatClient { } } - async apiAcceptContactRequest(contactReqId: number): Promise { - const r = await this.sendChatCommand({type: "apiAcceptContact", contactReqId}) + async apiAcceptContactRequest(contactReqId: number): Promise { + const r = await this.sendChatCmd(CC.APIAcceptContact.cmdString({contactReqId})) if (r.type === "acceptingContactRequest") return r.contact throw new ChatCommandError("error accepting contact request", r) } async apiRejectContactRequest(contactReqId: number): Promise { - const r = await this.sendChatCommand({type: "apiRejectContact", contactReqId}) + const r = await this.sendChatCmd(CC.APIRejectContact.cmdString({contactReqId})) if (r.type === "contactRequestRejected") return throw new ChatCommandError("error rejecting contact request", r) } - apiChatRead(chatType: ChatType, chatId: number, itemRange?: CC.ItemRange): Promise { - return this.okChatCommand({type: "apiChatRead", chatType, chatId, itemRange}) - } - - async apiContactInfo(contactId: number): Promise<[CR.ConnectionStats?, Profile?]> { - const r = await this.sendChatCommand({type: "apiContactInfo", contactId}) - if (r.type === "contactInfo") return [r.connectionStats, r.customUserProfile] - throw new ChatCommandError("error getting contact info", r) - } - - async apiGroupMemberInfo(groupId: number, memberId: number): Promise { - const r = await this.sendChatCommand({type: "apiGroupMemberInfo", groupId, memberId}) - if (r.type === "groupMemberInfo") return r.connectionStats_ - throw new ChatCommandError("error getting group info", r) - } - - async apiReceiveFile(fileId: number): Promise { - const r = await this.sendChatCommand({type: "receiveFile", fileId}) + async apiReceiveFile(fileId: number): Promise { + const r = await this.sendChatCmd(CC.ReceiveFile.cmdString({fileId, userApprovedRelays: true})) if (r.type === "rcvFileAccepted") return r.chatItem throw new ChatCommandError("error receiving file", r) } - async apiNewGroup(groupProfile: CR.GroupProfile): Promise { - const r = await this.sendChatCommand({type: "newGroup", groupProfile}) + async apiNewGroup(userId: number, groupProfile: T.GroupProfile): Promise { + const r = await this.sendChatCmd(CC.APINewGroup.cmdString({userId, groupProfile, incognito: false})) if (r.type === "groupCreated") return r.groupInfo throw new ChatCommandError("error creating group", r) } - async apiAddMember(groupId: number, contactId: number, memberRole: CC.GroupMemberRole): Promise { - const r = await this.sendChatCommand({type: "apiAddMember", groupId, contactId, memberRole}) + async apiAddMember(groupId: number, contactId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIAddMember.cmdString({groupId, contactId, memberRole})) if (r.type === "sentGroupInvitation") return r.member throw new ChatCommandError("error adding member", r) } - async apiJoinGroup(groupId: number): Promise { - const r = await this.sendChatCommand({type: "apiJoinGroup", groupId}) + async apiJoinGroup(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIJoinGroup.cmdString({groupId})) if (r.type === "userAcceptedGroupSent") return r.groupInfo throw new ChatCommandError("error joining group", r) } - async apiRemoveMember(groupId: number, memberId: number): Promise { - const r = await this.sendChatCommand({type: "apiRemoveMember", groupId, memberId}) - if (r.type === "userDeletedMember") return r.member + async apiRemoveMembers(groupId: number, memberIds: number[], withMessages = false): Promise { + const r = await this.sendChatCmd(CC.APIRemoveMembers.cmdString({groupId, groupMemberIds: memberIds, withMessages})) + if (r.type === "userDeletedMembers") return r.members throw new ChatCommandError("error removing member", r) } - async apiLeaveGroup(groupId: number): Promise { - const r = await this.sendChatCommand({type: "apiLeaveGroup", groupId}) + async apiLeaveGroup(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APILeaveGroup.cmdString({groupId})) if (r.type === "leftMemberUser") return r.groupInfo throw new ChatCommandError("error leaving group", r) } - async apiListMembers(groupId: number): Promise { - const r = await this.sendChatCommand({type: "apiListMembers", groupId}) + async apiListMembers(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIListMembers.cmdString({groupId})) if (r.type === "groupMembers") return r.group.members throw new ChatCommandError("error getting group members", r) } - async apiUpdateGroup(groupId: number, groupProfile: CR.GroupProfile): Promise { - const r = await this.sendChatCommand({type: "apiUpdateGroupProfile", groupId, groupProfile}) + async apiUpdateGroup(groupId: number, groupProfile: T.GroupProfile): Promise { + const r = await this.sendChatCmd(CC.APIUpdateGroupProfile.cmdString({groupId, groupProfile})) if (r.type === "groupUpdated") return r.toGroup throw new ChatCommandError("error updating group", r) } - private async okChatCommand(command: ChatCommand): Promise { - const r = await this.sendChatCommand(command) - if (r.type !== "cmdOk") throw new ChatCommandError(`${command.type} command error`, r) - } - get connected(): boolean { return this._connected } diff --git a/packages/simplex-chat-client/typescript/src/command.ts b/packages/simplex-chat-client/typescript/src/command.ts deleted file mode 100644 index 135dc41ba7..0000000000 --- a/packages/simplex-chat-client/typescript/src/command.ts +++ /dev/null @@ -1,822 +0,0 @@ -export type ChatCommand = - | ShowActiveUser - | CreateActiveUser - | ListUsers - | APISetActiveUser - | APIHideUser - | APIUnhideUser - | APIMuteUser - | APIUnmuteUser - | APIDeleteUser - | StartChat - | APIStopChat - | SetTempFolder - | SetFilesFolder - | SetIncognito - | APIExportArchive - | APIImportArchive - | APIDeleteStorage - | APIGetChats - | APIGetChat - | APISendMessage - | APIUpdateChatItem - | APIDeleteChatItem - | APIDeleteMemberChatItem - | APIChatRead - | APIDeleteChat - | APIClearChat - | APIAcceptContact - | APIRejectContact - | APIUpdateProfile - | APISetContactAlias - | NewGroup - | APIAddMember - | APIJoinGroup - | APIRemoveMember - | APILeaveGroup - | APIListMembers - | APIUpdateGroupProfile - | APICreateGroupLink - | APIGroupLinkMemberRole - | APIDeleteGroupLink - | APIGetGroupLink - | APIGetUserProtoServers - | APISetUserProtoServers - | APIContactInfo - | APIGroupMemberInfo - | APIGetContactCode - | APIGetGroupMemberCode - | APIVerifyContact - | APIVerifyGroupMember - | AddContact - | Connect - | ConnectSimplex - | CreateMyAddress - | DeleteMyAddress - | ShowMyAddress - | SetProfileAddress - | AddressAutoAccept - | APICreateMyAddress - | APIDeleteMyAddress - | APIShowMyAddress - | APISetProfileAddress - | APIAddressAutoAccept - | ReceiveFile - | CancelFile - | FileStatus - -// not included commands (they are not needed for Websocket clients, and can still be sent as strings): -// APIActivateChat -// APISuspendChat -// ResubscribeAllConnections -// APIGetChatItems - not implemented -// APISendCallInvitation -// APIRejectCall -// APISendCallOffer -// APISendCallAnswer -// APISendCallExtraInfo -// APIEndCall -// APIGetCallInvitations -// APICallStatus -// APIGetNtfToken -// APIRegisterToken -// APIVerifyToken -// APIDeleteToken -// APIGetNtfMessage -// APIMemberRole -- not implemented -// ListContacts -// ListGroups -// APISetChatItemTTL -// APIGetChatItemTTL -// APISetNetworkConfig -// APIGetNetworkConfig -// APISetChatSettings -// ShowMessages -// LastMessages -// SendMessageBroadcast - -type ChatCommandTag = - | "showActiveUser" - | "createActiveUser" - | "listUsers" - | "apiSetActiveUser" - | "setActiveUser" - | "apiHideUser" - | "apiUnhideUser" - | "apiMuteUser" - | "apiUnmuteUser" - | "apiDeleteUser" - | "startChat" - | "apiStopChat" - | "setTempFolder" - | "setFilesFolder" - | "setIncognito" - | "apiExportArchive" - | "apiImportArchive" - | "apiDeleteStorage" - | "apiGetChats" - | "apiGetChat" - | "apiSendMessage" - | "apiUpdateChatItem" - | "apiDeleteChatItem" - | "apiDeleteMemberChatItem" - | "apiChatRead" - | "apiDeleteChat" - | "apiClearChat" - | "apiAcceptContact" - | "apiRejectContact" - | "apiUpdateProfile" - | "apiSetContactAlias" - | "newGroup" - | "apiAddMember" - | "apiJoinGroup" - | "apiRemoveMember" - | "apiLeaveGroup" - | "apiListMembers" - | "apiUpdateGroupProfile" - | "apiCreateGroupLink" - | "apiGroupLinkMemberRole" - | "apiDeleteGroupLink" - | "apiGetGroupLink" - | "apiGetUserProtoServers" - | "apiSetUserProtoServers" - | "apiContactInfo" - | "apiGroupMemberInfo" - | "apiGetContactCode" - | "apiGetGroupMemberCode" - | "apiVerifyContact" - | "apiVerifyGroupMember" - | "addContact" - | "connect" - | "connectSimplex" - | "createMyAddress" - | "deleteMyAddress" - | "showMyAddress" - | "setProfileAddress" - | "addressAutoAccept" - | "apiCreateMyAddress" - | "apiDeleteMyAddress" - | "apiShowMyAddress" - | "apiSetProfileAddress" - | "apiAddressAutoAccept" - | "receiveFile" - | "cancelFile" - | "fileStatus" - -interface IChatCommand { - type: ChatCommandTag -} - -export interface ShowActiveUser extends IChatCommand { - type: "showActiveUser" -} - -export interface CreateActiveUser extends IChatCommand { - type: "createActiveUser" - profile?: Profile - sameServers: boolean - pastTimestamp: boolean -} - -export interface ListUsers extends IChatCommand { - type: "listUsers" -} - -export interface APISetActiveUser extends IChatCommand { - type: "apiSetActiveUser" - userId: number - viewPwd?: string -} - -export interface APIHideUser extends IChatCommand { - type: "apiHideUser" - userId: number - viewPwd: string -} - -export interface APIUnhideUser extends IChatCommand { - type: "apiUnhideUser" - userId: number - viewPwd: string -} - -export interface APIMuteUser extends IChatCommand { - type: "apiMuteUser" - userId: number -} - -export interface APIUnmuteUser extends IChatCommand { - type: "apiUnmuteUser" - userId: number -} - -export interface APIDeleteUser extends IChatCommand { - type: "apiDeleteUser" - userId: number - delSMPQueues: boolean - viewPwd?: string -} - -export interface StartChat extends IChatCommand { - type: "startChat" - subscribeConnections?: boolean - enableExpireChatItems?: boolean - startXFTPWorkers?: boolean -} - -export interface APIStopChat extends IChatCommand { - type: "apiStopChat" -} - -export interface SetTempFolder extends IChatCommand { - type: "setTempFolder" - tempFolder: string -} - -export interface SetFilesFolder extends IChatCommand { - type: "setFilesFolder" - filePath: string -} - -export interface SetIncognito extends IChatCommand { - type: "setIncognito" - incognito: boolean -} - -export interface APIExportArchive extends IChatCommand { - type: "apiExportArchive" - config: ArchiveConfig -} - -export interface APIImportArchive extends IChatCommand { - type: "apiImportArchive" - config: ArchiveConfig -} - -export interface APIDeleteStorage extends IChatCommand { - type: "apiDeleteStorage" -} - -export interface APIGetChats extends IChatCommand { - type: "apiGetChats" - userId: number - pendingConnections?: boolean -} - -export interface APIGetChat extends IChatCommand { - type: "apiGetChat" - chatType: ChatType - chatId: number - pagination: ChatPagination - search?: string -} - -export interface APISendMessage extends IChatCommand { - type: "apiSendMessage" - chatType: ChatType - chatId: number - messages: ComposedMessage[] -} - -export interface ComposedMessage { - filePath?: string - quotedItemId?: ChatItemId - msgContent: MsgContent -} - -export interface APIUpdateChatItem extends IChatCommand { - type: "apiUpdateChatItem" - chatType: ChatType - chatId: number - chatItemId: ChatItemId - msgContent: MsgContent -} - -export interface APIDeleteChatItem extends IChatCommand { - type: "apiDeleteChatItem" - chatType: ChatType - chatId: number - chatItemId: ChatItemId - deleteMode: DeleteMode -} - -export interface APIDeleteMemberChatItem extends IChatCommand { - type: "apiDeleteMemberChatItem" - groupId: number - groupMemberId: number - itemId: number -} - -export interface APIChatRead extends IChatCommand { - type: "apiChatRead" - chatType: ChatType - chatId: number - itemRange?: ItemRange -} - -export interface ItemRange { - fromItem: ChatItemId - toItem: ChatItemId -} - -export interface APIDeleteChat extends IChatCommand { - type: "apiDeleteChat" - chatType: ChatType - chatId: number -} - -export interface APIClearChat extends IChatCommand { - type: "apiClearChat" - chatType: ChatType - chatId: number -} - -export interface APIAcceptContact extends IChatCommand { - type: "apiAcceptContact" - contactReqId: number -} - -export interface APIRejectContact extends IChatCommand { - type: "apiRejectContact" - contactReqId: number -} - -export interface APIUpdateProfile extends IChatCommand { - type: "apiUpdateProfile" - userId: number - profile: Profile -} - -export interface APISetContactAlias extends IChatCommand { - type: "apiSetContactAlias" - contactId: number - localAlias: string -} - -export interface NewGroup extends IChatCommand { - type: "newGroup" - groupProfile: GroupProfile -} - -export interface APIAddMember extends IChatCommand { - type: "apiAddMember" - groupId: number - contactId: number - memberRole: GroupMemberRole -} - -export interface APIJoinGroup extends IChatCommand { - type: "apiJoinGroup" - groupId: number -} - -export interface APIRemoveMember extends IChatCommand { - type: "apiRemoveMember" - groupId: number - memberId: number -} - -export interface APILeaveGroup extends IChatCommand { - type: "apiLeaveGroup" - groupId: number -} - -export interface APIListMembers extends IChatCommand { - type: "apiListMembers" - groupId: number -} - -export interface APIUpdateGroupProfile extends IChatCommand { - type: "apiUpdateGroupProfile" - groupId: number - groupProfile: GroupProfile -} - -export interface APICreateGroupLink extends IChatCommand { - type: "apiCreateGroupLink" - groupId: number - memberRole: GroupMemberRole -} - -export interface APIGroupLinkMemberRole extends IChatCommand { - type: "apiGroupLinkMemberRole" - groupId: number - memberRole: GroupMemberRole -} - -export interface APIDeleteGroupLink extends IChatCommand { - type: "apiDeleteGroupLink" - groupId: number -} - -export interface APIGetGroupLink extends IChatCommand { - type: "apiGetGroupLink" - groupId: number -} - -export interface APIGetUserProtoServers extends IChatCommand { - type: "apiGetUserProtoServers" - userId: number - serverProtocol: ServerProtocol -} - -export interface APISetUserProtoServers extends IChatCommand { - type: "apiSetUserProtoServers" - userId: number - serverProtocol: ServerProtocol - servers: ServerCfg[] -} - -export interface ServerCfg { - server: string - preset: boolean - tested?: boolean - enabled: boolean -} - -export enum ServerProtocol { - SMP = "smp", - XFTP = "xftp", -} - -export interface APIContactInfo extends IChatCommand { - type: "apiContactInfo" - contactId: number -} - -export interface APIGroupMemberInfo extends IChatCommand { - type: "apiGroupMemberInfo" - groupId: number - memberId: number -} - -export interface APIGetContactCode extends IChatCommand { - type: "apiGetContactCode" - contactId: number -} - -export interface APIGetGroupMemberCode extends IChatCommand { - type: "apiGetGroupMemberCode" - groupId: number - groupMemberId: number -} - -export interface APIVerifyContact extends IChatCommand { - type: "apiVerifyContact" - contactId: number - connectionCode: string -} - -export interface APIVerifyGroupMember extends IChatCommand { - type: "apiVerifyGroupMember" - groupId: number - groupMemberId: number - connectionCode: string -} - -export interface AddContact extends IChatCommand { - type: "addContact" -} - -export interface Connect extends IChatCommand { - type: "connect" - connReq: string -} - -export interface ConnectSimplex extends IChatCommand { - type: "connectSimplex" -} - -export interface CreateMyAddress extends IChatCommand { - type: "createMyAddress" -} - -export interface DeleteMyAddress extends IChatCommand { - type: "deleteMyAddress" -} - -export interface ShowMyAddress extends IChatCommand { - type: "showMyAddress" -} - -export interface SetProfileAddress extends IChatCommand { - type: "setProfileAddress" - includeInProfile: boolean -} - -export interface AddressAutoAccept extends IChatCommand { - type: "addressAutoAccept" - autoAccept?: AutoAccept -} - -export interface APICreateMyAddress extends IChatCommand { - type: "apiCreateMyAddress" - userId: number -} - -export interface APIDeleteMyAddress extends IChatCommand { - type: "apiDeleteMyAddress" - userId: number -} - -export interface APIShowMyAddress extends IChatCommand { - type: "apiShowMyAddress" - userId: number -} - -export interface APISetProfileAddress extends IChatCommand { - type: "apiSetProfileAddress" - userId: number - includeInProfile: boolean -} - -export interface APIAddressAutoAccept extends IChatCommand { - type: "apiAddressAutoAccept" - userId: number - autoAccept?: AutoAccept -} - -export interface AutoAccept { - acceptIncognito: boolean - autoReply?: MsgContent -} - -export interface ReceiveFile extends IChatCommand { - type: "receiveFile" - fileId: number - filePath?: string -} - -export interface CancelFile extends IChatCommand { - type: "cancelFile" - fileId: number -} - -export interface FileStatus extends IChatCommand { - type: "fileStatus" - fileId: number -} - -interface NewUser { - profile?: Profile - sameServers: boolean - pastTimestamp: boolean -} - -export interface Profile { - displayName: string - fullName: string // can be empty string - image?: string - contactLink?: string - // preferences?: Preferences -} - -export interface LocalProfile { - profileId: number - displayName: string - fullName: string - image?: string - contactLink?: string - // preferences?: Preferences - localAlias: string -} - -export enum ChatType { - Direct = "@", - Group = "#", - ContactRequest = "<@", -} - -export type ChatPagination = - | {count: number} // count from the last item in case neither after nor before specified - | {count: number; after: ChatItemId} - | {count: number; before: ChatItemId} - -export type ChatItemId = number - -type MsgContentTag = "text" | "link" | "image" | "file" - -export type MsgContent = MCText | MCLink | MCImage | MCFile | MCUnknown - -interface MC { - type: MsgContentTag - text: string -} - -interface MCText extends MC { - type: "text" - text: string -} - -interface MCLink extends MC { - type: "link" - text: string - preview: LinkPreview -} - -interface MCImage extends MC { - type: "image" - image: string // image preview as base64 encoded data string -} - -interface MCFile extends MC { - type: "file" - text: string -} - -interface MCUnknown { - type: string - text: string -} - -interface LinkPreview { - uri: string - title: string - description: string - image: string -} - -export enum DeleteMode { - Broadcast = "broadcast", - Internal = "internal", -} - -interface ArchiveConfig { - archivePath: string - disableCompression?: boolean - parentTempDirectory?: string -} - -export enum GroupMemberRole { - GRMember = "member", - GRAdmin = "admin", - GROwner = "owner", -} - -interface GroupProfile { - displayName: string - fullName: string // can be empty string - image?: string -} - -export function cmdString(cmd: ChatCommand): string { - switch (cmd.type) { - case "showActiveUser": - return "/u" - case "createActiveUser": { - const user: NewUser = {profile: cmd.profile, sameServers: cmd.sameServers, pastTimestamp: cmd.pastTimestamp} - return `/_create user ${JSON.stringify(user)}` - } - case "listUsers": - return `/users` - case "apiSetActiveUser": - return `/_user ${cmd.userId}${maybeJSON(cmd.viewPwd)}` - case "apiHideUser": - return `/_hide user ${cmd.userId} ${JSON.stringify(cmd.viewPwd)}` - case "apiUnhideUser": - return `/_unhide user ${cmd.userId} ${JSON.stringify(cmd.viewPwd)}` - case "apiMuteUser": - return `/_mute user ${cmd.userId}` - case "apiUnmuteUser": - return `/_unmute user ${cmd.userId}` - case "apiDeleteUser": - return `/_delete user ${cmd.userId} del_smp=${onOff(cmd.delSMPQueues)}${maybeJSON(cmd.viewPwd)}` - case "startChat": - return `/_start subscribe=${cmd.subscribeConnections ? "on" : "off"} expire=${cmd.enableExpireChatItems ? "on" : "off"}` - case "apiStopChat": - return "/_stop" - case "setTempFolder": - return `/_temp_folder ${cmd.tempFolder}` - case "setFilesFolder": - return `/_files_folder ${cmd.filePath}` - case "setIncognito": - return `/incognito ${onOff(cmd.incognito)}` - case "apiExportArchive": - return `/_db export ${JSON.stringify(cmd.config)}` - case "apiImportArchive": - return `/_db import ${JSON.stringify(cmd.config)}` - case "apiDeleteStorage": - return "/_db delete" - case "apiGetChats": - return `/_get chats pcc=${onOff(cmd.pendingConnections)}` - case "apiGetChat": - return `/_get chat ${cmd.chatType}${cmd.chatId}${paginationStr(cmd.pagination)}` - case "apiSendMessage": - return `/_send ${cmd.chatType}${cmd.chatId} json ${JSON.stringify(cmd.messages)}` - case "apiUpdateChatItem": - return `/_update item ${cmd.chatType}${cmd.chatId} ${cmd.chatItemId} json ${JSON.stringify(cmd.msgContent)}` - case "apiDeleteChatItem": - return `/_delete item ${cmd.chatType}${cmd.chatId} ${cmd.chatItemId} ${cmd.deleteMode}` - case "apiDeleteMemberChatItem": - return `/_delete member item #${cmd.groupId} ${cmd.groupMemberId} ${cmd.itemId}` - case "apiChatRead": { - const itemRange = cmd.itemRange ? ` from=${cmd.itemRange.fromItem} to=${cmd.itemRange.toItem}` : "" - return `/_read chat ${cmd.chatType}${cmd.chatId}${itemRange}` - } - case "apiDeleteChat": - return `/_delete ${cmd.chatType}${cmd.chatId}` - case "apiClearChat": - return `/_clear chat ${cmd.chatType}${cmd.chatId}` - case "apiAcceptContact": - return `/_accept ${cmd.contactReqId}` - case "apiRejectContact": - return `/_reject ${cmd.contactReqId}` - case "apiUpdateProfile": - return `/_profile ${cmd.userId} ${JSON.stringify(cmd.profile)}` - case "apiSetContactAlias": - return `/_set alias @${cmd.contactId} ${cmd.localAlias.trim()}` - case "newGroup": - return `/_group ${JSON.stringify(cmd.groupProfile)}` - case "apiAddMember": - return `/_add #${cmd.groupId} ${cmd.contactId} ${cmd.memberRole}` - case "apiJoinGroup": - return `/_join #${cmd.groupId}` - case "apiRemoveMember": - return `/_remove #${cmd.groupId} ${cmd.memberId}` - case "apiLeaveGroup": - return `/_leave #${cmd.groupId}` - case "apiListMembers": - return `/_members #${cmd.groupId}` - case "apiUpdateGroupProfile": - return `/_group_profile #${cmd.groupId} ${JSON.stringify(cmd.groupProfile)}` - case "apiCreateGroupLink": - return `/_create link #${cmd.groupId} ${cmd.memberRole}` - case "apiGroupLinkMemberRole": - return `/_set link role #${cmd.groupId} ${cmd.memberRole}` - case "apiDeleteGroupLink": - return `/_delete link #${cmd.groupId}` - case "apiGetGroupLink": - return `/_get link #${cmd.groupId}` - case "apiGetUserProtoServers": - return `/_servers ${cmd.userId} ${cmd.serverProtocol}` - case "apiSetUserProtoServers": - return `/_servers ${cmd.userId} ${cmd.serverProtocol} ${JSON.stringify({servers: cmd.servers})}` - case "apiContactInfo": - return `/_info @${cmd.contactId}` - case "apiGroupMemberInfo": - return `/_info #${cmd.groupId} ${cmd.memberId}` - case "apiGetContactCode": - return `/_get code @${cmd.contactId}` - case "apiGetGroupMemberCode": - return `/_get code #${cmd.groupId} ${cmd.groupMemberId}` - case "apiVerifyContact": - return `/_verify code @${cmd.contactId}${maybe(cmd.connectionCode)}` - case "apiVerifyGroupMember": - return `/_verify code #${cmd.groupId} ${cmd.groupMemberId}${maybe(cmd.connectionCode)}` - case "addContact": - return "/connect" - case "connect": - return `/connect ${cmd.connReq}` - case "connectSimplex": - return "/simplex" - case "createMyAddress": - return "/address" - case "deleteMyAddress": - return "/delete_address" - case "showMyAddress": - return "/show_address" - case "setProfileAddress": - return `/profile_address ${onOff(cmd.includeInProfile)}` - case "addressAutoAccept": - return `/auto_accept ${autoAcceptStr(cmd.autoAccept)}` - case "apiCreateMyAddress": - return `/_address ${cmd.userId}` - case "apiDeleteMyAddress": - return `/_delete_address ${cmd.userId}` - case "apiShowMyAddress": - return `/_show_address ${cmd.userId}` - case "apiSetProfileAddress": - return `/_profile_address ${cmd.userId} ${onOff(cmd.includeInProfile)}` - case "apiAddressAutoAccept": - return `/_auto_accept ${cmd.userId} ${autoAcceptStr(cmd.autoAccept)}` - case "receiveFile": - return `/freceive ${cmd.fileId}${cmd.filePath ? " " + cmd.filePath : ""}` - case "cancelFile": - return `/fcancel ${cmd.fileId}` - case "fileStatus": - return `/fstatus ${cmd.fileId}` - } -} - -function paginationStr(cp: ChatPagination): string { - const base = "after" in cp ? ` after=${cp.after}` : "before" in cp ? ` before=${cp.before}` : "" - return base + ` count=${cp.count}` -} - -function maybe(value: T | undefined): string { - return value ? ` ${value}` : "" -} - -function maybeJSON(value: T | undefined): string { - return value ? ` json ${JSON.stringify(value)}` : "" -} - -function onOff(value: T | undefined): string { - return value ? "on" : "off" -} - -function autoAcceptStr(autoAccept: AutoAccept | undefined): string { - if (!autoAccept) return "off" - const msg = autoAccept.autoReply - return "on" + (autoAccept.acceptIncognito ? " incognito=on" : "") + (msg ? " json " + JSON.stringify(msg) : "") -} diff --git a/packages/simplex-chat-client/typescript/src/response.ts b/packages/simplex-chat-client/typescript/src/response.ts deleted file mode 100644 index 95eac89249..0000000000 --- a/packages/simplex-chat-client/typescript/src/response.ts +++ /dev/null @@ -1,1109 +0,0 @@ -import {ChatItemId, MsgContent, DeleteMode, Profile, GroupMemberRole, LocalProfile, ServerProtocol, ServerCfg} from "./command" - -export type ChatResponse = - | CRActiveUser - | CRUsersList - | CRChatStarted - | CRChatRunning - | CRChatStopped - | CRApiChats - | CRApiChat - | CRApiParsedMarkdown - | CRUserProtoServers - | CRContactInfo - | CRGroupMemberInfo - | CRNewChatItems - | CRChatItemStatusUpdated - | CRChatItemUpdated - | CRChatItemDeleted - | CRMsgIntegrityError - | CRCmdOk - | CRUserContactLink - | CRUserContactLinkUpdated - | CRContactRequestRejected - | CRUserProfile - | CRUserProfileNoChange - | CRUserProfileUpdated - | CRContactAliasUpdated - | CRInvitation - | CRSentConfirmation - | CRSentInvitation - | CRContactUpdated - | CRContactsMerged - | CRContactDeleted - | CRChatCleared - | CRUserContactLinkCreated - | CRUserContactLinkDeleted - | CRReceivedContactRequest - | CRAcceptingContactRequest - | CRContactAlreadyExists - | CRContactRequestAlreadyAccepted - | CRContactConnecting - | CRContactConnected - | CRContactAnotherClient - | CRContactSubError - | CRContactSubSummary - | CRContactsDisconnected - | CRContactsSubscribed - | CRHostConnected - | CRHostDisconnected - | CRGroupEmpty - | CRMemberSubError - | CRMemberSubSummary - | CRGroupSubscribed - | CRRcvFileAccepted - | CRRcvFileAcceptedSndCancelled - | CRRcvFileStart - | CRRcvFileComplete - | CRRcvFileCancelled - | CRRcvFileSndCancelled - | CRSndFileStart - | CRSndFileComplete - | CRSndFileCancelled - | CRSndFileRcvCancelled - | CRSndGroupFileCancelled - | CRSndFileSubError - | CRRcvFileSubError - | CRPendingSubSummary - | CRGroupCreated - | CRGroupMembers - | CRUserAcceptedGroupSent - | CRUserDeletedMember - | CRSentGroupInvitation - | CRLeftMemberUser - | CRGroupDeletedUser - | CRGroupInvitation - | CRReceivedGroupInvitation - | CRUserJoinedGroup - | CRJoinedGroupMember - | CRJoinedGroupMemberConnecting - | CRConnectedToGroupMember - | CRDeletedMember - | CRDeletedMemberUser - | CRLeftMember - | CRGroupRemoved - | CRGroupDeleted - | CRGroupUpdated - | CRUserContactLinkSubError - | CRContactConnectionDeleted - | CRMessageError - | CRChatCmdError - | CRChatError - -// not included -// CRChatItemDeletedNotFound -// CRBroadcastSent -// CRGroupsList -// CRFileTransferStatus - -type ChatResponseTag = - | "activeUser" - | "usersList" - | "chatStarted" - | "chatRunning" - | "chatStopped" - | "apiChats" - | "apiChat" - | "apiParsedMarkdown" - | "userProtoServers" - | "contactInfo" - | "groupMemberInfo" - | "newChatItems" - | "chatItemStatusUpdated" - | "chatItemUpdated" - | "chatItemDeleted" - | "msgIntegrityError" - | "cmdOk" - | "userContactLink" - | "userContactLinkUpdated" - | "userContactLinkCreated" - | "userContactLinkDeleted" - | "contactRequestRejected" - | "userProfile" - | "userProfileNoChange" - | "userProfileUpdated" - | "contactAliasUpdated" - | "invitation" - | "sentConfirmation" - | "sentInvitation" - | "contactUpdated" - | "contactsMerged" - | "contactDeleted" - | "chatCleared" - | "receivedContactRequest" - | "acceptingContactRequest" - | "contactAlreadyExists" - | "contactRequestAlreadyAccepted" - | "contactConnecting" - | "contactConnected" - | "contactAnotherClient" - | "contactSubError" - | "contactSubSummary" - | "contactsDisconnected" - | "contactsSubscribed" - | "hostConnected" - | "hostDisconnected" - | "groupEmpty" - | "memberSubError" - | "memberSubSummary" - | "groupSubscribed" - | "rcvFileAccepted" - | "rcvFileAcceptedSndCancelled" - | "rcvFileStart" - | "rcvFileComplete" - | "rcvFileCancelled" - | "rcvFileSndCancelled" - | "sndFileStart" - | "sndFileComplete" - | "sndFileCancelled" - | "sndFileRcvCancelled" - | "sndGroupFileCancelled" - | "fileTransferStatus" - | "sndFileSubError" - | "rcvFileSubError" - | "pendingSubSummary" - | "groupCreated" - | "groupMembers" - | "userAcceptedGroupSent" - | "userDeletedMember" - | "sentGroupInvitation" - | "leftMemberUser" - | "groupDeletedUser" - | "groupInvitation" - | "receivedGroupInvitation" - | "userJoinedGroup" - | "joinedGroupMember" - | "joinedGroupMemberConnecting" - | "connectedToGroupMember" - | "deletedMember" - | "deletedMemberUser" - | "leftMember" - | "groupRemoved" - | "groupDeleted" - | "groupUpdated" - | "userContactLinkSubError" - | "newContactConnection" - | "contactConnectionDeleted" - | "messageError" - | "chatCmdError" - | "chatError" - -interface CR { - type: ChatResponseTag -} - -export interface CRActiveUser extends CR { - type: "activeUser" - user: User -} - -export interface CRUsersList extends CR { - type: "usersList" - users: UserInfo[] -} - -export interface CRChatStarted extends CR { - type: "chatStarted" -} - -export interface CRChatRunning extends CR { - type: "chatRunning" -} - -export interface CRChatStopped extends CR { - type: "chatStopped" -} - -export interface CRApiChats extends CR { - type: "apiChats" - user: User - chats: Chat[] -} - -export interface CRApiChat extends CR { - type: "apiChat" - user: User - chat: Chat -} - -export interface CRApiParsedMarkdown extends CR { - type: "apiParsedMarkdown" - formattedText?: FormattedText[] -} - -export interface CRUserProtoServers extends CR { - type: "userProtoServers" - user: User - servers: UserProtoServers -} - -export interface CRContactInfo extends CR { - type: "contactInfo" - user: User - contact: Contact - connectionStats: ConnectionStats - customUserProfile?: Profile -} - -export interface CRGroupMemberInfo extends CR { - type: "groupMemberInfo" - user: User - groupInfo: GroupInfo - member: GroupMember - connectionStats_?: ConnectionStats -} - -export interface CRNewChatItems extends CR { - type: "newChatItems" - user: User - chatItems: AChatItem[] -} - -export interface CRChatItemStatusUpdated extends CR { - type: "chatItemStatusUpdated" - user: User - chatItem: AChatItem -} - -export interface CRChatItemUpdated extends CR { - type: "chatItemUpdated" - user: User - chatItem: AChatItem -} - -export interface CRChatItemDeleted extends CR { - type: "chatItemDeleted" - user: User - deletedChatItem: AChatItem - toChatItem?: AChatItem - byUser: boolean -} - -export interface CRMsgIntegrityError extends CR { - type: "msgIntegrityError" - user: User - msgError: MsgErrorType -} - -export interface CRCmdOk extends CR { - type: "cmdOk" - user_?: User -} - -export interface CRUserContactLink extends CR { - type: "userContactLink" - user: User - contactLink: UserContactLink -} - -export interface CRUserContactLinkUpdated extends CR { - type: "userContactLinkUpdated" - user: User - contactLink: UserContactLink -} - -export interface CRContactRequestRejected extends CR { - type: "contactRequestRejected" - user: User - contactRequest: UserContactRequest -} - -export interface CRUserProfile extends CR { - type: "userProfile" - user: User - profile: Profile -} - -export interface CRUserProfileNoChange extends CR { - type: "userProfileNoChange" - user: User -} - -export interface CRUserProfileUpdated extends CR { - type: "userProfileUpdated" - user: User - fromProfile: Profile - toProfile: Profile -} - -export interface CRContactAliasUpdated extends CR { - type: "contactAliasUpdated" - user: User - toContact: Contact -} - -export interface CRInvitation extends CR { - type: "invitation" - user: User - connLinkInvitation: CreatedConnLink -} - -export interface CRSentConfirmation extends CR { - type: "sentConfirmation" - user: User -} - -export interface CRSentInvitation extends CR { - type: "sentInvitation" - user: User -} - -export interface CRContactUpdated extends CR { - type: "contactUpdated" - user: User - fromContact: Contact - toContact: Contact -} - -export interface CRContactsMerged extends CR { - type: "contactsMerged" - user: User - intoContact: Contact - mergedContact: Contact -} - -export interface CRContactDeleted extends CR { - type: "contactDeleted" - user: User - contact: Contact -} - -export interface CRChatCleared extends CR { - type: "chatCleared" - user: User - chatInfo: ChatInfo -} - -export interface CRUserContactLinkCreated extends CR { - type: "userContactLinkCreated" - user: User - connLinkContact: CreatedConnLink -} - -export interface CRUserContactLinkDeleted extends CR { - type: "userContactLinkDeleted" - user: User -} - -export interface CRReceivedContactRequest extends CR { - type: "receivedContactRequest" - user: User - contactRequest: UserContactRequest -} - -export interface CRAcceptingContactRequest extends CR { - type: "acceptingContactRequest" - user: User - contact: Contact -} - -export interface CRContactAlreadyExists extends CR { - type: "contactAlreadyExists" - user: User - contact: Contact -} - -export interface CRContactRequestAlreadyAccepted extends CR { - type: "contactRequestAlreadyAccepted" - user: User - contact: Contact -} - -export interface CRContactConnecting extends CR { - type: "contactConnecting" - user: User - contact: Contact -} - -export interface CRContactConnected extends CR { - type: "contactConnected" - contact: Contact - user: User - userCustomProfile?: Profile -} - -export interface CRContactAnotherClient extends CR { - type: "contactAnotherClient" - user: User - contact: Contact -} - -export interface CRContactSubError extends CR { - type: "contactSubError" - user: User - contact: Contact - chatError: ChatError -} - -export interface CRContactSubSummary extends CR { - type: "contactSubSummary" - user: User - contactSubscriptions: ContactSubStatus[] -} - -export interface CRContactsDisconnected extends CR { - type: "contactsDisconnected" - user: User - server: string - contactRefs: ContactRef[] -} - -export interface CRContactsSubscribed extends CR { - type: "contactsSubscribed" - user: User - server: string - contactRefs: ContactRef[] -} - -export interface CRHostConnected extends CR { - type: "hostConnected" - protocol: string - transportHost: string -} - -export interface CRHostDisconnected extends CR { - type: "hostDisconnected" - protocol: string - transportHost: string -} - -export interface CRGroupEmpty extends CR { - type: "groupEmpty" - user: User - groupInfo: GroupInfo -} - -export interface CRMemberSubError extends CR { - type: "memberSubError" - user: User - groupInfo: GroupInfo - member: GroupMember - chatError: ChatError -} - -export interface CRMemberSubSummary extends CR { - type: "memberSubSummary" - user: User - memberSubscriptions: MemberSubStatus[] -} - -export interface CRGroupSubscribed extends CR { - type: "groupSubscribed" - user: User - groupInfo: GroupInfo -} - -export interface CRRcvFileAccepted extends CR { - type: "rcvFileAccepted" - user: User - chatItem: AChatItem -} - -export interface CRRcvFileAcceptedSndCancelled extends CR { - type: "rcvFileAcceptedSndCancelled" - user: User - rcvFileTransfer: RcvFileTransfer -} - -export interface CRRcvFileStart extends CR { - type: "rcvFileStart" - user: User - chatItem: AChatItem -} - -export interface CRRcvFileComplete extends CR { - type: "rcvFileComplete" - user: User - chatItem: AChatItem -} - -export interface CRRcvFileCancelled extends CR { - type: "rcvFileCancelled" - user: User - rcvFileTransfer: RcvFileTransfer -} - -export interface CRRcvFileSndCancelled extends CR { - type: "rcvFileSndCancelled" - user: User - rcvFileTransfer: RcvFileTransfer -} - -export interface CRSndFileStart extends CR { - type: "sndFileStart" - user: User - chatItem: AChatItem - sndFileTransfer: SndFileTransfer -} - -export interface CRSndFileComplete extends CR { - type: "sndFileComplete" - user: User - chatItem: AChatItem - sndFileTransfer: SndFileTransfer -} - -export interface CRSndFileCancelled extends CR { - type: "sndFileCancelled" - user: User - chatItem: AChatItem - sndFileTransfer: SndFileTransfer -} - -export interface CRSndFileRcvCancelled extends CR { - type: "sndFileRcvCancelled" - user: User - chatItem: AChatItem - sndFileTransfer: SndFileTransfer -} - -export interface CRSndGroupFileCancelled extends CR { - type: "sndGroupFileCancelled" - user: User - chatItem: AChatItem - fileTransferMeta: FileTransferMeta - sndFileTransfers: SndFileTransfer[] -} - -export interface CRSndFileSubError extends CR { - type: "sndFileSubError" - user: User - sndFileTransfer: SndFileTransfer - chatError: ChatError -} - -export interface CRRcvFileSubError extends CR { - type: "rcvFileSubError" - user: User - rcvFileTransfer: RcvFileTransfer - chatError: ChatError -} - -export interface CRPendingSubSummary extends CR { - type: "pendingSubSummary" - user: User - pendingSubStatus: PendingSubStatus[] -} - -export interface CRGroupCreated extends CR { - type: "groupCreated" - user: User - groupInfo: GroupInfo -} - -export interface CRGroupMembers extends CR { - type: "groupMembers" - user: User - group: Group -} - -export interface CRUserAcceptedGroupSent extends CR { - type: "userAcceptedGroupSent" - user: User - groupInfo: GroupInfo - hostContact?: Contact // included when joining group via group link -} - -export interface CRUserDeletedMember extends CR { - type: "userDeletedMember" - user: User - groupInfo: GroupInfo - member: GroupMember -} - -export interface CRSentGroupInvitation extends CR { - type: "sentGroupInvitation" - user: User - groupInfo: GroupInfo - contact: Contact - member: GroupMember -} - -export interface CRLeftMemberUser extends CR { - type: "leftMemberUser" - user: User - groupInfo: GroupInfo -} - -export interface CRGroupDeletedUser extends CR { - type: "groupDeletedUser" - user: User - groupInfo: GroupInfo -} - -export interface CRGroupInvitation extends CR { - type: "groupInvitation" - user: User - groupInfo: GroupInfo -} - -export interface CRReceivedGroupInvitation extends CR { - type: "receivedGroupInvitation" - user: User - groupInfo: GroupInfo - contact: Contact - memberRole: GroupMemberRole -} - -export interface CRUserJoinedGroup extends CR { - type: "userJoinedGroup" - user: User - groupInfo: GroupInfo - hostMember: GroupMember -} - -export interface CRJoinedGroupMember extends CR { - type: "joinedGroupMember" - user: User - groupInfo: GroupInfo - member: GroupMember -} - -export interface CRJoinedGroupMemberConnecting extends CR { - type: "joinedGroupMemberConnecting" - user: User - groupInfo: GroupInfo - hostMember: GroupMember - member: GroupMember -} - -export interface CRConnectedToGroupMember extends CR { - type: "connectedToGroupMember" - user: User - groupInfo: GroupInfo - member: GroupMember -} - -export interface CRDeletedMember extends CR { - type: "deletedMember" - user: User - groupInfo: GroupInfo - byMember: GroupMember - deletedMember: GroupMember -} - -export interface CRDeletedMemberUser extends CR { - type: "deletedMemberUser" - user: User - groupInfo: GroupInfo - member: GroupMember -} - -export interface CRLeftMember extends CR { - type: "leftMember" - user: User - groupInfo: GroupInfo - member: GroupMember -} - -export interface CRGroupRemoved extends CR { - type: "groupRemoved" - user: User - groupInfo: GroupInfo -} - -export interface CRGroupDeleted extends CR { - type: "groupDeleted" - user: User - groupInfo: GroupInfo - member: GroupMember -} - -export interface CRGroupUpdated extends CR { - type: "groupUpdated" - user: User - fromGroup: GroupInfo - toGroup: GroupInfo - member_?: GroupMember -} - -export interface CRUserContactLinkSubError extends CR { - type: "userContactLinkSubError" - chatError: ChatError -} - -export interface CRContactConnectionDeleted extends CR { - type: "contactConnectionDeleted" - user: User - connection: PendingContactConnection -} - -export interface CRMessageError extends CR { - type: "messageError" - user: User - severity: string - errorMessage: string -} - -export interface CRChatCmdError extends CR { - type: "chatCmdError" - user_?: User - chatError: ChatError -} - -export interface CRChatError extends CR { - type: "chatError" - user_?: User - chatError: ChatError -} - -export interface User { - userId: number - agentUserId: string - userContactId: number - localDisplayName: string - profile: LocalProfile - // fullPreferences :: FullPreferences - activeUser: boolean - viewPwdHash: string - showNtfs: boolean -} - -export interface UserProtoServers { - serverProtocol: ServerProtocol - protoServers: ServerCfg[] - presetServers: string -} - -export interface Chat { - chatInfo: ChatInfo - chatItems: [ChatItem] - chatStats: ChatStats -} - -export type ChatInfo = CInfoDirect | CInfoGroup | CInfoContactRequest - -export enum ChatInfoType { - Direct = "direct", - Group = "group", - ContactRequest = "contactRequest", -} - -interface IChatInfo { - type: ChatInfoType -} - -interface CInfoDirect extends IChatInfo { - type: ChatInfoType.Direct - contact: Contact -} - -interface CInfoGroup extends IChatInfo { - type: ChatInfoType.Group - groupInfo: GroupInfo -} - -interface CInfoContactRequest extends IChatInfo { - type: ChatInfoType.ContactRequest - contactRequest: UserContactRequest -} - -export interface UserPwdHash { - hash: string - salt: string -} - -interface UserInfo { - user: User - unreadCount: number -} - -export interface Contact { - contactId: number - localDisplayName: string - profile: Profile - activeConn: Connection - viaGroup?: number - createdAt: Date -} - -export interface ContactRef { - contactId: number - localDisplayName: string -} - -export interface Group { - groupInfo: GroupInfo - members: GroupMember[] -} - -export interface GroupInfo { - groupId: number - localDisplayName: string - groupProfile: GroupProfile - membership: GroupMember - createdAt: Date -} - -export interface GroupProfile { - displayName: string - fullName: string - image?: string // web-compatible data/base64 string for the image -} - -export interface GroupMember { - groupMemberId: number - memberId: string - memberRole: GroupMemberRole - // memberCategory: GroupMemberCategory - // memberStatus: GroupMemberStatus - // invitedBy: InvitedBy - localDisplayName: string - memberProfile: Profile - memberContactId?: number - activeConn?: Connection -} - -export interface UserContactRequest { - contactRequestId: number - localDisplayName: string - profile: Profile - createdAt: Date -} - -interface Connection { - connId: number -} - -export interface AChatItem { - chatInfo: ChatInfo - chatItem: ChatItem -} - -export interface ChatItem { - chatDir: CIDirection - meta: CIMeta - content: CIContent - formattedText?: FormattedText[] - quotedItem?: CIQuote -} - -export type CIDirection = CIDirectSnd | CIDirectRcv | CIGroupSnd | CIGroupRcv - -interface ICIDirection { - type: "directSnd" | "directRcv" | "groupSnd" | "groupRcv" -} - -interface CIDirectSnd extends ICIDirection { - type: "directSnd" -} - -interface CIDirectRcv extends ICIDirection { - type: "directRcv" -} - -interface CIGroupSnd extends ICIDirection { - type: "groupSnd" -} - -interface CIGroupRcv extends ICIDirection { - type: "groupRcv" - groupMember: GroupMember -} - -export interface CIMeta { - itemId: ChatItemId - itemTs: Date - itemText: string - itemStatus: CIStatus - createdAt: Date - itemDeleted: boolean - itemEdited: boolean - editable: boolean -} - -export type CIContent = CISndMsgContent | CIRcvMsgContent | CISndDeleted | CIRcvDeleted | CISndFileInvitation | CIRcvFileInvitation - -interface ICIContent { - type: "sndMsgContent" | "rcvMsgContent" | "sndDeleted" | "rcvDeleted" | "sndFileInvitation" | "rcvFileInvitation" -} - -interface CISndMsgContent extends ICIContent { - type: "sndMsgContent" - msgContent: MsgContent -} - -interface CIRcvMsgContent extends ICIContent { - type: "rcvMsgContent" - msgContent: MsgContent -} - -interface CISndDeleted extends ICIContent { - type: "sndDeleted" - deleteMode: DeleteMode -} - -interface CIRcvDeleted extends ICIContent { - type: "rcvDeleted" - deleteMode: DeleteMode -} - -interface CISndFileInvitation extends ICIContent { - type: "sndFileInvitation" - fileId: number - filePath: string -} - -interface CIRcvFileInvitation extends ICIContent { - type: "rcvFileInvitation" - rcvFileTransfer: RcvFileTransfer -} - -export function ciContentText(content: CIContent): string | undefined { - switch (content.type) { - case "sndMsgContent": - return content.msgContent.text - case "rcvMsgContent": - return content.msgContent.text - default: - return undefined - } -} - -interface RcvFileTransfer { - fileId: number - // fileInvitation: FileInvitation - // fileStatus: RcvFileStatus - senderDisplayName: string - chunkSize: number - cancelled: boolean - grpMemberId?: number -} - -interface SndFileTransfer { - fileId: number - fileName: string - filePath: string - fileSize: number - chunkSize: number - recipientDisplayName: string - connId: number - // agentConnId: string - // fileStatus: FileStatus -} - -interface FileTransferMeta { - fileId: number - fileName: string - filePath: string - fileSize: number - chunkSize: number - cancelled: boolean -} - -interface UserContactLink { - connLinkContact: CreatedConnLink - autoAccept?: AutoAccept -} - -interface CreatedConnLink { - connFullLink: string - connShortLink?: string -} - -interface AutoAccept { - acceptIncognito: boolean - autoReply?: MsgContent -} - -export interface ChatStats { - unreadCount: number - minUnreadItemId: number -} - -export interface CIQuote { - chatDir?: CIDirection - itemId?: number - sharedMsgId?: string - sentAt: Date - content: MsgContent - formattedText?: FormattedText[] -} - -export type CIStatus = CISndNew | CISndSent | CISndErrorAuth | CISndError | CIRcvNew | CIRcvRead - -interface ICIStatus { - type: "sndNew" | "sndSent" | "sndErrorAuth" | "sndError" | "rcvNew" | "rcvRead" -} - -interface CISndNew extends ICIStatus { - type: "sndNew" -} - -interface CISndSent extends ICIStatus { - type: "sndSent" -} - -interface CISndErrorAuth extends ICIStatus { - type: "sndErrorAuth" -} - -interface CISndError extends ICIStatus { - type: "sndError" - agentError: AgentErrorType -} - -interface CIRcvNew extends ICIStatus { - type: "rcvNew" -} - -interface CIRcvRead extends ICIStatus { - type: "rcvRead" -} - -interface FormattedText {} - -interface MsgErrorType {} - -export type ChatError = ChatErrorChat | ChatErrorAgent | ChatErrorStore - -interface ChatErrorChat { - type: "error" - errorType: ChatErrorType -} - -interface ChatErrorAgent { - type: "errorAgent" - agentError: AgentErrorType -} - -interface ChatErrorStore { - type: "errorStore" - storeError: StoreErrorType -} - -type ChatErrorType = CENoActiveUser | CEActiveUserExists - -interface CENoActiveUser { - type: "noActiveUser" -} - -interface CEActiveUserExists { - type: "activeUserExists" -} - -interface ContactSubStatus {} - -interface PendingSubStatus {} - -export interface ConnectionStats { - rcvServers?: string[] - sndServers?: string[] -} - -interface PendingContactConnection {} - -interface MemberSubStatus { - member: GroupMember - memberError?: ChatError -} - -interface AgentErrorType { - type: string - [x: string]: any -} - -interface StoreErrorType { - type: string - [x: string]: any -} diff --git a/packages/simplex-chat-client/typescript/src/transport.ts b/packages/simplex-chat-client/typescript/src/transport.ts index 7ac4673ca1..aad3f39579 100644 --- a/packages/simplex-chat-client/typescript/src/transport.ts +++ b/packages/simplex-chat-client/typescript/src/transport.ts @@ -1,5 +1,5 @@ import {ABQueue, NextIter} from "./queue" -import {ChatResponse} from "./response" +import {ChatResponse, ChatEvent} from "@simplex-chat/types" export class TransportError extends Error {} @@ -90,13 +90,17 @@ export interface ChatSrvRequest { } export interface ChatSrvResponse { - corrId?: string + corrId: string resp: ChatResponse } +export interface ChatSrvEvent { + resp: ChatEvent +} + interface ParsedChatSrvResponse { corrId?: string - resp?: ChatResponse + resp?: ChatResponse | ChatEvent } export class ChatResponseError extends Error { @@ -105,7 +109,7 @@ export class ChatResponseError extends Error { } } -export class ChatTransport extends Transport { +export class ChatTransport extends Transport { private constructor(private readonly ws: WSTransport, readonly timeout: number, qSize: number) { super(qSize) } diff --git a/packages/simplex-chat-client/typescript/tests/client.test.ts b/packages/simplex-chat-client/typescript/tests/client.test.ts index eef57558f2..77dca8fa5a 100644 --- a/packages/simplex-chat-client/typescript/tests/client.test.ts +++ b/packages/simplex-chat-client/typescript/tests/client.test.ts @@ -1,42 +1,43 @@ import * as assert from "assert" import {ChatClient} from "../src/index" import {ConnReqType} from "../src/client" -import * as CC from "../src/command" -import * as CR from "../src/response" +import {ChatEvent, CEvt, T} from "@simplex-chat/types" +// This test is currently failing - it gets as far as starting connection. +// It has to be written differently, with event loop to only process "interesting" events, as some events may arrive in different order. describe.skip("ChatClient (expects SimpleX Chat server with a user, without contacts, on localhost:5225)", () => { test("connect, send message to themselves, delete contact", async () => { const c = await ChatClient.create("ws://localhost:5225") assert.strictEqual((await c.msgQ.dequeue()).type, "contactSubSummary") - assert.strictEqual((await c.msgQ.dequeue()).type, "memberSubErrors") - assert.strictEqual((await c.msgQ.dequeue()).type, "pendingSubSummary") + assert.strictEqual((await c.msgQ.dequeue()).type, "userContactSubSummary") + assert.strictEqual((await c.msgQ.dequeue()).type, "terminalEvent") + assert.strictEqual((await c.msgQ.dequeue()).type, "terminalEvent") const user = await c.apiGetActiveUser() assert.strictEqual(typeof user?.localDisplayName, "string") - const connReq = await c.apiCreateLink() + const connReq = await c.apiCreateLink(user!.userId) + console.log("created link") assert.strictEqual(typeof connReq, "string") - assert.strictEqual((await c.msgQ.dequeue()).type, "newContactConnection") // TODO add to response types - const connReqType = await c.apiConnect(connReq) - assert.strictEqual((await c.msgQ.dequeue()).type, "newContactConnection") // TODO add to response types - assert((await c.msgQ.dequeue()).type === "contactConnecting") - assert((await c.msgQ.dequeue()).type === "contactConnecting") + const connReqType = await c.apiConnectActiveUser(connReq) + assert.strictEqual((await c.msgQ.dequeue()).type, "contactConnecting") + assert.strictEqual((await c.msgQ.dequeue()).type, "contactConnected") assert(connReqType === ConnReqType.Invitation || connReqType === ConnReqType.Contact) const r1 = await c.msgQ.dequeue() const r2 = await c.msgQ.dequeue() - assert(r1.type === "contactConnected") - assert(r2.type === "contactConnected") - const contact1 = (r1 as CR.CRContactConnected).contact + assert.strictEqual(r1.type, "contactConnecting") + assert.strictEqual(r2.type, "contactConnected") + const contact1 = (r1 as CEvt.ContactConnected).contact // const contact2 = (r2 as C.CRContactConnected).contact - const r3 = await c.apiSendTextMessage(CC.ChatType.Direct, contact1.contactId, "hello") + const r3 = await c.apiSendTextMessage(T.ChatType.Direct, contact1.contactId, "hello") assert(r3[0].chatItem.content.type === "sndMsgContent" && r3[0].chatItem.content.msgContent.text === "hello") const r4 = await c.msgQ.dequeue() assert(isItemSent(r4) || isNewRcvItem(r4)) await c.disconnect() - function isItemSent(r: CR.ChatResponse): boolean { - return r.type === "chatItemStatusUpdated" && r.chatItem.chatItem.meta.itemStatus.type === "sndSent" + function isItemSent(r: ChatEvent): boolean { + return r.type === "chatItemsStatusesUpdated" && r.chatItems[0].chatItem.meta.itemStatus.type === "sndSent" } - function isNewRcvItem(r: CR.ChatResponse): boolean { + function isNewRcvItem(r: ChatEvent): boolean { return ( r.type === "newChatItems" && r.chatItems[0].chatItem.content.type === "rcvMsgContent" && diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 9b9c5a9581..b2bc988d79 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -524,6 +524,7 @@ test-suite simplex-chat-test API.Docs.Commands API.Docs.Events API.Docs.Generate + API.Docs.Generate.TypeScript API.Docs.Responses API.Docs.Syntax API.Docs.Syntax.Types diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 998d7542b3..53d3839f42 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -460,8 +460,8 @@ data ChatCommand | APIChangePreparedGroupUser GroupId UserId | APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent} | APIConnectPreparedGroup GroupId IncognitoEnabled (Maybe MsgContent) - | APIConnect {userId :: UserId, incognito :: IncognitoEnabled, connLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error - | Connect IncognitoEnabled (Maybe AConnectionLink) + | APIConnect {userId :: UserId, incognito :: IncognitoEnabled, preparedLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error + | Connect {incognito :: IncognitoEnabled, connLink_ :: Maybe AConnectionLink} | APIConnectContactViaAddress UserId IncognitoEnabled ContactId | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | DeleteContact ContactName ChatDeleteMode diff --git a/tests/APIDocs.hs b/tests/APIDocs.hs index 51261f6fe5..aa9cc8459f 100644 --- a/tests/APIDocs.hs +++ b/tests/APIDocs.hs @@ -8,6 +8,7 @@ module APIDocs where import API.Docs.Commands import API.Docs.Events import API.Docs.Generate +import qualified API.Docs.Generate.TypeScript as TS import API.Docs.Responses import API.Docs.Types import API.TypeInfo @@ -15,6 +16,7 @@ import Control.Monad import Data.Containers.ListUtils (nubOrd) import Data.List (foldl', intercalate, sort, (\\)) import qualified Data.Set as S +import qualified Data.Text as T import qualified Data.Text.IO as T import Simplex.Messaging.Util (ifM) import System.Directory (doesFileExist) @@ -26,15 +28,20 @@ apiDocsTest = do it "should be documented" testCommandsHaveDocs it "should have field names" testCommandsHaveNamedFields it "should have defined responses" testCommandsHaveResponses - it "generate markdown" testGenerateCommandsMD + it "generate markdown" $ testGenerate commandsDocFile commandsDocText describe "API responses" $ do it "should be documented" testResponsesHaveDocs describe "API events" $ do it "should be documented" testEventsHaveDocs - it "generate markdown" testGenerateEventsMD + it "generate markdown" $ testGenerate eventsDocFile eventsDocText describe "API types" $ do it "should be documented" testTypesHaveDocs - it "generate markdown" testGenerateTypesMD + it "generate markdown" $ testGenerate typesDocFile typesDocText + describe "TypeScript" $ do + it "generate typescript commands code" $ testGenerate TS.commandsCodeFile TS.commandsCodeText + it "generate typescript responses code" $ testGenerate TS.responsesCodeFile TS.responsesCodeText + it "generate typescript events code" $ testGenerate TS.eventsCodeFile TS.eventsCodeText + it "generate typescript types code" $ testGenerate TS.typesCodeFile TS.typesCodeText documentedCmds :: [String] documentedCmds = concatMap (map consName' . commands) chatCommandsDocs @@ -137,20 +144,8 @@ testCommandsHaveResponses = do unless (null undocResps) $ expectationFailure $ "Undocumented command responses: " <> intercalate ", " undocResps unless (null extraResps) $ expectationFailure $ "Unused documented command responses: " <> intercalate ", " extraResps -testGenerateCommandsMD :: IO () -testGenerateCommandsMD = do - cmdsDoc <- ifM (doesFileExist commandsDocFile) (T.readFile commandsDocFile) (pure "") - T.writeFile commandsDocFile commandsDocText - commandsDocText `shouldBe` cmdsDoc - -testGenerateEventsMD :: IO () -testGenerateEventsMD = do - evtsDoc <- ifM (doesFileExist eventsDocFile) (T.readFile eventsDocFile) (pure "") - T.writeFile eventsDocFile eventsDocText - eventsDocText `shouldBe` evtsDoc - -testGenerateTypesMD :: IO () -testGenerateTypesMD = do - typesDoc <- ifM (doesFileExist typesDocFile) (T.readFile typesDocFile) (pure "") - T.writeFile typesDocFile typesDocText - typesDocText `shouldBe` typesDoc +testGenerate :: FilePath -> T.Text -> IO () +testGenerate file text = do + current <- ifM (doesFileExist file) (T.readFile file) (pure "") + T.writeFile file text + text `shouldBe` current From 7048a1c63bde9518090736d8127c72ae8ea2d42f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 27 Aug 2025 18:10:30 +0100 Subject: [PATCH 010/137] core: 6.4.4.2 --- simplex-chat.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index b2bc988d79..8029915ace 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.4.4.1 +version: 6.4.4.2 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 2585ea2bbf7c7a3319ef77aa2d3c09be8f0e7fb1 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 27 Aug 2025 18:42:48 +0100 Subject: [PATCH 011/137] Translated using Weblate (Hungarian) (#6228) Currently translated at 100.0% (257 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/hu/ Co-authored-by: summoner001 --- website/langs/hu.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/website/langs/hu.json b/website/langs/hu.json index 05a2fea760..7b643c2d97 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -12,10 +12,10 @@ "simplex-explained-tab-2-text": "2. Hogyan működik", "simplex-explained-tab-3-text": "3. Mit látnak a kiszolgálók", "simplex-explained-tab-1-p-1": "Létrehozhat kapcsolatokat és csoportokat, valamint kétirányú beszélgetéseket folytathat, ugyanúgy mint bármely más üzenetváltó-alkalmazásban.", - "simplex-explained-tab-1-p-2": "Hogyan működhet egyirányú üzenet-sorballítással és felhasználói profilazonosítók nélkül?", - "simplex-explained-tab-2-p-1": "Minden kapcsolathoz két különböző üzenetküldési sorbaállítást használ a különböző kiszolgálókon keresztül történő üzenetküldéshez és -fogadáshoz.", + "simplex-explained-tab-1-p-2": "Hogyan működhet egyirányú üzenetvárólistával és felhasználói profilazonosítók nélkül?", + "simplex-explained-tab-2-p-1": "Minden kapcsolathoz két különböző üzenetküldési várólistát használ a különböző kiszolgálókon keresztül történő üzenetküldéshez és -fogadáshoz.", "simplex-explained-tab-2-p-2": "A kiszolgálók csak egyetlen irányba továbbítják az üzeneteket, anélkül, hogy teljes képet kapnának a felhasználók beszélgetéseiről vagy kapcsolatairól.", - "simplex-explained-tab-3-p-1": "A kiszolgálók minden egyes üzenetsorbaállításhoz külön névtelen hitelesítő-adatokkal rendelkeznek, és nem tudják, hogy melyik felhasználóhoz tartoznak.", + "simplex-explained-tab-3-p-1": "A kiszolgálók minden egyes üzenetvárólistához külön névtelen hitelesítő-adatokkal rendelkeznek, és nem tudják, hogy melyik felhasználóhoz tartoznak.", "simplex-explained-tab-3-p-2": "A felhasználók tovább fokozhatják a metaadatok adatvédelmét, ha a Tor hálózat használatával férnek hozzá a kiszolgálókhoz, így megakadályozva az IP-cím szerinti korrelációt.", "smp-protocol": "SMP-protokoll", "chat-protocol": "Csevegési protokoll", @@ -51,10 +51,10 @@ "simplex-private-6-title": "Sávon kívüli
kulcscsere", "simplex-private-7-title": "Üzenetintegritás
hitelesítés", "simplex-private-8-title": "Üzenetek keverése
a korreláció csökkentése érdekében", - "simplex-private-9-title": "Egyirányú
üzenetsorbaállítás", + "simplex-private-9-title": "Egyirányú
üzenetvárólista", "simplex-private-10-title": "Ideiglenes, névtelen, páronkénti azonosítók", "simplex-private-card-1-point-1": "Double-Ratchet-protokoll —
OTR-üzenetküldés, sérülés utáni titkosság-védelemmel és -helyreállítással.", - "simplex-private-card-1-point-2": "NaCL cryptobox minden egyes üzenet sorbaállításához, hogy megakadályozza a forgalom korrelációját az üzenetsorbaállítások között, ha a TLS veszélybe kerül.", + "simplex-private-card-1-point-2": "NaCL cryptobox minden egyes üzenetvárólistához, hogy megakadályozza a forgalom korrelációját az üzenetvárólisták között, ha a TLS veszélybe kerül.", "simplex-private-card-2-point-1": "Kiegészítő kiszolgáló titkosítási réteg a címzettnek történő kézbesítéshez, hogy megakadályozza a fogadott és az elküldött kiszolgálóforgalom közötti korrelációt, ha a TLS veszélybe kerül.", "simplex-private-card-3-point-1": "A kliens és a kiszolgálók közötti kapcsolatokhoz csak az erős algoritmusokkal rendelkező TLS 1.2/1.3 protokollt használja.", "simplex-private-card-3-point-2": "A kiszolgáló ujjlenyomata és a csatornakötés megakadályozza a MITM- és a visszajátszási támadásokat.", @@ -62,10 +62,10 @@ "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a Tor hálózaton vagy más átvitelátfedő hálózaton keresztül is elérheti.", "simplex-private-card-6-point-1": "Számos kommunikációs hálózat sebezhető a kiszolgálók vagy a hálózat-szolgáltatók MITM-támadásaival szemben.", "simplex-private-card-6-point-2": "Ennek megakadályozása érdekében a SimpleX-alkalmazások egyszeri kulcsokat adnak át sávon kívül, amikor egy címet hivatkozásként vagy QR-kódként oszt meg.", - "simplex-private-card-7-point-1": "Az integritás garantálása érdekében az üzenetek sorszámozással vannak ellátva, és tartalmazzák az előző üzenet hasítóértékét.", + "simplex-private-card-7-point-1": "Az integritás garantálása érdekében az üzenetek sorszámozással vannak ellátva, és tartalmazzák az előző üzenet kivonatát.", "simplex-private-card-7-point-2": "Ha bármilyen üzenetet hozzáadnak, eltávolítanak vagy módosítanak, a címzett értesítést kap róla.", "simplex-private-card-8-point-1": "A SimpleX kiszolgálók alacsony késleltetésű keverési csomópontokként működnek — a bejövő és kimenő üzenetek sorrendje eltérő.", - "simplex-private-card-9-point-1": "Minden üzenetsorbaállítás egy irányba továbbítja az üzeneteket, a különböző küldési és vételi címekkel.", + "simplex-private-card-9-point-1": "Minden üzenetvárólista egy irányba továbbítja az üzeneteket, a különböző küldési és vételi címekkel.", "simplex-private-card-9-point-2": "Kevesebb támadási felülettel rendelkezik, mint a hagyományos üzenetváltó-alkalmazások, és kevesebb metaadatot tesz elérhetővé.", "simplex-private-card-10-point-1": "A SimpleX ideiglenes, névtelen, páros címeket és hitelesítő adatokat használ minden egyes felhasználói kapcsolathoz vagy csoporttaghoz.", "simplex-private-card-10-point-2": "Lehetővé teszi az üzenetek felhasználói profilazonosítók nélküli kézbesítését, ami az alternatíváknál jobb metaadat-védelmet biztosít.", @@ -86,7 +86,7 @@ "simplex-unique-4-title": "Öné a SimpleX hálózat", "simplex-unique-4-overlay-1-title": "Teljesen decentralizált — a SimpleX hálózat a felhasználóké", "hero-overlay-card-1-p-1": "Sok felhasználó kérdezte: ha a SimpleXnek nincsenek felhasználói azonosítói, honnan tudja, hogy hová kell eljuttatni az üzeneteket?", - "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi hálózat által használt felhasználói azonosítók helyett a SimpleX az üzenetek sorbaállításához ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", + "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi hálózat által használt felhasználói azonosítók helyett a SimpleX az üzenetek várólistába rendezéséhez ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", "hero-overlay-card-1-p-4": "Ez a kialakítás megakadályozza a felhasználók metaadatainak kiszivárgását az alkalmazás szintjén. Az adatvédelem további javítása és az IP-cím védelme érdekében az üzenetküldő kiszolgálókhoz Tor hálózaton keresztül is kapcsolódhat.", "hero-overlay-card-1-p-5": "Csak a kliensek tárolják a felhasználói profilokat, kapcsolatokat és csoportokat; az üzenetek küldése 2 rétegű végpontok közötti titkosítással történik.", "hero-overlay-card-1-p-6": "További leírást a SimpleX ismertetőben olvashat.", @@ -113,7 +113,7 @@ "privacy-matters-overlay-card-3-p-3": "Átlagos embereket letartóztatnak azért, amit online megosztanak, még „névtelen” fiókjaikon keresztül is, még demokratikus országokban is.", "privacy-matters-overlay-card-3-p-4": "Nem elég csak egy végpontok között titkosított üzenetváltó-alkalmazást használnunk, mindannyiunknak olyan üzenetváltó-alkalmazásokat kell használnunk, amelyek védik a személyes partnereink magánéletét — akikkel kapcsolatban állunk.", "simplex-unique-overlay-card-1-p-1": "Más üzenetküldő hálózatoktól eltérően a SimpleX nem rendel azonosítókat a felhasználókhoz. Nem támaszkodik telefonszámokra, tartomány-alapú címekre (mint az e-mail, XMPP vagy a Matrix), felhasználónevekre, nyilvános kulcsokra vagy akár véletlenszerű számokra a felhasználók azonosításához — a SimpleX kiszolgálók üzemeltetői nem tudják, hogy hányan használják a kiszolgálóikat.", - "simplex-unique-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez a SimpleX az egyirányú üzenet várakoztatást használ páronkénti névtelen címekkel, külön a fogadott és külön az elküldött üzenetek számára, általában különböző kiszolgálókon keresztül.", + "simplex-unique-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez a SimpleX az egyirányú üzenetvárólistákat használ páronkénti névtelen címekkel, külön a fogadott és külön az elküldött üzenetek számára, általában különböző kiszolgálókon keresztül.", "simplex-unique-overlay-card-1-p-3": "Ez a kialakítás védi partnerének adatait, elrejtve azt a SimpleX hálózat kiszolgálói és a külső megfigyelők elől. Az IP-címe elrejtésének érdekében aTor hálózaton keresztül is kapcsolódhat a SimpleX kiszolgálókhoz.", "simplex-unique-overlay-card-2-p-1": "Mivel ön nem rendelkezik azonosítóval a SimpleX hálózaton, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", "simplex-unique-overlay-card-2-p-2": "Még a felhasználói cím használata esetén is, aminek használata nem kötelező – ugyanakkor ez a kéretlen kapcsolatkérelmek küldésére is használható – módosíthatja vagy teljesen törölheti a címet anélkül, hogy elveszítené a meglévő kapcsolatait.", @@ -144,7 +144,7 @@ "more-info": "További információ", "hide-info": "Információ elrejtése", "contact-hero-subheader": "Olvassa be a QR-kódot a SimpleX Chat alkalmazással a telefonjával vagy táblagépével.", - "contact-hero-p-1": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várakoztatási címét NEM küldi el a hálózaton keresztül, amikor megtekinti ezt az oldalt — azokat, a hivatkozás webcímének hasítóérték-töredéke tartalmazza.", + "contact-hero-p-1": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várólistájának címét NEM küldi el a hálózaton keresztül, amikor megtekinti ezt az oldalt — azokat, a hivatkozás webcímének kivonattöredéke tartalmazza.", "contact-hero-p-2": "Még nem töltötte le a SimpleX Chatet?", "contact-hero-p-3": "Az alkalmazás letöltéséhez használja az alábbi hivatkozásokat.", "scan-qr-code-from-mobile-app": "QR-kód beolvasása mobilalkalmazásból", @@ -153,7 +153,7 @@ "open-simplex-app": "Simplex alkalmazás megnyitása", "tap-the-connect-button-in-the-app": "Koppintson a „kapcsolódás” gombra az alkalmazásban", "scan-the-qr-code-with-the-simplex-chat-app": "Olvassa be a QR-kódot a SimpleX Chat alkalmazással", - "scan-the-qr-code-with-the-simplex-chat-app-description": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várakoztatási címét NEM küldjük el a hálózaton keresztül, amikor ezt az oldalt megtekinti —
ezek a hivatkozás webcímének hasítóérték-töredékében szerepelnek.", + "scan-the-qr-code-with-the-simplex-chat-app-description": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várólistájának címét NEM küldjük el a hálózaton keresztül, amikor ezt az oldalt megtekinti —
ezek a hivatkozás webcímének kivonattöredékében szerepelnek.", "installing-simplex-chat-to-terminal": "A SimpleX chat telepítése a terminálhoz", "use-this-command": "Használja ezt a parancsot:", "see-simplex-chat": "Az utasításokat megtekintheti a SimpleX Chat", @@ -173,7 +173,7 @@ "simplex-network-2-header": "A föderált hálózatokkal ellentétben", "simplex-network-2-desc": "A SimpleX továbbítókiszolgálói NEM tárolnak felhasználói profilokat, kapcsolatokat és kézbesített üzeneteket, NEM kapcsolódnak egymáshoz, és NINCS kiszolgálókönyvtár.", "simplex-network-3-header": "SimpleX hálózat", - "simplex-network-3-desc": "a kiszolgálók egyirányú üzenet várakoztatásokat biztosítanak a felhasználók összekapcsolásához, de nem látják a hálózati kapcsolati gráfot; azt csak a felhasználók látják.", + "simplex-network-3-desc": "a kiszolgálók egyirányú üzenetvárólistákat biztosítanak a felhasználók összekapcsolásához, de nem látják a hálózati kapcsolati gráfot; azt csak a felhasználók látják.", "comparison-section-header": "Összehasonlítás más protokollokkal", "protocol-1-text": "Signal, nagy platformok", "protocol-2-text": "XMPP, Matrix", From 0b7a33bf87fc2d6a8eb6ff683eca85ea2cf8be54 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 27 Aug 2025 18:48:56 +0100 Subject: [PATCH 012/137] ui: translations (#6227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Russian) Currently translated at 99.9% (2498 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Romanian) Currently translated at 99.8% (2497 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (German) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Russian) Currently translated at 99.9% (2498 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (German) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/ * Translated using Weblate (Italian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Japanese) Currently translated at 55.9% (1211 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ja/ * Translated using Weblate (Czech) Currently translated at 97.3% (2433 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Persian) Currently translated at 99.9% (2499 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Russian) Currently translated at 99.9% (2498 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Indonesian) Currently translated at 98.8% (2471 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Russian) Currently translated at 99.9% (2498 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * process * ru --------- Co-authored-by: Skyward Copied Co-authored-by: summoner001 Co-authored-by: Nicolae Fericitu Co-authored-by: mlanp Co-authored-by: Random Co-authored-by: daydreamer-json Co-authored-by: zenobit Co-authored-by: jonnysemon Co-authored-by: firmfox Co-authored-by: 大王叫我来巡山 Co-authored-by: Rafi Co-authored-by: No name --- .../bg.xcloc/Localized Contents/bg.xliff | 9 +-- .../cs.xcloc/Localized Contents/cs.xliff | 8 +- .../de.xcloc/Localized Contents/de.xliff | 9 +-- .../en.xcloc/Localized Contents/en.xliff | 10 +-- .../es.xcloc/Localized Contents/es.xliff | 9 +-- .../fi.xcloc/Localized Contents/fi.xliff | 8 +- .../fr.xcloc/Localized Contents/fr.xliff | 9 +-- .../hu.xcloc/Localized Contents/hu.xliff | 45 ++++++------ .../it.xcloc/Localized Contents/it.xliff | 9 +-- .../ja.xcloc/Localized Contents/ja.xliff | 9 ++- .../nl.xcloc/Localized Contents/nl.xliff | 9 +-- .../pl.xcloc/Localized Contents/pl.xliff | 9 +-- .../ru.xcloc/Localized Contents/ru.xliff | 9 +-- .../th.xcloc/Localized Contents/th.xliff | 8 +- .../tr.xcloc/Localized Contents/tr.xliff | 9 +-- .../uk.xcloc/Localized Contents/uk.xliff | 9 +-- .../Localized Contents/zh-Hans.xliff | 9 +-- apps/ios/bg.lproj/Localizable.strings | 3 - apps/ios/de.lproj/Localizable.strings | 3 - apps/ios/es.lproj/Localizable.strings | 3 - apps/ios/fr.lproj/Localizable.strings | 3 - apps/ios/hu.lproj/Localizable.strings | 39 +++++----- apps/ios/it.lproj/Localizable.strings | 3 - apps/ios/ja.lproj/Localizable.strings | 3 + apps/ios/nl.lproj/Localizable.strings | 3 - apps/ios/pl.lproj/Localizable.strings | 3 - apps/ios/ru.lproj/Localizable.strings | 3 - apps/ios/tr.lproj/Localizable.strings | 3 - apps/ios/uk.lproj/Localizable.strings | 3 - apps/ios/zh-Hans.lproj/Localizable.strings | 3 - .../commonMain/resources/MR/ar/strings.xml | 2 +- .../commonMain/resources/MR/cs/strings.xml | 18 ++++- .../commonMain/resources/MR/de/strings.xml | 2 +- .../commonMain/resources/MR/es/strings.xml | 6 +- .../commonMain/resources/MR/fa/strings.xml | 1 + .../commonMain/resources/MR/hu/strings.xml | 40 +++++----- .../commonMain/resources/MR/in/strings.xml | 73 ++++++++++++++++--- .../commonMain/resources/MR/it/strings.xml | 2 +- .../commonMain/resources/MR/ro/strings.xml | 10 +-- .../commonMain/resources/MR/ru/strings.xml | 20 ++--- .../resources/MR/zh-rCN/strings.xml | 2 +- 41 files changed, 233 insertions(+), 205 deletions(-) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 32d32aeb67..9ae115b867 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -4645,6 +4645,10 @@ This is your link for group %@! Член No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission No comment provided by engineer. @@ -5613,11 +5617,6 @@ Requires compatible VPN. Парола за показване No comment provided by engineer. - - Past member %@ - Бивш член %@ - past/unknown group member - Paste desktop address Постави адрес на настолно устройство diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 4bfe80e388..f94bc682e1 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -4472,6 +4472,10 @@ This is your link for group %@! Člen No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission No comment provided by engineer. @@ -5412,10 +5416,6 @@ Vyžaduje povolení sítě VPN. Heslo k zobrazení No comment provided by engineer. - - Past member %@ - past/unknown group member - Paste desktop address No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 4db5d3a826..46d854b31e 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -4892,6 +4892,10 @@ Das ist Ihr Link für die Gruppe %@! Mitglied No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission Aufnahme von Mitgliedern @@ -5949,11 +5953,6 @@ Dies erfordert die Aktivierung eines VPNs. Passwort anzeigen No comment provided by engineer. - - Past member %@ - Ehemaliges Mitglied %@ - past/unknown group member - Paste desktop address Desktop-Adresse einfügen diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 849e75a6ec..a4c4cdecf4 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -4892,6 +4892,11 @@ This is your link for group %@! Member No comment provided by engineer. + + Member %@ + Member %@ + past/unknown group member + Member admission Member admission @@ -5949,11 +5954,6 @@ Requires compatible VPN. Password to show No comment provided by engineer. - - Past member %@ - Past member %@ - past/unknown group member - Paste desktop address Paste desktop address diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 1bcda9cc39..ca7a31d5c3 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -4892,6 +4892,10 @@ This is your link for group %@! Miembro No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission Admisión de miembros @@ -5949,11 +5953,6 @@ Requiere activación de la VPN. Contraseña para hacerlo visible No comment provided by engineer. - - Past member %@ - Miembro pasado %@ - past/unknown group member - Paste desktop address Pegar dirección de ordenador diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 8ab115b001..f5b29745cc 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -4440,6 +4440,10 @@ This is your link for group %@! Jäsen No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission No comment provided by engineer. @@ -5378,10 +5382,6 @@ Edellyttää VPN:n sallimista. Salasana näytettäväksi No comment provided by engineer. - - Past member %@ - past/unknown group member - Paste desktop address No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 221618b691..27c73ddc54 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -4845,6 +4845,10 @@ Voici votre lien pour le groupe %@ ! Membre No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission No comment provided by engineer. @@ -5863,11 +5867,6 @@ Nécessite l'activation d'un VPN. Mot de passe à entrer No comment provided by engineer. - - Past member %@ - Ancien membre %@ - past/unknown group member - Paste desktop address Coller l'adresse du bureau diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index fd2412e0ec..71bf7d382b 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -1209,7 +1209,7 @@ swipe action Bad message hash - Érvénytelen az üzenet hasítóértéke + Érvénytelen az üzenet kivonata No comment provided by engineer. @@ -1264,12 +1264,12 @@ swipe action Bio - Bemutatkozás + Névjegy No comment provided by engineer. Bio too large - A bemutatkozás túl hosszú + A névjegy túl hosszú alert title @@ -2222,7 +2222,7 @@ Ez a saját egyszer használható meghívója! Create queue - Sorba állítás létrehozása + Várólista létrehozása server test step @@ -2621,7 +2621,7 @@ swipe action Delete queue - Sorba állítás törlése + Várólista törlése server test step @@ -4647,12 +4647,12 @@ További fejlesztések hamarosan! Join group - Csatlakozás csoporthoz + Csatlakozás a csoporthoz new chat sheet title Join group conversations - Csatlakozás csoportos beszélgetésekhez + Csatlakozás a csoportbeszélgetésekhez No comment provided by engineer. @@ -4892,6 +4892,10 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Tag No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission Tagbefogadás @@ -5029,7 +5033,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Message queue info - Üzenetsorbaállítási információ + Üzenet várólista információi No comment provided by engineer. @@ -5949,11 +5953,6 @@ VPN engedélyezése szükséges. Jelszó a megjelenítéshez No comment provided by engineer. - - Past member %@ - (Már nem tag) %@ - past/unknown group member - Paste desktop address Számítógép címének beillesztése @@ -7030,7 +7029,7 @@ chat item action Secure queue - Biztonságos sorba állítás + Biztonságos várólista server test step @@ -7125,7 +7124,7 @@ chat item action Send link previews - Hivatkozás előnézete + Hivatkozások előnézetének megjelenítése No comment provided by engineer. @@ -7335,7 +7334,7 @@ chat item action Server requires authorization to create queues, check password - A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát + A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát server test error @@ -7440,7 +7439,7 @@ chat item action Set profile bio and welcome message. - Bemutatkozószöveg és üdvözlőüzenet beállítása a profilokhoz. + Névjegy és üdvözlőüzenet beállítása a profilokhoz. No comment provided by engineer. @@ -7581,7 +7580,7 @@ chat item action Show last messages - Legutóbbi üzenet előnézetének megjelenítése + Legutóbbi üzenetek előnézetének megjelenítése No comment provided by engineer. @@ -7596,7 +7595,7 @@ chat item action Show preview - Értesítés előnézete + Értesítések előnézetének megjelenítése No comment provided by engineer. @@ -8146,7 +8145,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. The hash of the previous message is different. - Az előző üzenet hasítóértéke különbözik. + Az előző üzenet kivonata különbözik. No comment provided by engineer. @@ -9704,7 +9703,7 @@ Megismétli a kapcsolódási kérést? bad message hash - hibás az üzenet hasítóértéke + hibás az üzenet kivonata integrity error chat item @@ -10449,14 +10448,14 @@ time to disappear security code changed - a biztonsági kód módosult + biztonsági kódja módosult chat item text server queue info: %1$@ last received msg: %2$@ - a kiszolgáló sorbaállítási információi: %1$@ + a kiszolgáló várólista információi: %1$@ utoljára fogadott üzenet: %2$@ queue info diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index ad0db1ad18..af5fced837 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -4892,6 +4892,10 @@ Questo è il tuo link per il gruppo %@! Membro No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission Ammissione dei membri @@ -5949,11 +5953,6 @@ Richiede l'attivazione della VPN. Password per mostrare No comment provided by engineer. - - Past member %@ - Membro passato %@ - past/unknown group member - Paste desktop address Incolla l'indirizzo desktop diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 2e71c2b37a..ec3f33433d 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -612,6 +612,7 @@ swipe action Acknowledged + 了承済み No comment provided by engineer. @@ -4521,6 +4522,10 @@ This is your link for group %@! メンバー No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission No comment provided by engineer. @@ -5463,10 +5468,6 @@ VPN を有効にする必要があります。 パスワードを表示する No comment provided by engineer. - - Past member %@ - past/unknown group member - Paste desktop address No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 079e4a8775..83656910fe 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -4864,6 +4864,10 @@ Dit is jouw link voor groep %@! Lid No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission Toelating van leden @@ -5906,11 +5910,6 @@ Vereist het inschakelen van VPN. Wachtwoord om weer te geven No comment provided by engineer. - - Past member %@ - Voormalig lid %@ - past/unknown group member - Paste desktop address Desktopadres plakken diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 0791436e94..6133961dfd 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -4766,6 +4766,10 @@ To jest twój link do grupy %@! Członek No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission No comment provided by engineer. @@ -5763,11 +5767,6 @@ Wymaga włączenia VPN. Hasło do wyświetlenia No comment provided by engineer. - - Past member %@ - Były członek %@ - past/unknown group member - Paste desktop address Wklej adres komputera diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 1bd12beddb..7afe44aa1f 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -4891,6 +4891,10 @@ This is your link for group %@! Член группы No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission Приём членов в группу @@ -5948,11 +5952,6 @@ Requires compatible VPN. Пароль чтобы раскрыть No comment provided by engineer. - - Past member %@ - Бывший член %@ - past/unknown group member - Paste desktop address Вставить адрес компьютера diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 7b4c27e61e..fbea584f76 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -4423,6 +4423,10 @@ This is your link for group %@! สมาชิก No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission No comment provided by engineer. @@ -5357,10 +5361,6 @@ Requires compatible VPN. รหัสผ่านที่จะแสดง No comment provided by engineer. - - Past member %@ - past/unknown group member - Paste desktop address No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index c06e95ecaf..870b633488 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -4884,6 +4884,10 @@ Bu senin grup için bağlantın %@! Kişi No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission Üye kabulü @@ -5935,11 +5939,6 @@ VPN'nin etkinleştirilmesi gerekir. Gösterilecek şifre No comment provided by engineer. - - Past member %@ - Geçmiş üye %@ - past/unknown group member - Paste desktop address Bilgisayar adresini yapıştır diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 10cb209a8b..16457e05cb 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -4884,6 +4884,10 @@ This is your link for group %@! Учасник No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission Прийом членів @@ -5935,11 +5939,6 @@ Requires compatible VPN. Показати пароль No comment provided by engineer. - - Past member %@ - Колишній учасник %@ - past/unknown group member - Paste desktop address Вставте адресу робочого столу diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index a92005b8b7..852d1f0583 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -4853,6 +4853,10 @@ This is your link for group %@! 成员 No comment provided by engineer. + + Member %@ + past/unknown group member + Member admission No comment provided by engineer. @@ -5888,11 +5892,6 @@ Requires compatible VPN. 显示密码 No comment provided by engineer. - - Past member %@ - 前任成员 %@ - past/unknown group member - Paste desktop address 粘贴桌面地址 diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 3cc59b898f..186d7e4224 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -2905,9 +2905,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Парола за показване"; -/* past/unknown group member */ -"Past member %@" = "Бивш член %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Постави адрес на настолно устройство"; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index b64b7b3ec4..40194684e6 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -3961,9 +3961,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Passwort anzeigen"; -/* past/unknown group member */ -"Past member %@" = "Ehemaliges Mitglied %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Desktop-Adresse einfügen"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 1158380c34..8b8331e097 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -3961,9 +3961,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Contraseña para hacerlo visible"; -/* past/unknown group member */ -"Past member %@" = "Miembro pasado %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Pegar dirección de ordenador"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 8f9be38180..09bebeddd9 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -3661,9 +3661,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Mot de passe à entrer"; -/* past/unknown group member */ -"Past member %@" = "Ancien membre %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Coller l'adresse du bureau"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index f87f19c586..a137d03d8c 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -777,10 +777,10 @@ swipe action */ "Bad desktop address" = "Érvénytelen számítógépcím"; /* integrity error chat item */ -"bad message hash" = "hibás az üzenet hasítóértéke"; +"bad message hash" = "hibás az üzenet kivonata"; /* No comment provided by engineer. */ -"Bad message hash" = "Érvénytelen az üzenet hasítóértéke"; +"Bad message hash" = "Érvénytelen az üzenet kivonata"; /* integrity error chat item */ "bad message ID" = "hibás az üzenet azonosítója"; @@ -819,10 +819,10 @@ swipe action */ "Better user experience" = "Továbbfejlesztett felhasználói élmény"; /* No comment provided by engineer. */ -"Bio" = "Bemutatkozás"; +"Bio" = "Névjegy"; /* alert title */ -"Bio too large" = "A bemutatkozás túl hosszú"; +"Bio too large" = "A névjegy túl hosszú"; /* No comment provided by engineer. */ "Black" = "Fekete"; @@ -1492,7 +1492,7 @@ set passcode view */ "Create profile" = "Profil létrehozása"; /* server test step */ -"Create queue" = "Sorba állítás létrehozása"; +"Create queue" = "Várólista létrehozása"; /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX-cím létrehozása"; @@ -1755,7 +1755,7 @@ swipe action */ "Delete profile" = "Profil törlése"; /* server test step */ -"Delete queue" = "Sorba állítás törlése"; +"Delete queue" = "Várólista törlése"; /* No comment provided by engineer. */ "Delete report" = "Jelentés törlése"; @@ -3102,10 +3102,10 @@ snd error text */ "Join as %@" = "csatlakozás mint %@"; /* new chat sheet title */ -"Join group" = "Csatlakozás csoporthoz"; +"Join group" = "Csatlakozás a csoporthoz"; /* No comment provided by engineer. */ -"Join group conversations" = "Csatlakozás csoportos beszélgetésekhez"; +"Join group conversations" = "Csatlakozás a csoportbeszélgetésekhez"; /* No comment provided by engineer. */ "Join incognito" = "Csatlakozás inkognitóban"; @@ -3351,7 +3351,7 @@ snd error text */ "Message may be delivered later if member becomes active." = "Az üzenet később is kézbesíthető, ha a tag aktívvá válik."; /* No comment provided by engineer. */ -"Message queue info" = "Üzenetsorbaállítási információ"; +"Message queue info" = "Üzenet várólista információi"; /* chat feature */ "Message reactions" = "Üzenetreakciók"; @@ -3961,9 +3961,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Jelszó a megjelenítéshez"; -/* past/unknown group member */ -"Past member %@" = "(Már nem tag) %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Számítógép címének beillesztése"; @@ -4691,7 +4688,7 @@ chat item action */ "secret" = "titok"; /* server test step */ -"Secure queue" = "Biztonságos sorba állítás"; +"Secure queue" = "Biztonságos várólista"; /* No comment provided by engineer. */ "Secured" = "Biztosítva"; @@ -4703,7 +4700,7 @@ chat item action */ "Security code" = "Biztonsági kód"; /* chat item text */ -"security code changed" = "a biztonsági kód módosult"; +"security code changed" = "biztonsági kódja módosult"; /* chat item action */ "Select" = "Kijelölés"; @@ -4751,7 +4748,7 @@ chat item action */ "Send errors" = "Üzenetküldési hibák"; /* No comment provided by engineer. */ -"Send link previews" = "Hivatkozás előnézete"; +"Send link previews" = "Hivatkozások előnézetének megjelenítése"; /* No comment provided by engineer. */ "Send live message" = "Élő üzenet küldése"; @@ -4877,10 +4874,10 @@ chat item action */ "Server protocol changed." = "A kiszolgálóprotokoll módosult."; /* queue info */ -"server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló sorbaállítási információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; +"server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló várólista információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát"; +"Server requires authorization to create queues, check password" = "A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát"; /* server test error */ "Server requires authorization to upload, check password" = "A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát"; @@ -4949,7 +4946,7 @@ chat item action */ "Set passphrase to export" = "Jelmondat beállítása az exportáláshoz"; /* No comment provided by engineer. */ -"Set profile bio and welcome message." = "Bemutatkozószöveg és üdvözlőüzenet beállítása a profilokhoz."; +"Set profile bio and welcome message." = "Névjegy és üdvözlőüzenet beállítása a profilokhoz."; /* No comment provided by engineer. */ "Set the message shown to new members!" = "Megjelenítendő üzenet beállítása az új tagok számára!"; @@ -5034,7 +5031,7 @@ chat item action */ "Show developer options" = "Fejlesztői beállítások megjelenítése"; /* No comment provided by engineer. */ -"Show last messages" = "Legutóbbi üzenet előnézetének megjelenítése"; +"Show last messages" = "Legutóbbi üzenetek előnézetének megjelenítése"; /* No comment provided by engineer. */ "Show message status" = "Üzenet állapotának megjelenítése"; @@ -5043,7 +5040,7 @@ chat item action */ "Show percentage" = "Százalék megjelenítése"; /* No comment provided by engineer. */ -"Show preview" = "Értesítés előnézete"; +"Show preview" = "Értesítések előnézetének megjelenítése"; /* No comment provided by engineer. */ "Show QR code" = "QR-kód megjelenítése"; @@ -5383,7 +5380,7 @@ report reason */ "The future of messaging" = "Az üzenetváltás jövője"; /* No comment provided by engineer. */ -"The hash of the previous message is different." = "Az előző üzenet hasítóértéke különbözik."; +"The hash of the previous message is different." = "Az előző üzenet kivonata különbözik."; /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "A következő üzenet azonosítója érvénytelen (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index dc39cf9270..02920a4714 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -3961,9 +3961,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Password per mostrare"; -/* past/unknown group member */ -"Past member %@" = "Membro passato %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Incolla l'indirizzo desktop"; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 818b2eb1ce..b6c658c7f6 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -371,6 +371,9 @@ swipe action */ /* No comment provided by engineer. */ "Accepted conditions" = "承諾された条件"; +/* No comment provided by engineer. */ +"Acknowledged" = "了承済み"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 86bd6d4dbf..147b12d7ab 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -3826,9 +3826,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Wachtwoord om weer te geven"; -/* past/unknown group member */ -"Past member %@" = "Voormalig lid %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Desktopadres plakken"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index a3847f802f..3a706400e0 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -3373,9 +3373,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Hasło do wyświetlenia"; -/* past/unknown group member */ -"Past member %@" = "Były członek %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Wklej adres komputera"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index f3b88969c9..e3188c762f 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -3961,9 +3961,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Пароль чтобы раскрыть"; -/* past/unknown group member */ -"Past member %@" = "Бывший член %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Вставить адрес компьютера"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index e4285f12ba..b91b6e3dd1 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -3919,9 +3919,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Gösterilecek şifre"; -/* past/unknown group member */ -"Past member %@" = "Geçmiş üye %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Bilgisayar adresini yapıştır"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index c4efd9513a..bf8966bd08 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -3919,9 +3919,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "Показати пароль"; -/* past/unknown group member */ -"Past member %@" = "Колишній учасник %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "Вставте адресу робочого столу"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 6ba3c08be2..afd9ac8dc0 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -3739,9 +3739,6 @@ new chat action */ /* No comment provided by engineer. */ "Password to show" = "显示密码"; -/* past/unknown group member */ -"Past member %@" = "前任成员 %@"; - /* No comment provided by engineer. */ "Paste desktop address" = "粘贴桌面地址"; diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index e741d4a188..c2096dea57 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -1556,7 +1556,7 @@ يحتوي سطح المكتب على رمز دعوة خاطئ سطح المكتب مشغول يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين - العضو السابق %1$s + العضو %1$s وظيفة بطيئة خيارات المطور تغيّر العضو %1$s إلى %2$s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index c369ab6b8d..76172ccab8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -254,7 +254,7 @@ Hovory na uzamčené obrazovce: Otevřete SimpleX Chat pro přijetí hovoru Povolte volání ze zamčené obrazovky prostřednictvím Nastavení. - Otevřený + Otevřít Zvuk zapnut Reproduktor vypnut Reproduktor zapnut @@ -2450,4 +2450,20 @@ Načítání profilu… Člen je smazán - nemůže přijmout žádost Nová skupinová role: Moderátor + Zastaralé možnosti + Soubory a média jsou zakázány v tomto chatu. + Pouze vy můžete odesílat soubory a média. + Pouze vaše kontakty mohou posílat soubory a média. + Otevřít čistý odkaz + Otevřít celý odkaz + Otevřít pro přijetí + Otevřít pro připojení + Otevřít pro připojení + Otevřít pro použítí bota + Vypršel čas soukromého směrování + Odesílání souborů a médií zakázáno. + Vypršel čas protokolu na pozadí + Odmítnout žádost o kontakt + Odebrat sledování odkazů + Odstranit zprávy a blokovat členy. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 795441b079..e788e2fcc4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -1641,7 +1641,7 @@ Interner Fehler %s wird eine nicht unterstützte Version verwendet. Bitte stellen Sie sicher, dass beide Geräte die selbe Version nutzen]]> %s ist besetzt]]> - Ehemaliges Mitglied %1$s + Mitglied %1$s Die Ausführung dieser Funktion dauert zu lange: %1$d Sekunden: %2$s Langsame Funktion Langsame API-Aufrufe anzeigen diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 35452354a3..720be554b9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -1229,10 +1229,10 @@ Interfaz en japonés y portugués sin texto Han ocurrido algunos errores no críticos durante la importación: - ¿Cerrar\? + ¿Salir de SimpleX? APLICACIÓN Reiniciar - Cerrar + Salir Las notificaciones dejarán de funcionar hasta que vuelvas a iniciar la aplicación Desactivado Error al cancelar cambio de dirección @@ -1609,7 +1609,7 @@ Por favor, informa a los desarrolladores: \n%s Reiniciar chat - Miembro pasado %1$s + Miembro %1$s el miembro %1$s ha cambiado a %2$s dirección de contacto eliminada ha eliminado la imagen del perfil diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index fef8f7f12e..cbb4849067 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -2527,4 +2527,5 @@ فقط شما می‌توانید فایل و رسانه ارسال کنید. فقط مخاطب شما می‌تواند فایل‌ها و رسانه‌ها را ارسال کند. ارسال فایل‌ها و رسانه‌ها در این چت ممنوع است. + پل ارتباطی سیمپلکس diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 57a91d2c42..579bdf74db 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -43,7 +43,7 @@ Az összes csevegési profiljához az alkalmazásban külön TCP-kapcsolat (és SOCKS-hitelesítési adat) lesz használva.]]> Mindkét fél küldhet eltűnő üzeneteket. Az Android Keystore-t a jelmondat biztonságos tárolására használják – lehetővé teszi az értesítési szolgáltatás működését. - Hibás az üzenet hasítóértéke + Hibás az üzenet kivonata Háttér Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének küldése közvetlen kapcsolatot használ.]]> Alkalmazásadatok biztonsági mentése @@ -122,7 +122,7 @@ Élő csevegési üzenet visszavonása Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra) Hang- és videóhívások - hibás az üzenet hasítóértéke + hibás az üzenet kivonata Mindig fut Az Android Keystore biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat módosítása után – lehetővé teszi az értesítések fogadását. Az összes alkalmazásadat törölve. @@ -302,7 +302,7 @@ befejezett CSEVEGÉSI ADATBÁZIS Önmegsemmisítő jelkód módosítása - Sorba állítás létrehozása + Várólista létrehozása színezett kapcsolódás… Sötét téma @@ -322,7 +322,7 @@ Csoporthivatkozás létrehozása Csevegési konzol Fájlok törlése az összes csevegési profilból - Sorba állítás törlése + Várólista törlése Partner törlése cím módosítása… Társítva a hordozható eszközhöz @@ -710,7 +710,7 @@ Nagy fájl! Helyi név Hálózat és kiszolgálók - Értesítés előnézete + Értesítésekben megjelenő információk Társítsa össze a hordozható eszköz- és számítógépes alkalmazásokat! 🔗 közvetett (%1$s) Hamarosan további fejlesztések érkeznek! @@ -882,7 +882,7 @@ Megnyitás Protokoll időtúllépése titok - Értesítés előnézete + Értesítések előnézetének megjelenítése várakozás a visszaigazolásra… Fájl megállítása a csoporthivatkozáson keresztül @@ -904,7 +904,7 @@ Frissítés Kijelölés Csak Ön tud hívásokat indítani. - Biztonságos sorba állítás + Biztonságos várólista Értékelje az alkalmazást Egyszer használható meghívó megosztása Hiba történt az adatbázis visszaállításakor @@ -931,8 +931,8 @@ Új inkognitóprofil használata Frissítse az alkalmazást, és lépjen kapcsolatba a fejlesztőkkel. SimpleX - Hivatkozás előnézete - a biztonsági kód módosult + Hivatkozások előnézetének megjelenítése + biztonsági kódja módosult Csak a partner nevének megjelenítése Hangszóró bekapcsolva Importált csevegési adatbázis használatához indítsa újra az alkalmazást. @@ -1134,7 +1134,7 @@ Csoport megnyitása Elküldve A hangüzenetek küldése le van tiltva. - Legutóbbi üzenet előnézetének megjelenítése + Legutóbbi üzenetek előnézetének megjelenítése Az előre beállított kiszolgáló címe Időszakos értesítések letiltva! A jelkód módosult! @@ -1305,7 +1305,7 @@ Az alkalmazás 1 perc után bezárható a háttérben. Ön meghívást kapott a csoportba Engedélyezze a következő párbeszédpanelen az azonnali értesítések fogadásához.]]> - A kiszolgálónak engedélyre van szüksége a sorba állítás létrehozásához, ellenőrizze a jelszavát + A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát Kapcsolódni fog a csoport összes tagjához. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen A biztonsága érdekében kapcsolja be a SimpleX-zár funkciót.\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. @@ -1475,7 +1475,7 @@ Használati útmutatóban olvasható.]]> A jelmondat a beállításokban egyszerű szövegként van tárolva. Konzol megjelenítése új ablakban - Az előző üzenet hasítóértéke különbözik. + Az előző üzenet kivonata különbözik. Ezek a beállítások csak a jelenlegi csevegési profiljára vonatkoznak Várjon, amíg a fájl betöltődik a társított hordozható eszközről GitHub-tárolónkban.]]> @@ -1543,7 +1543,7 @@ Fejlesztői beállítások A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s %s hordozható eszköz elfoglalt]]> - (Már nem tag) %1$s + %1$s tag ismeretlen állapot %1$s a következőre módosította a nevét: %2$s eltávolította a kapcsolattartási címet @@ -1561,7 +1561,7 @@ Létrehozva: %s Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Továbbfejlesztett üzenetkézbesítés - Csatlakozás csoportos beszélgetésekhez + Csatlakozás a csoportbeszélgetésekhez Hivatkozás beillesztése a kapcsolódáshoz! Privát jegyzetek A keresősáv elfogadja a meghívási hivatkozásokat. @@ -1799,10 +1799,10 @@ Csökkentett akkumulátor-használattal. Hiba történt a WebView előkészítésekor. Frissítse rendszerét az új verzióra. Lépjen kapcsolatba a fejlesztőkkel.\nHiba: %s Felhasználó által létrehozott téma visszaállítása - Üzenetsorbaállítási információ + Üzenet várólista információi nincs Kézbesítési hibák felderítése - a kiszolgáló sorbaállítási információi: %1$s\n\nutoljára fogadott üzenet: %2$s + a kiszolgáló várólista információi: %1$s\n\nutoljára fogadott üzenet: %2$s Érvénytelen kulcs vagy ismeretlen fájltöredékcím – valószínűleg a fájl törlődött. Ideiglenes fájlhiba Üzenet állapota @@ -2450,9 +2450,9 @@ TCP-kapcsolat időtúllépése a háttérben Profil betöltése… Rövid leírás: - Bemutatkozás: - Bemutatkozás: - A bemutatkozás túl hosszú + Saját névjegy: + Névjegy: + A névjegy túl hosszú A leírás túl hosszú Partneri kapcsolatkérés elfogadása Üzleti kapcsolat @@ -2468,7 +2468,7 @@ Saját cím létrehozása Eltűnő üzenetek engedélyezése alapértelmezetten. Tartsa tisztán a csevegéseit - Bemutatkozószöveg és üdvözlőüzenet beállítása a profilokhoz. + Névjegy és üdvözlőüzenet beállítása a profilokhoz. Saját cím megosztása Rövid SimpleX-cím Cím frissítése diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 95ad71a47d..917133829c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -111,7 +111,7 @@ Lokasi file tidak valid Tampilan macet Anda membagikan lokasi file yang tidak valid. Laporkan masalah ini ke pengembang aplikasi. - error + galat Tautan sekali %1$d pesan yang terlewati %1$d pesan yang dilewati @@ -341,7 +341,7 @@ %1$d berkas lainnya gagal. Kontak sudah ada menghubungkan… - kirim pesan langsung + kirim untuk terhubung Anda tidak memiliki obrolan Memuat obrolan… Ketuk untuk Hubungkan @@ -474,7 +474,7 @@ Izin Anda Izin kontak bawaan (%s) - Pesan sementara + Pesan menghilang Pesan pribadi Pesan suara Setel preferensi grup @@ -664,7 +664,7 @@ Kirim pesan suara tidak diizinkan. Hingga 100 pesan terakhir dikirim ke anggota baru. Pesan suara - Pesan sementara + Pesan menghilang Pesan langsung Profil obrolan tersembunyi Moderasi grup @@ -793,7 +793,7 @@ Teks ini tersedia di pengaturan Ketuk untuk memulai obrolan baru Anda diundang ke grup - gabung sebagai %s + Gabung sebagai %s menghubungkan… Tidak dapat kirim pesan Bagikan pesan… @@ -1339,7 +1339,7 @@ Ketentuan diterima pada: %s. Ketentuan akan diterima pada: %s. Koneksi - Rol + Peran Ganti rol Profil obrolan Anda akan dikirim ke anggota grup Profil obrolan Anda akan dikirim ke anggota obrolan @@ -1359,7 +1359,7 @@ kode keamanan berubah %d kontak dipilih Tautan grup - Anggota lama %1$s + Anggota %1$s enkripsi end-to-end standar enkripsi e2e quantum resistant menghubungkan (undangan perkenalan) @@ -1393,7 +1393,7 @@ Ketentuan akan diterima untuk operator yang diaktifkan setelah 30 hari. Alat pengembang Warna obrolan - terhubung langsung + permintaan koneksi Koneksi ke desktop dalam kondisi buruk Konfirmasi hapus kontak? Waktu kustom @@ -1881,7 +1881,7 @@ Alamat penerima akan diubah ke server lain. Perubahan alamat akan selesai setelah pengirim online. Minta kontak Anda untuk aktifkan kirim pesan suara. gambar profil - Simpan pengaturan terima otomatis + Simpan pengaturan alamat SimpleX Catatan diperbarui pada: %s dtk Alamat desktop salah @@ -2423,4 +2423,59 @@ enkripsi end-to-end.]]> hanya setelah permintaan Anda diterima.]]> Obrolan dengan admin + Chat dengan anggota sebelum mereka bergabung. + Hubungkan + Terhubung lebih cepat! 🚀 + PERMINTAAN KONTAK DARI GRUP + kontak harus menerima… + Buat alamat Anda + Opsi tidak berlaku + Deskripsi terlalu panjang + Aktifkan pesan menghilang secara default. + Gagal mengubah profil + Gagal membuka obrolan + Gagal membuka grup + Gagal menolak permintaan kontak + Berkas dan media dilarang dalam obrolan ini. + Grup + Gabung grup + Membersihkan obrolan Anda + Lalu lintas jaringan seluler lebih sedikit. + Memuat profil… + Anggota dihapus - tidak dapat menerima permintaan + Kirim pesan instan setelah tekan Hubungkan. + Peran grup baru: Moderator + Tidak ada sesi routing pribadi + Hanya Anda yang dapat kirim berkas dan media. + Hanya kontak Anda yang dapat kirim berkas dan media. + Buka obrolan + Buka tautan bersih + Buka tautan + Buka obrolan baru + Buka grup baru + Buka untuk terima + Buka untuk hubungkan + Buka untuk gabung + Buka untuk gunakan bot + Routing pribadi terlalu lama + Larang mengirim berkas dan media. + Latar belakang protokol terlalu lama + Tolak permintaan kontak + Hapus tautan pelacak + Menghapus pesan dan memblokir anggota. + permintaan koneksi dari grup %1$s + permintaan dikirim + Tinjau anggota grup + Kirim permintaan kontak? + Kirim permintaan + Kirim permintaan tanpa pesan + Kirimkan masukan pribadi Anda ke grup. + Dikirim ke kontak Anda setelah terhubung. + Pasang bio profil dan pesan sambutan. + Bagikan alamat lama + Bagikan tautan lama + Bagikan alamat Anda + Deskripsi singkat: + Alamat singkat SimpleX + Tautan relay SimpleX diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 8c2c4cfed1..65423f1bbd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1569,7 +1569,7 @@ Il desktop è inattivo Il desktop è stato disconnesso Tempo scaduto durante la connessione al desktop - Membro passato %1$s + Membro %1$s L\'esecuzione della funzione impiega troppo tempo: %1$d secondi: %2$s Funzione lenta Mostra chiamate API lente diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index 585b967613..3ba57571c7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -697,7 +697,7 @@ Estompează pentru o mai bună confidențialitate. Statusul conexiunii și al serverelor. Eroare bază de date - Contactele pot marca mesaje pentru ștergere; dvs. le veți putea vizualiza. + Contactele pot marca mesajele pentru ștergere; tu le vei putea vizualiza. Downgrade al bazei de date Migrarea bazei de date este în curs.\nAceasta poate dura câteva minute. Parola bazei de date și export @@ -1170,7 +1170,7 @@ Nou rol de membru Invitați membrii Editează profilul grupului - Grupul va fi șters pentru dvs. - această acțiune nu poate fi anulată! + Grupul va fi șters doar pentru tine – această acțiune nu poate fi anulată! Membrul va fi eliminat din grup - acest lucru nu poate fi anulat! Membrii vor fi eliminați din chat - acest lucru nu poate fi anulat! Membrii vor fi eliminați din grup - acest lucru nu poate fi anulat! @@ -1267,7 +1267,7 @@ Fără parolă pentru aplicație Favorite Fișier salvat - Fișierul va fi primit când contactul dvs. va fi online, vă rugăm să așteptați sau să verificați mai târziu! + Fișierul va fi primit când contactul tău este online. Te rugăm să aștepți sau să verifici mai târziu! niciun detaliu Deschide cu %s Link de invitație unic @@ -1365,7 +1365,7 @@ Eroare la încărcarea detaliilor Eroare la ștergerea profilului de utilizator Descărcare - NU trimiteți mesaje direct, chiar dacă serverul dvs. sau serverul de destinație nu acceptă rutare privată. + NU trimite mesaje direct, chiar dacă serverul tău sau cel de destinație nu acceptă rutare privată. Se redirecționează %1$s mesaje grupul este șters nesincronizat @@ -2353,7 +2353,7 @@ Puteți vizualiza rapoartele dvs. în Chat cu administratorii. Schimbați ordinea Vei fi conectat(ă) atunci când dispozitivul contactului tău este online, te rugăm să aștepți sau să verifici mai târziu! - scana codul QR în apelul video sau contactul dvs. poate partaja un link de invitație.]]> + scana codul QR în timpul apelului video sau contactul tău poate partaja un link de invitație.]]> Profilul tău %1$s va fi distribuit. Nu îți vei pierde contactele dacă ulterior îți ștergi adresa. Securitatea conexiunii diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 7351687957..329e75c681 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -267,7 +267,7 @@ Нажмите кнопку сверху, затем: Чтобы соединиться через ссылку - Если Вы получили ссылку с приглашением из SimpleX Chat, Вы можете открыть ее в браузере: + Если Вы получили ссылку с приглашением из SimpleX Chat, Вы можете открыть её в браузере: Сканировать QR-код.]]> Open in mobile app на веб странице, затем нажмите Соединиться в приложении.]]> @@ -1608,7 +1608,7 @@ Камера недоступна Код доступа в приложение Добавить контакт: создать новую ссылку-приглашение или подключиться через полученную ссылку.]]> - Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата. + Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите её обратно до запуска чата. Соединение остановлено Создано Оставить @@ -1678,7 +1678,7 @@ Или отсканируйте QR-код Вы можете увидеть ссылку-приглашение снова открыв соединение. Показывать медленные вызовы API - Бывший член %1$s + Член группы %1$s Сохранённое сообщение неизвестно неизвестный статус @@ -1753,7 +1753,7 @@ Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений. не должны использовать одну и ту же базу данных на двух устройствах.]]> Проверьте подключение к Интернету и повторите попытку - Подтвердите, что Вы помните пароль базы данных для ее миграции. + Подтвердите, что Вы помните пароль базы данных для её миграции. Проверка пароля базы данных Проверить пароль Ошибка подтверждения пароля: @@ -2138,7 +2138,7 @@ Сохранение %1$s сообщений Убедитесь, что конфигурация прокси правильная. Аутентификация прокси - Использовать случайные учетные данные + Использовать случайные учётные данные Режим системы Ошибка пересылки сообщений %1$d ошибок файлов:\n%2$s @@ -2184,13 +2184,13 @@ Переключайте профиль чата для одноразовых приглашений. Аудит SimpleX протоколов от Trail of Bits. Чтобы совершать звонки, разрешите использовать микрофон. Завершите вызов и попробуйте позвонить снова. - Не использовать учетные данные с прокси. + Не использовать учётные данные с прокси. Ошибка сохранения прокси Пароль - Использовать разные учетные данные прокси для каждого соединения. - Использовать разные учетные данные прокси для каждого профиля. + Использовать разные учётные данные прокси для каждого соединения. + Использовать разные учётные данные прокси для каждого профиля. Имя пользователя - Ваши учетные данные могут быть отправлены в незашифрованном виде. + Ваши учётные данные могут быть отправлены в незашифрованном виде. Удалить архив? Загруженный архив базы данных будет навсегда удален с серверов. Принятые условия @@ -2348,7 +2348,7 @@ Все чаты будут удалены из списка %s, а сам список удален Добавить список Примечания - Открыто с %s + Открыть в %s Создать список Добавить в список Изменить список diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index fe2dce437b..a72d95c3b1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -1557,7 +1557,7 @@ 显示名无效。请另选一个名称。 慢函数 显示缓慢的 API 调用 - 过往成员 %1$s + 成员 %1$s 未知 未知状态 开发者选项 From 98b11b3df1f80bedf7ec550f06c9e6f1dbcfbed6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 27 Aug 2025 19:10:48 +0100 Subject: [PATCH 013/137] ios: update core library --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 0ce1c25f88..f9c389da26 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.1-AqSxSIb0U5QHvxr03mY9UK.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */, ); path = Libraries; sourceTree = ""; From d1d2e4bc185ef930c1f615d54e57be86e43eab4a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 27 Aug 2025 19:54:53 +0100 Subject: [PATCH 014/137] android: hold wakelock for 30 seconds after received event was processed, to prevent race condition with incoming call activity. (#6224) * android: hold wakelock for 45 seconds after event is received * create new lock instances, delay release by 30 seconds * comment --- .../simplex/common/platform/SimplexService.android.kt | 7 +++++-- .../kotlin/chat/simplex/common/model/SimpleXAPI.kt | 9 ++++++++- .../simplex/common/platform/SimplexService.desktop.kt | 5 +---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/SimplexService.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/SimplexService.android.kt index 14e4134010..3ccdcecab0 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/SimplexService.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/SimplexService.android.kt @@ -12,8 +12,11 @@ actual fun getWakeLock(timeout: Long): (() -> Unit) { } } return { - wakeLock?.release() - wakeLock = null + val lock = wakeLock + if (lock != null) { + if (lock.isHeld) lock.release() + wakeLock = null + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 1b0714dd9d..9b60a968b7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -653,7 +653,14 @@ object ChatController { break } try { - releaseLock() + val release = releaseLock + // delaying the release of wake lock in order to: + // 1. avoid race condition with the incoming call activity that fails to show if called after wake lock release (primary reason), + // 2. allow any other necessary processing for a bit of time with wakelock held. + launch { + delay(30000) + release() + } val msg = recvMsg(ctrl) releaseLock = getWakeLock(timeout = 60000) if (msg != null) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/SimplexService.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/SimplexService.desktop.kt index a9d411916c..d29871f842 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/SimplexService.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/SimplexService.desktop.kt @@ -1,6 +1,3 @@ package chat.simplex.common.platform -actual fun getWakeLock(timeout: Long): (() -> Unit) { - return {} -} - +actual fun getWakeLock(timeout: Long): (() -> Unit) = {} From 3a52fbf3e325252f3eac86fe848a3eb4b62122f9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 27 Aug 2025 21:19:02 +0100 Subject: [PATCH 015/137] 6.4.4: ios 297, android 317, desktop 119 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 20 ++++++++++---------- apps/multiplatform/gradle.properties | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index f9c389da26..7a7f1e481d 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -2003,7 +2003,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2053,7 +2053,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2095,7 +2095,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2115,7 +2115,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2140,7 +2140,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2177,7 +2177,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2214,7 +2214,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2265,7 +2265,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2316,7 +2316,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2350,7 +2350,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 296; + CURRENT_PROJECT_VERSION = 297; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 02e2e78729..02acc2abb4 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.4.4-beta.0 -android.version_code=316 +android.version_name=6.4.4 +android.version_code=317 android.bundle=false -desktop.version_name=6.4.4-beta.0 -desktop.version_code=118 +desktop.version_name=6.4.4 +desktop.version_code=119 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 From a66cad494df4511d13cd97dad9f8784bd97c1f1b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 28 Aug 2025 07:13:52 +0100 Subject: [PATCH 016/137] rfc: chat widgets and activities approach (#6169) * rfc: chat widgets and activities approach * Narasimha sc patch 17 (#6171) * docs: Update FAQ * Update FAQ.md * update --------- Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> * widget syntax/examples * update --------- Co-authored-by: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> --- docs/rfcs/2025-08-09-chat-widgets.md | 197 ++++++++++++++++++ .../2025-08-09-widget-state-machine.mmd | 30 +++ .../2025-08-09-widget-state-machine.svg | 1 + 3 files changed, 228 insertions(+) create mode 100644 docs/rfcs/2025-08-09-chat-widgets.md create mode 100644 docs/rfcs/diagrams/2025-08-09-widget-state-machine.mmd create mode 100644 docs/rfcs/diagrams/2025-08-09-widget-state-machine.svg diff --git a/docs/rfcs/2025-08-09-chat-widgets.md b/docs/rfcs/2025-08-09-chat-widgets.md new file mode 100644 index 0000000000..6e9a13737c --- /dev/null +++ b/docs/rfcs/2025-08-09-chat-widgets.md @@ -0,0 +1,197 @@ +# Chat widgets and activities + +## Problems + +A short-term problem is to decide and implement support for bots in the apps UI. Just released v6.4.3 includes support for bot commands, but the most commonly used UI approach for Telegram bots is inline buttons. + +They are, effectively, simple widgets constructed as multiple lines of buttons. While currently Telegram offers web apps for bots, they are much more complex to develop for both ourselves and for bot owners, and they have many issues with security model. + +Inline buttons "pros": +- super simple to develop for bot owners - they are just a 2D array of buttons with configuration, where a button can send a visible or invisible message to a bot, which in turn can update buttons however it wants. +- allow for quite advanced interfaces, with multiple layers of navigation. +- relatively simple to implement in the app. + +Inline buttons "cons": +- very bad visual design. +- very limiting, compared with full inline widgets. + +A longer-term problem is more advanced user activities with the bot and between each other, that could include: +- polls, +- "doodles" (see doodle.com), +- more advanced bot UIs, +- mini-games, +- etc. + +## Solution + +A general UX pattern that may solve both problems is "inline chat widgets". + +For the examples of the possible use cases for inline chat widgets see https://webxdc.org/apps/ + +Problems with web apps/webxdc. +- JavaScript/Web have large binary size, that is hard to justify unless an app is a browser as well (which is not impossible), +- JavaScript has a complex security model, +- Widget size are likely to be larger than our usual message size (~15kb after compression), so they have to be sent either as files or as multiple messages. + +Irrespective of what technology is used to implement widgets, there are likely to be two kinds of widgets: +- Bot UI. These widgets do not necessarily need to be "an activity" (see the next), as there is no much scrolling in the chat with the bot, but they may also benefit from being marked as "active" in the same way. +- Widgets sent by users. In this case widget once sent cannot be replaced, but it can react to events, both from the sender and from the recipients. In this case, widgets have to be linked with "chat activities", so they can be easily discovered and accessed from any place in the chat while they are active, without scrolling to the point where they were started, and with an additional message posted to the chat once they complete. + +This RFC describes both the widget security and execution model, and also a possible implementation approach. + +## Widget security and execution models + +Widgets are code that is sent by untrusted parties (or parties with the limited trust) to the users devices, so it should not be treated as trusted code. + +Rather than defining what widgets code should be prevented from doing, we should define what it can do, its execution model and lifecycle. + +At any point in time widget has: +- code - this is fixed, and cannot be changed. While this may seem as a limitation for inline buttons UI, but it can be overcome by putting the button definitions into the state, if that's what the bot wants. +- state - this is variable, and can be changed as described below. State can be anything, other than executable code, and it can be changed by events in the way widget code allows - including full state replacement (that would allow completely changing inline buttons) + +Widget can react to user and message, as shown on the diagram. + +![Widget events](./diagrams/2025-08-09-widget-state-machine.svg) + +WL stands for "Widget Library", more on that below. + +There are should be the following restrictions to widget events/processing: +- only user events (actions) can trigger sending messages, to prevent different instances of the same widget in the chat endlessly "talking" to each other. +- only one message from each remote chat peer can be sent "to the widget" to update its state. "To the widget" means that the message with event would reference the message with the widget by shared message ID that all peers in the chat have. If the widget already processed a state update it would update further state updates from this peer until user action is processed. +- once user performs some actions on the widget, further events can be processed from the same participants who previously sent events. + +This execution model prevents abuse when widget state update can be requested multiple times. At the same time, this execution model allows for all necessary interactions, including UI updates by bot in response to user actions, polls (each user would only be able to send one poll event), and two- or multi-party games where "moves" have to be made in turns - each client would know that it should not send any events until it receives moves from all parties, and other parties won't be sending events too. + +That all raises several questions: +- which layer should be enforcing this execution model. It can't be widget "code", as this is what we are defending from. It can be either core, or widget library, or maybe both. Probably enforcing it in widget library makes most sense. +- for direct chats, both peers can participate and send message events "to widget". While each party has its own instance of widget, with its own state, they would arrive to consistent state (not necessarily the same state, as it can be programmed to be different - as would be the case for games), once they process events. But for groups, there probably needs to be two options - 1. any party can participate (e.g., doodle or poll). 2. only pre-defined peers (by group member ID) can participate. This model works for business chat where bot, customer and multiple business agents can participate, but only customer and the bot would interact with the UI widget. 3. up to a certain number of peers can participate, but it's not defined in advance who they are. This model can work for multi-party games that can be entered by a certain number of members, but it's not defined in advance which ones. Once they enter, they would be fixed and will have to send events in turn. +- another question about execution model is access to any client data. On one hand, it may be used to improve user experience. On another hand, we have to ensure that data that is received from client device can participate in view computation, but cannot participate in computation of sent message. There have been ideas of data tainting (when each piece from client device is "tainted", and result of any computation where tainted data participates becomes tainted too, and tainted messages cannot be sent), but if we go this it has to be enforced outside of bot code, so that bot code cannot remove "tainting". Such client data could be dark or light color scheme, app and system language to localize UI, timezone, screen size. Asking user permission to access this data is a bad idea, as even if it is granted it can still be used to fingerprint users. + +## Proposed implementation model + +### Widget programming language + +Using JavaScript or any other traditional language is problematic, as they are all general purpose languages that cannot be sufficiently constrained to ensure that they comply with the execution model. Data tainting idea would be particularly hard to implement. One language that can achieve what is required is the language where code is data that can be analyzed, sanitized and constrained in its execution, and where tainted data cannot be untainted by untrusted code - Lisp. One variant of Lisp particularly stands out - PicoLisp, due to its simplicity, maturity and the existence of very advanced libraries. + +While in its current state [PicoLisp](https://picolisp.com/) can only be run as a standalone process, it is feasible to change to execute it as a library. Even though it is not multithreaded, it is not required as widget execution can be queued, and Lisp execution environment will be stateless - it receives widget state, participation state (who can participate and who already sent events - not in diagram) and events, computes new state, view and an optional message to send, and stops. In addition to that PicoLisp can be "hardened" to prevent it from crashing, from accessing files and sensors, etc., even in the library code, not only in widget code. + +To achieve the required security, it is might be that Widget code needs to be interpreted by Lisp library that in its turn needs to be interpreted by PicoLisp, but possibly there can be more efficient approaches for secure code execution - e.g., it could analyze some part of the expressions and decide if to continue execution on the boundaries of some functions (e.g., those that can be called recursively, to protect against endless recursion of widget code). + +The design of this "framework"/widget library is not sufficiently clear, and some bottom-up exploration is needed. + +### Widget UI rendering + +PicoLisp has libraries to render on canvas, HTML and SVG. + +We considered using a UI library to render UI primitives without them being part of widget code. One possible option is [Nuklear](https://immediate-mode-ui.github.io/Nuklear/) - a self-contained C library that renders UI elements with event handlers in a platform-independent way, without any specific platform adapters. It can either return its own commands or OpenGL instructions that can then be converted to bitmaps. + +A simpler option, particularly to get to MVP sooner, seems an ad-hoc rendering of buttons and UX elements either to SVG or to bitmap - TBC. + +### A possible example of widget definition for inline buttons + +Widget function definition: + +```lisp +( de Activity (widget state) () # library code implementing activity from its definition + # where + # - widget a function without parameters evaluating to (render receive version), where: + # - render: function defined as '((state0) (...)), and evaluating to view medium, + # whatever we choose to use - e.g., SVG or bitmap, and any activity attributes + # (e.g., name, icon, enabled status, duration, etc.), e.g. as (view activity'), + # or maybe be as a single object value. + # Any user event/gesture handlers would be defined as part of this function, + # as they should be local to UI definition, and probably the event handlers + # would be required to return (state1 view message), + # where message can be NIL or it will be sent as visible message or invisible event. + # - receive: function defined as '((sender message state0) (...)) that will be + # invoked when message to widget is received, and evaluate to (state1 view) + # - ver: minimal library version that the activity requires to work + # (library would expose supported version range). + # - state: the initial widget state, can be updated by event handlers and receive function + # We don't explicitly include participants as parameter, they can be included in state. +) +``` + +Inline buttons widget: + +```lisp +( Activity + # ButtonGrid is a pre-defined widget function that returns: + # - render: converts buttons state to view + # - receive: simply replaces state with the message content, e.g. `((_sender message _state) message)` + # - version: (version 1) + # Bundling these things together results in better UX for widget developers, allowing us + # (and widget developers) to supply a single pre-defined symbol that combines + # rendering, message handling and required library version + + ButtonGrid + + # widget state is buttons, so they can be updated by bot in response to user actions + ( ( ("Reply" (reply "message")) # visible reply message (replying to widget) + ("Menu" (event ":menu")) # invisible message + ("Help" (send "/help")) # visible message + ) # a row of 3 buttons + ( ("Site" (link "https://example.com)) + ("Call" (link "+447777777777")) + ("Email" (link "info@example.com")) + ("Connect" (link "https://smp5.simplex.im/a#abcd)) + ("Copy" (copy "text to copy)) + ) # a row of 5 buttons + ) +) +``` + +Poll widget: + +```lisp +( Activity + # Poll is a predefined widget function. + # Note that the question and the poll options are part of the widget function, + # so they cannot be changed via events. + # Its receive function updates counts. + ( Poll + "Do you agree?" # could be NIL, to put question in widget's message text + "Yes" + "No" + ) + + # initial widget state is poll counts, questions could not be changed + (0 0) + # but we probably can just default missing counts to 0s, + # so the initial state would be just () or NIL + # Or we could even use variable arguments to allow omitting empty list as the initial state. +) +``` + +If we can achieve that these functions (ButtonGrid, Poll) can be implemented in PicoLisp using the library, without any special hacks, so that they can be used as examples, and yet they would also would be available as predefined, it will be a sufficient confirmation that the framework does what we need it to do, +and can be used in more advanced scenarios. + +Also, given that widgets that use predefined functions can be very concise, as shown in above examples, they can be attached not only to text messages, but to images, videos and link previews. + +Even `Activity` itself doesn't need to be part of widget message, we can treat widget code as the list of parameters passed to activity, so the poll would be as simple as: + +```lisp +((Poll "Do you agree?" "Yes" "No")) # the second absent parameter is interpreted as empty initial state +``` + +and the button grid as: + +```lisp +( ButtonGrid + ( ( ("Reply" (reply "message")) + ("Menu" (event ":menu")) + ("Help" (send "/help")) + ) + ( ("Site" (link "https://example.com)) + ("Call" (link "+447777777777")) + ("Email" (link "info@example.com")) + ("Connect" (link "https://smp5.simplex.im/a#abcd)) + ("Copy" (copy "text to copy)) + ) + ) +) +``` + +with the buttons definitions (the second parameter) being the initial state, that allows to have them fully replaced via bot's message. + +This is somehow similar to PicoLisp design itself where three core types are derived from a single root type - cell, here we have two distinct UX problems - bot UI and user activities, such as polls, - implemented via the same underlying abstraction. diff --git a/docs/rfcs/diagrams/2025-08-09-widget-state-machine.mmd b/docs/rfcs/diagrams/2025-08-09-widget-state-machine.mmd new file mode 100644 index 0000000000..cae598a0fb --- /dev/null +++ b/docs/rfcs/diagrams/2025-08-09-widget-state-machine.mmd @@ -0,0 +1,30 @@ +sequenceDiagram + participant Contact + participant UI + participant Core + participant DB + participant WL + note over Contact, WL: 1. Receive and initialize widget + Contact->>Core: widget code and initial state + Core->>DB: persist message with widget + Core->>UI: new message
(with widget placeholder) + Core->>WL: widget code + state + WL->>Core: new state + view + Core->>DB: persist new state + Core->>UI: view + note over Contact, WL: 2. Process user event + UI->>Core: widget user event
(UI "gesture") + DB->>Core: get code + state + Core->>WL: user event + code + state + WL->>Core: updated state + view + optional message + Core->>DB: updated state + Core->>UI: new widget view
(non-optional, for visual feedback) + Core->>Contact: optional event message "from" widget + note over Contact, WL: 3. Process user event + Contact->>Core: Message with event for widget + Core->>DB: persist message (to resume/retry) + DB->>Core: get code + state + Core->>WL: message event + code + state + WL->>Core: updated state + view + Core->>DB: updated state + Core->>UI: new widget view
(plus some visual indication) diff --git a/docs/rfcs/diagrams/2025-08-09-widget-state-machine.svg b/docs/rfcs/diagrams/2025-08-09-widget-state-machine.svg new file mode 100644 index 0000000000..c8c9dd7bb4 --- /dev/null +++ b/docs/rfcs/diagrams/2025-08-09-widget-state-machine.svg @@ -0,0 +1 @@ +WLDBCoreUIContactWLDBCoreUIContact1. Receive and initialize widget2. Process user event3. Process user eventwidget code and initial statepersist message with widgetnew message(with widget placeholder)widget code + statenew state + viewpersist new stateviewwidget user event(UI "gesture")get code + stateuser event + code + stateupdated state + view + optional messageupdated statenew widget view(non-optional, for visual feedback)optional event message "from" widgetMessage with event for widgetpersist message (to resume/retry)get code + statemessage event + code + stateupdated state + viewupdated statenew widget view(plus some visual indication) \ No newline at end of file From e6e99ab7090e84689fed80ecc1956126e2e98bc6 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:26:11 +0000 Subject: [PATCH 017/137] flatpak: update metainfo (#6229) --- .../flatpak/chat.simplex.simplex.metainfo.xml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index 6b6af1032b..630b178aaf 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,27 @@ + + https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html + +

New in v6.4.4:

+
    +
  • reduced battery usage.
  • +
  • fixes.
  • +
+

New in v6.4-6.4.3.1:

+
    +
  • new UX to connect.
  • +
  • review new group members.
  • +
  • chat with group admins.
  • +
  • new UI languages: Catalan, Indonesian, Romanian and Vietnamese.
  • +
  • Linux app builds for aarch64 CPUs
  • +
  • UI support for bot commands.
  • +
  • support markdown hyperlinks, such as [click here](https://example.com).
  • +
  • option to remove tracking parameters from the links.
  • +
+
+
https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html From f6c699f3a56ec60fda3be048308ccaeccd6636cd Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 29 Aug 2025 10:05:20 +0100 Subject: [PATCH 018/137] core: update simplexmq (error handling) (#6231) * core: update simplexmq (error handling) * update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Archive.hs | 2 +- src/Simplex/Chat/Controller.hs | 46 +++------------ src/Simplex/Chat/Library/Commands.hs | 80 +++++++++++++------------- src/Simplex/Chat/Library/Internal.hs | 48 ++++++++-------- src/Simplex/Chat/Library/Subscriber.hs | 36 ++++++------ src/Simplex/Chat/Remote.hs | 26 +++------ src/Simplex/Chat/Remote/Types.hs | 4 ++ src/Simplex/Chat/Store/Shared.hs | 5 ++ 10 files changed, 110 insertions(+), 141 deletions(-) diff --git a/cabal.project b/cabal.project index 07a0b427e8..6618c088c5 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a2d777bda0af2a7ee7cd68952eaf7c86329427ad + tag: beafac1f73f7d61b99c79d609731f94a05ced97f source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 6558a66e31..25784e181c 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a2d777bda0af2a7ee7cd68952eaf7c86329427ad" = "04h8vdxf732jwsim2fcrql47gsmv680lgg2kylgmfk4al0pnpkdk"; + "https://github.com/simplex-chat/simplexmq.git"."beafac1f73f7d61b99c79d609731f94a05ced97f" = "11f1d0h5n7lkwsjz1csyxkrrcxqsyf2rv7339qcfbpdmqhq6k3ni"; "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"; diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index 2cbc941b44..b54c23959b 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -158,7 +158,7 @@ sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = D -- closing after encryption prevents closing in case wrong encryption key was passed liftIO $ closeDBStore `withStores` fs (moveExported `withStores` fs) - `catchChatError` \e -> (restore `withDBs` fs) >> throwError e + `catchAllErrors` \e -> (restore `withDBs` fs) >> throwError e where backup f = copyFile f (f <> ".bak") restore f = copyFile (f <> ".bak") f diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 53d3839f42..919274f4d2 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -19,7 +19,7 @@ module Simplex.Chat.Controller where import Control.Concurrent (ThreadId) import Control.Concurrent.Async (Async) -import Control.Exception (Exception, SomeException) +import Control.Exception (Exception) import qualified Control.Exception as E import Control.Monad.Except import Control.Monad.IO.Unlift @@ -88,7 +88,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), Msg import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, TransportPeer (..), simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) -import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>)) +import Simplex.Messaging.Util (AnyError (..), catchAllErrors, (<$$>)) import Simplex.RemoteControl.Client import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation) import Simplex.RemoteControl.Types @@ -1419,6 +1419,10 @@ data ArchiveError | AEFileError {file :: String, fileError :: String} deriving (Show, Exception) +instance AnyError ChatError where + fromSomeException = ChatError . CEException . show + {-# INLINE fromSomeException #-} + -- | Host (mobile) side of transport to process remote commands and forward notifications data RemoteCtrlSession = RCSessionStarting @@ -1505,46 +1509,10 @@ setContactNetworkStatus :: Contact -> NetworkStatus -> CM' () setContactNetworkStatus Contact {activeConn = Nothing} _ = pure () setContactNetworkStatus Contact {activeConn = Just Connection {agentConnId}} status = chatModifyVar' connNetworkStatuses $ M.insert agentConnId status -tryChatError :: CM a -> CM (Either ChatError a) -tryChatError = tryAllErrors mkChatError -{-# INLINE tryChatError #-} - -tryChatError' :: CM a -> CM' (Either ChatError a) -tryChatError' = tryAllErrors' mkChatError -{-# INLINE tryChatError' #-} - -catchChatError :: CM a -> (ChatError -> CM a) -> CM a -catchChatError = catchAllErrors mkChatError -{-# INLINE catchChatError #-} - -catchChatError' :: CM a -> (ChatError -> CM' a) -> CM' a -catchChatError' = catchAllErrors' mkChatError -{-# INLINE catchChatError' #-} - -chatFinally :: CM a -> CM b -> CM a -chatFinally = allFinally mkChatError -{-# INLINE chatFinally #-} - onChatError :: CM a -> CM b -> CM a -a `onChatError` onErr = a `catchChatError` \e -> onErr >> throwError e +a `onChatError` onErr = a `catchAllErrors` \e -> onErr >> throwError e {-# INLINE onChatError #-} -mkChatError :: SomeException -> ChatError -mkChatError = ChatError . CEException . show -{-# INLINE mkChatError #-} - -catchStoreError :: ExceptT StoreError IO a -> (StoreError -> ExceptT StoreError IO a) -> ExceptT StoreError IO a -catchStoreError = catchAllErrors mkStoreError -{-# INLINE catchStoreError #-} - -tryStoreError' :: ExceptT StoreError IO a -> IO (Either StoreError a) -tryStoreError' = tryAllErrors' mkStoreError -{-# INLINE tryStoreError' #-} - -mkStoreError :: SomeException -> StoreError -mkStoreError = SEInternalError . show -{-# INLINE mkStoreError #-} - throwCmdError :: String -> CM a throwCmdError = throwError . ChatError . CECommandError {-# INLINE throwCmdError #-} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 4ae58d8a29..b6b7dbe87c 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -230,7 +230,7 @@ startReceiveUserFiles :: User -> CM () startReceiveUserFiles user = do filesToReceive <- withStore' (`getRcvFilesToReceive` user) forM_ filesToReceive $ \ft -> - flip catchChatError eToView $ + flip catchAllErrors eToView $ toView =<< receiveFileEvt' user ft False Nothing Nothing restoreCalls :: CM' () @@ -300,7 +300,7 @@ handleCommandError a = runExceptT a `E.catches` ioErrors where ioErrors = [ E.Handler $ \(e :: ExitCode) -> E.throwIO e, - E.Handler $ pure . Left . mkChatError + E.Handler $ pure . Left . fromSomeException ] parseChatCommand :: ByteString -> Either String ChatCommand @@ -324,7 +324,7 @@ processChatCommand vr nm = \case user <- withFastStore $ \db -> do user <- createUserRecordAt db (AgentUserId auId) p True ts mapM_ (setUserServers db user ts) uss - createPresetContactCards db user `catchStoreError` \_ -> pure () + createPresetContactCards db user `catchAllErrors` \_ -> pure () createNoteFolder db user pure user atomically . writeTVar u $ Just user @@ -363,7 +363,7 @@ processChatCommand vr nm = \case chatWriteVar currentUser $ Just user'' pure $ CRActiveUser user'' SetActiveUser uName viewPwd_ -> do - tryChatError (withFastStore (`getUserIdByName` uName)) >>= \case + tryAllErrors (withFastStore (`getUserIdByName` uName)) >>= \case Left _ -> throwChatError CEUserUnknown Right userId -> processChatCommand vr nm $ APISetActiveUser userId viewPwd_ SetAllContactReceipts onOff -> withUser $ \_ -> withFastStore' (`updateAllContactReceipts` onOff) >> ok_ @@ -1101,7 +1101,7 @@ processChatCommand vr nm = \case where sendDelDeleteConns ct notify = do let doSendDel = contactReady ct && contactActive ct && notify - when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) + when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchAllErrors` const (pure ()) contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct) deleteAgentConnectionsAsync' contactConnIds doSendDel CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId $ do @@ -1123,7 +1123,7 @@ processChatCommand vr nm = \case when doSendDel . void $ sendGroupMessage' user gInfo recipients XGrpDel deleteGroupLinkIfExists user gInfo deleteMembersConnections' user members doSendDel - updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure () + updateCIGroupInvitationStatus user gInfo CIGISRejected `catchAllErrors` \_ -> pure () withFastStore' $ \db -> deleteGroupChatItems db user gInfo withFastStore' $ \db -> cleanupHostGroupLinkConn db user gInfo withFastStore' $ \db -> deleteGroupMembers db user gInfo @@ -1467,7 +1467,7 @@ processChatCommand vr nm = \case oldTTL = fromMaybe globalTTL oldTTL_ when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do lift $ setExpireCIFlag user False - expireChat user globalTTL `catchChatError` eToView + expireChat user globalTTL `catchAllErrors` eToView lift $ setChatItemsExpiration user globalTTL ttlCount ok user where @@ -1538,7 +1538,7 @@ processChatCommand vr nm = \case liftIO $ updateGroupSettings db user chatId chatSettings pure ms forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId -> - withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchChatError` eToView + withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchAllErrors` eToView ok user _ -> throwCmdError "not supported" APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do @@ -1829,7 +1829,7 @@ processChatCommand vr nm = \case case preparedContact of Nothing -> throwCmdError "contact doesn't have link to connect" Just PreparedContact {connLinkToConnect = ACCL SCMInvitation ccLink} -> do - (_, customUserProfile) <- connectViaInvitation user incognito ccLink (Just contactId) `catchChatError` \e -> do + (_, customUserProfile) <- connectViaInvitation user incognito ccLink (Just contactId) `catchAllErrors` \e -> do -- get updated contact, in case connection was started - in UI it would lock ability to change -- user or incognito profile for contact, in case server received request while client got network error ct' <- withFastStore $ \db -> getContact db vr user contactId @@ -1852,7 +1852,7 @@ processChatCommand vr nm = \case smId <- getSharedMsgId withFastStore' $ \db -> setRequestSharedMsgIdForContact db contactId smId pure (smId, mc) - r <- connectViaContact user (Just $ PCEContact ct) incognito ccLink welcomeSharedMsgId msg_ `catchChatError` \e -> do + r <- connectViaContact user (Just $ PCEContact ct) incognito ccLink welcomeSharedMsgId msg_ `catchAllErrors` \e -> do -- get updated contact, in case connection was started - in UI it would lock ability to change -- user or incognito profile for contact, in case server received request while client got network error ct' <- withFastStore $ \db -> getContact db vr user contactId @@ -1880,7 +1880,7 @@ processChatCommand vr nm = \case smId <- getSharedMsgId withFastStore' $ \db -> setRequestSharedMsgIdForGroup db groupId smId pure (smId, mc) - r <- connectViaContact user (Just $ PCEGroup gInfo hostMember) incognito connLinkToConnect welcomeSharedMsgId msg_ `catchChatError` \e -> do + r <- connectViaContact user (Just $ PCEGroup gInfo hostMember) incognito connLinkToConnect welcomeSharedMsgId msg_ `catchAllErrors` \e -> do -- get updated group info, in case connection was started (connLinkPreparedConnection) - in UI it would lock ability to change -- user or incognito profile for group or business chat, in case server received request while client got network error gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId @@ -1908,7 +1908,7 @@ processChatCommand vr nm = \case CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do - (ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e + (ccLink, plan) <- connectPlan user cLink `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e connectWithPlan user incognito ccLink plan Connect _ Nothing -> throwChatError CEInvalidConnReq APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do @@ -1919,14 +1919,14 @@ processChatCommand vr nm = \case (cReq, _cData) <- getShortLinkConnReq user sLnk pure $ CCLink cReq $ Just sLnk Nothing -> throwCmdError "no address in contact profile" - connectContactViaAddress user incognito ct ccLink `catchChatError` \e -> do + connectContactViaAddress user incognito ct ccLink `catchAllErrors` \e -> do -- get updated contact, in case connection was started - in UI it would lock ability to change incognito choice -- on next connection attempt, in case server received request while client got network error ct' <- withFastStore $ \db -> getContact db vr user contactId toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e ConnectSimplex incognito -> withUser $ \user -> do - plan <- contactRequestPlan user adminContactReq Nothing `catchChatError` const (pure $ CPContactAddress (CAPOk Nothing)) + plan <- contactRequestPlan user adminContactReq Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing)) connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing @@ -2200,12 +2200,12 @@ processChatCommand vr nm = \case -- MFAll is default for new groups unless (enableNtfs == MFAll) $ updateGroupSettings db user groupId chatSettings {enableNtfs} void (withAgent $ \a -> joinConnection a nm (aUserId user) agentConnId (enableNtfs /= MFNone) connRequest dm PQSupportOff subMode) - `catchChatError` \e -> do + `catchAllErrors` \e -> do withFastStore' $ \db -> do updateGroupMemberStatus db userId fromMember GSMemInvited updateGroupMemberStatus db userId membership GSMemInvited throwError e - updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` eToView + updateCIGroupInvitationStatus user g CIGISAccepted `catchAllErrors` eToView pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do @@ -2308,7 +2308,7 @@ processChatCommand vr nm = \case changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember]) changeRoleInvitedMems user gInfo memsToChange = do -- not batched, as we need to send different invitations to different connections anyway - mems_ <- forM memsToChange $ \m -> (Right <$> changeRole m) `catchChatError` (pure . Left) + mems_ <- forM memsToChange $ \m -> (Right <$> changeRole m) `catchAllErrors` (pure . Left) pure $ partitionEithers mems_ where changeRole :: GroupMember -> CM GroupMember @@ -2620,7 +2620,7 @@ processChatCommand vr nm = \case APIAcceptMemberContact contactId -> withUser $ \user -> do (g, mConn, ct, groupDirectInv) <- withFastStore $ \db -> getMemberContactInvited db vr user contactId when (groupDirectInvStartedConnection groupDirectInv) $ throwCmdError "connection already started" - connectMemberContact user g mConn ct groupDirectInv `catchChatError` \e -> do + connectMemberContact user g mConn ct groupDirectInv `catchAllErrors` \e -> do -- get updated contact, in case connection was started ct' <- withFastStore $ \db -> getContact db vr user contactId toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') @@ -3233,7 +3233,7 @@ processChatCommand vr nm = \case mergedProfile' = userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') False when (mergedProfile' /= mergedProfile) $ withContactLock "updateContactPrefs" (contactId' ct) $ do - void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchChatError` eToView + void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchAllErrors` eToView lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' pure $ CRContactPrefsUpdated user ct ct' runUpdateGroupProfile :: User -> Group -> GroupProfile -> CM ChatResponse @@ -3411,7 +3411,7 @@ processChatCommand vr nm = \case drgRandomBytes n = asks random >>= atomically . C.randomBytes n privateGetUser :: UserId -> CM User privateGetUser userId = - tryChatError (withStore (`getUser` userId)) >>= \case + tryAllErrors (withStore (`getUser` userId)) >>= \case Left _ -> throwChatError CEUserUnknown Right user -> pure user validateUserPassword :: User -> User -> Maybe UserPwd -> CM () @@ -3452,7 +3452,7 @@ processChatCommand vr nm = \case filesInfo <- withFastStore' (`getUserFileInfo` user) deleteCIFiles user filesInfo withAgent (\a -> deleteUser a (aUserId user) delSMPQueues) - `catchChatError` \case + `catchAllErrors` \case e@(ChatErrorAgent NO_USER _) -> eToView e e -> throwError e withFastStore' (`deleteUserRecord` user) @@ -3491,11 +3491,11 @@ processChatCommand vr nm = \case -- deleted contact is returned as known, as invitation link cannot be re-used too connect anyway Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db vr user l' invitationReqAndPlan cReq sLnk_ contactSLinkData_ = do - plan <- invitationRequestPlan user cReq contactSLinkData_ `catchChatError` (pure . CPError) + plan <- invitationRequestPlan user cReq contactSLinkData_ `catchAllErrors` (pure . CPError) pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) connectPlan user (ACL SCMContact cLink) = case cLink of CLFull cReq -> do - plan <- contactOrGroupRequestPlan user cReq `catchChatError` (pure . CPError) + plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError) pure (ACCL SCMContact $ CCLink cReq Nothing, plan) CLShort l@(CSLContact _ ct _ _) -> do let l' = serverShortLink l @@ -3875,7 +3875,7 @@ processChatCommand vr nm = \case case contactOrGroup of CGContact Contact {activeConn} -> forM_ activeConn $ \conn -> withFastStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr - CGGroup _ ms -> forM_ ms $ \m -> saveMemberFD m `catchChatError` eToView + CGGroup _ ms -> forM_ ms $ \m -> saveMemberFD m `catchAllErrors` eToView where -- we are not sending files to pending members, same as with inline files saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} = @@ -4061,7 +4061,7 @@ startExpireCIThread user@User {userId} = do liftIO $ threadDelay' delay interval <- asks $ ciExpirationInterval . config forever $ do - flip catchChatError' (eToView') $ do + flip catchAllErrors' (eToView') $ do expireFlags <- asks expireCIFlags atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry lift waitChatStartedAndActivated @@ -4103,7 +4103,7 @@ agentSubscriber = do SAERcvFile -> processAgentMsgRcvFile corrId entId msg SAESndFile -> processAgentMsgSndFile corrId entId msg where - run action = action `catchChatError'` (eToView') + run action = action `catchAllErrors'` (eToView') type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType (Maybe ClientServiceId))) @@ -4251,7 +4251,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do pendingConnSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId PendingContactConnection -> CM () pendingConnSubsToView rs = toViewTE . TEPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a] - withStore_ a = withStore' (`a` user) `catchChatError` \e -> eToView e $> [] + withStore_ a = withStore' (`a` user) `catchAllErrors` \e -> eToView e $> [] filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)] filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_) resultsFor :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId a -> [(a, Maybe ChatError)] @@ -4273,40 +4273,40 @@ cleanupManager = do liftIO $ threadDelay' initialDelay stepDelay <- asks (cleanupManagerStepDelay . config) forever $ do - flip catchChatError eToView $ do + flip catchAllErrors eToView $ do lift waitChatStartedAndActivated users <- withStore' getUsers let (us, us') = partition activeUser users forM_ us $ cleanupUser interval stepDelay forM_ us' $ cleanupUser interval stepDelay - cleanupMessages `catchChatError` eToView + cleanupMessages `catchAllErrors` eToView -- TODO possibly, also cleanup async commands - cleanupProbes `catchChatError` eToView + cleanupProbes `catchAllErrors` eToView liftIO $ threadDelay' $ diffToMicroseconds interval where - runWithoutInitialDelay cleanupInterval = flip catchChatError eToView $ do + runWithoutInitialDelay cleanupInterval = flip catchAllErrors eToView $ do lift waitChatStartedAndActivated users <- withStore' getUsers let (us, us') = partition activeUser users - forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` eToView - forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` eToView + forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchAllErrors` eToView + forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchAllErrors` eToView cleanupUser cleanupInterval stepDelay user = do - cleanupTimedItems cleanupInterval user `catchChatError` eToView + cleanupTimedItems cleanupInterval user `catchAllErrors` eToView liftIO $ threadDelay' stepDelay -- TODO remove in future versions: legacy step - contacts are no longer marked as deleted - cleanupDeletedContacts user `catchChatError` eToView + cleanupDeletedContacts user `catchAllErrors` eToView liftIO $ threadDelay' stepDelay cleanupTimedItems cleanupInterval user = do ts <- liftIO getCurrentTime let startTimedThreadCutoff = addUTCTime cleanupInterval ts timedItems <- withStore' $ \db -> getTimedItems db user startTimedThreadCutoff - forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchChatError` const (pure ()) + forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchAllErrors` const (pure ()) cleanupDeletedContacts user = do vr <- chatVersionRange contacts <- withStore' $ \db -> getDeletedContacts db vr user forM_ contacts $ \ct -> withStore (\db -> deleteContactWithoutGroups db user ct) - `catchChatError` eToView + `catchAllErrors` eToView cleanupMessages = do ts <- liftIO getCurrentTime let cutoffTs = addUTCTime (-(30 * nominalDay)) ts @@ -4332,7 +4332,7 @@ expireChatItems user@User {userId} globalTTL sync = do loop :: [Int64] -> (Int64 -> CM ()) -> CM () loop [] _ = pure () loop (a : as) process = continue $ do - process a `catchChatError` eToView + process a `catchAllErrors` eToView loop as process continue :: CM () -> CM () continue a = @@ -4347,7 +4347,7 @@ expireContactChatItems :: User -> VersionRangeChat -> Int64 -> ContactId -> CM ( expireContactChatItems user vr globalTTL ctId = -- reading contacts and groups inside the loop, -- to allow ttl changing while processing and to reduce memory usage - tryChatError (withStore $ \db -> getContact db vr user ctId) >>= mapM_ process + tryAllErrors (withStore $ \db -> getContact db vr user ctId) >>= mapM_ process where process ct@Contact {chatItemTTL} = withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do @@ -4358,7 +4358,7 @@ expireContactChatItems user vr globalTTL ctId = expireGroupChatItems :: User -> VersionRangeChat -> Int64 -> UTCTime -> GroupId -> CM () expireGroupChatItems user vr globalTTL createdAtCutoff groupId = - tryChatError (withStore $ \db -> getGroupInfo db vr user groupId) >>= mapM_ process + tryAllErrors (withStore $ \db -> getGroupInfo db vr user groupId) >>= mapM_ process where process gInfo@GroupInfo {chatItemTTL} = withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 413a36f567..e3885a3c9a 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -193,7 +193,7 @@ toggleNtf :: GroupMember -> Bool -> CM () toggleNtf m ntfOn = when (memberActive m) $ forM_ (memberConnId m) $ \connId -> - withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` eToView + withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchAllErrors` eToView prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> Maybe MsgScope -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup)) prepareGroupMsg db user g@GroupInfo {membership} msgScope mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of @@ -385,7 +385,7 @@ cancelFilesInProgress :: User -> [CIFileInfo] -> CM () cancelFilesInProgress user filesInfo = do let filesInfo' = filter (not . fileEnded) filesInfo (sfs, rfs) <- lift $ splitFTTypes <$> withStoreBatch (\db -> map (getFT db) filesInfo') - forM_ rfs $ \RcvFileTransfer {fileId} -> lift (closeFileHandle fileId rcvFiles) `catchChatError` \_ -> pure () + forM_ rfs $ \RcvFileTransfer {fileId} -> lift (closeFileHandle fileId rcvFiles) `catchAllErrors` \_ -> pure () lift . void . withStoreBatch' $ \db -> map (updateSndFileCancelled db) sfs lift . void . withStoreBatch' $ \db -> map (updateRcvFileCancelled db) rfs let xsfIds = mapMaybe (\(FileTransferMeta {fileId, xftpSndFile}, _) -> (,fileId) <$> xftpSndFile) sfs @@ -655,7 +655,7 @@ setFileToEncrypt ft@RcvFileTransfer {fileId} = do receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatResponse receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do - (CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError + (CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchAllErrors` processError where -- TODO AChatItem in Cancelled events processError e @@ -664,7 +664,7 @@ receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do receiveFileEvt' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatEvent receiveFileEvt' user ft userApprovedRelays rcvInline_ filePath_ = do - (CEvtRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError + (CEvtRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchAllErrors` processError where -- TODO AChatItem in Cancelled events processError e @@ -788,7 +788,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cleanupACIFile :: AChatItem -> CM () cleanupACIFile (AChatItem _ _ _ ChatItem {file = Just CIFile {fileSource = Just CryptoFile {filePath}}}) = do fsFilePath <- lift $ toFSFilePath filePath - removeFile fsFilePath `catchChatError` \_ -> pure () + removeFile fsFilePath `catchAllErrors` \_ -> pure () cleanupACIFile _ = pure () getKnownAgentServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> User -> CM (NonEmpty (ServerCfg p)) @@ -1089,7 +1089,7 @@ introduceMember vr user gInfo@GroupInfo {groupId} m@GroupMember {activeConn = Ju forM_ (L.nonEmpty events) $ \events' -> sendGroupMemberMessages user conn events' groupId else forM_ shuffledIntros $ \intro -> - processIntro intro `catchChatError` eToView + processIntro intro `catchAllErrors` eToView memberIntro :: GroupMember -> ChatMsgEvent 'Json memberIntro reMember = let mInfo = memberInfo reMember @@ -1113,7 +1113,7 @@ sendHistory _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternal sendHistory user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn = Just conn} = when (m `supportsVersion` batchSendVersion) $ do (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) - (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items + (errs', events) <- partitionEithers <$> mapM (tryAllErrors . itemForwardEvents) items let errors = map ChatErrorStore errs <> errs' unless (null errors) $ toView $ CEvtChatErrors errors let events' = concat events @@ -1286,7 +1286,7 @@ metaBrokerTs MsgMeta {broker = (_, brokerTs)} = brokerTs createContactPQSndItem :: User -> Contact -> Connection -> PQEncryption -> CM (Contact, Connection) createContactPQSndItem user ct conn@Connection {pqSndEnabled} pqSndEnabled' = - flip catchChatError (const $ pure (ct, conn)) $ case (pqSndEnabled, pqSndEnabled') of + flip catchAllErrors (const $ pure (ct, conn)) $ case (pqSndEnabled, pqSndEnabled') of (Just b, b') | b' /= b -> createPQItem $ CISndConnEvent (SCEPqEnabled pqSndEnabled') (Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (E2EInfo $ Just pqSndEnabled') _ -> pure (ct, conn) @@ -1301,7 +1301,7 @@ createContactPQSndItem user ct conn@Connection {pqSndEnabled} pqSndEnabled' = updateContactPQRcv :: User -> Contact -> Connection -> PQEncryption -> CM (Contact, Connection) updateContactPQRcv user ct conn@Connection {connId, pqRcvEnabled} pqRcvEnabled' = - flip catchChatError (const $ pure (ct, conn)) $ case (pqRcvEnabled, pqRcvEnabled') of + flip catchAllErrors (const $ pure (ct, conn)) $ case (pqRcvEnabled, pqRcvEnabled') of (Just b, b') | b' /= b -> updatePQ $ CIRcvConnEvent (RCEPqEnabled pqRcvEnabled') (Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (E2EInfo $ Just pqRcvEnabled') _ -> pure (ct, conn) @@ -1539,13 +1539,13 @@ appendFileChunk ft@RcvFileTransfer {fileId, fileStatus, cryptoArgs, fileInvitati lift $ closeFileHandle fileId rcvFiles forM_ cryptoArgs $ \cfArgs -> do tmpFile <- lift getChatTempDirectory >>= liftIO . (`uniqueCombine` fileName) - tryChatError (liftError encryptErr $ encryptFile fsFilePath tmpFile cfArgs) >>= \case + tryAllErrors (liftError encryptErr $ encryptFile fsFilePath tmpFile cfArgs) >>= \case Right () -> do - removeFile fsFilePath `catchChatError` \_ -> pure () + removeFile fsFilePath `catchAllErrors` \_ -> pure () renameFile tmpFile fsFilePath Left e -> do eToView e - removeFile tmpFile `catchChatError` \_ -> pure () + removeFile tmpFile `catchAllErrors` \_ -> pure () withStore' (`removeFileCryptoArgs` fileId) where encryptErr e = fileErr $ e <> ", received file not encrypted" @@ -1569,7 +1569,7 @@ isFileActive fileId files = do cancelRcvFileTransfer :: User -> RcvFileTransfer -> CM (Maybe ConnId) cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInline} = - cancel' `catchChatError` (\e -> eToView e $> fileConnId) + cancel' `catchAllErrors` (\e -> eToView e $> fileConnId) where cancel' = do lift $ closeFileHandle fileId rcvFiles @@ -1587,13 +1587,13 @@ cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInlin cancelSndFile :: User -> FileTransferMeta -> [SndFileTransfer] -> Bool -> CM [ConnId] cancelSndFile user FileTransferMeta {fileId, xftpSndFile} fts sendCancel = do withStore' (\db -> updateFileCancelled db user fileId CIFSSndCancelled) - `catchChatError` eToView + `catchAllErrors` eToView case xftpSndFile of Nothing -> catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel) Just xsf -> do forM_ fts (\ft -> cancelSndFileTransfer user ft False) - lift (agentXFTPDeleteSndFileRemote user xsf fileId) `catchChatError` eToView + lift (agentXFTPDeleteSndFileRemote user xsf fileId) `catchAllErrors` eToView pure [] -- TODO v6.0 remove @@ -1601,7 +1601,7 @@ cancelSndFileTransfer :: User -> SndFileTransfer -> Bool -> CM (Maybe ConnId) cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, agentConnId = AgentConnId acId, fileStatus, fileInline} sendCancel = if fileStatus == FSCancelled || fileStatus == FSComplete then pure Nothing - else cancel' `catchChatError` (\e -> eToView e $> fileConnId) + else cancel' `catchAllErrors` (\e -> eToView e $> fileConnId) where cancel' = do withStore' $ \db -> do @@ -1661,7 +1661,7 @@ sendDirectContactMessages user ct events = do if v >= batchSend2Version then sendDirectContactMessages' user ct events else forM (L.toList events) $ \evt -> - (Right . fst <$> sendDirectContactMessage user ct evt) `catchChatError` \e -> pure (Left e) + (Right . fst <$> sendDirectContactMessage user ct evt) `catchAllErrors` \e -> pure (Left e) sendDirectContactMessages' :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] sendDirectContactMessages' user ct events = do @@ -1856,7 +1856,7 @@ sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope sendGroupMessages user gInfo scope members events = do -- TODO [knocking] send current profile to pending member after approval? when shouldSendProfileUpdate $ - sendProfileUpdate `catchChatError` eToView + sendProfileUpdate `catchAllErrors` eToView sendGroupMessages_ user gInfo members events where User {profile = p, userMemberProfileUpdatedAt} = user @@ -2013,7 +2013,7 @@ memberSendAction gInfo events members m@GroupMember {memberRole, memberStatus} = sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM () sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do msg <- createSndMessage chatMsgEvent (GroupId groupId) - messageMember msg `catchChatError` eToView + messageMember msg `catchAllErrors` eToView where messageMember :: SndMessage -> CM () messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case @@ -2054,7 +2054,7 @@ saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} msg <- withStore (\db -> createNewMessageAndRcvMsgDelivery db (GroupId groupId) newMsg sharedMsgId_ rcvMsgDelivery $ Just amGroupMemId) - `catchChatError` \e -> case e of + `catchAllErrors` \e -> case e of ChatErrorStore (SEDuplicateGroupMessage _ _ _ (Just forwardedByGroupMemberId)) -> do vr <- chatVersionRange fm <- withStore $ \db -> getGroupMember db vr user groupId forwardedByGroupMemberId @@ -2070,7 +2070,7 @@ saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember@GroupMember {me fwdMemberId = Just $ groupMemberId' forwardingMember refAuthorId = Just $ groupMemberId' refAuthorMember withStore (\db -> createNewRcvMessage db (GroupId groupId) newMsg sharedMsgId_ refAuthorId fwdMemberId) - `catchChatError` \e -> case e of + `catchAllErrors` \e -> case e of ChatErrorStore (SEDuplicateGroupMessage _ _ (Just authorGroupMemberId) Nothing) -> do vr <- chatVersionRange am@GroupMember {memberId = amMemberId} <- withStore $ \db -> getGroupMember db vr user groupId authorGroupMemberId @@ -2213,7 +2213,7 @@ deleteAgentConnectionAsync acId = deleteAgentConnectionAsync' acId False deleteAgentConnectionAsync' :: ConnId -> Bool -> CM () deleteAgentConnectionAsync' acId waitDelivery = do - withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` eToView + withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchAllErrors` eToView deleteAgentConnectionsAsync :: [ConnId] -> CM () deleteAgentConnectionsAsync acIds = deleteAgentConnectionsAsync' acIds False @@ -2222,7 +2222,7 @@ deleteAgentConnectionsAsync acIds = deleteAgentConnectionsAsync' acIds False deleteAgentConnectionsAsync' :: [ConnId] -> Bool -> CM () deleteAgentConnectionsAsync' [] _ = pure () deleteAgentConnectionsAsync' acIds waitDelivery = do - withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` eToView + withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchAllErrors` eToView agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM () agentXFTPDeleteRcvFile aFileId fileId = do @@ -2271,7 +2271,7 @@ agentXFTPDeleteSndFilesRemote user sndFiles = do case privateSndFileDescr of Nothing -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr Just sfdText -> - tryChatError' (parseFileDescription sfdText) >>= \case + tryAllErrors' (parseFileDescription sfdText) >>= \case Left _ -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr Right sfd -> partitionSndDescr xsfs filesWithoutDescr ((aFileId, sfd) : filesWithDescr) diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 3517f76152..3109fd5fe9 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -103,7 +103,7 @@ processAgentMessage corrId connId msg = do vr <- chatVersionRange -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case - Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` eToView + Just user -> processAgentMessageConn vr user corrId connId msg `catchAllErrors` eToView _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) -- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. @@ -115,7 +115,7 @@ processAgentMessage corrId connId msg = do -- Full app restart is likely to resolve database condition and the message will be received and processed again. critical :: CM a -> CM a critical a = - a `catchChatError` \case + a `catchAllErrors` \case ChatErrorStore SEDBBusyError {message} -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing e -> throwError e @@ -156,7 +156,7 @@ processAgentMsgSndFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId) withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $ withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case - Just user -> process user fileId `catchChatError` eToView + Just user -> process user fileId `catchAllErrors` eToView _ -> do lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId @@ -298,7 +298,7 @@ processAgentMsgRcvFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $ withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case - Just user -> process user fileId `catchChatError` eToView + Just user -> process user fileId `catchAllErrors` eToView _ -> do lift $ withAgent' (`xftpDeleteRcvFile` aFileId) throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId @@ -472,10 +472,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withAckMessage "contact msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do let MsgMeta {pqEncryption} = msgMeta (ct', conn') <- updateContactPQRcv user ct conn pqEncryption - checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure () + checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchAllErrors` \_ -> pure () forM_ aChatMsgs $ \case Right (ACMsg _ chatMsg) -> - processEvent ct' conn' tags eInfo chatMsg `catchChatError` \e -> eToView e + processEvent ct' conn' tags eInfo chatMsg `catchAllErrors` \e -> eToView e Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e @@ -537,7 +537,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [async agent commands] no continuation needed, but command should be asynchronous for stability allowAgentConnectionAsync user conn'' confId XOk XInfo profile -> do - ct' <- processContactProfileUpdate ct profile False `catchChatError` const (pure ct) + ct' <- processContactProfileUpdate ct profile False `catchAllErrors` const (pure ct) -- [incognito] send incognito profile incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId let p = userProfileDirect user (fromLocalProfile <$> incognitoProfile) (Just ct') True @@ -897,12 +897,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withAckMessage "group msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do -- possible improvement is to choose scope based on event (some events specify scope) (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m - checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchChatError` \_ -> pure () + checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchAllErrors` \_ -> pure () (fwdScopesMsgs, shouldDelConns) <- foldM (processAChatMsg gInfo' m' tags eInfo) (M.empty, False) aChatMsgs when (isUserGrpFwdRelay gInfo') $ do unless (blockedByAdmin m) $ forM_ (M.assocs fwdScopesMsgs) $ \(groupForwardScope, fwdMsgs) -> - forwardMsgs groupForwardScope (L.reverse fwdMsgs) `catchChatError` eToView + forwardMsgs groupForwardScope (L.reverse fwdMsgs) `catchAllErrors` eToView when shouldDelConns $ deleteGroupConnections gInfo' True withRcpt <- checkSendRcpt $ rights aChatMsgs pure (withRcpt, shouldDelConns) @@ -920,7 +920,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processAChatMsg gInfo' m' tags eInfo (fwdScopeMap, shouldDelConns) = \case Right (ACMsg SJson chatMsg) -> do (cmFwdScope_, cmShouldDelConns) <- - processEvent gInfo' m' tags eInfo chatMsg `catchChatError` \e -> eToView e $> (Nothing, False) + processEvent gInfo' m' tags eInfo chatMsg `catchAllErrors` \e -> eToView e $> (Nothing, False) let fwdScopeMap' = case cmFwdScope_ of Nothing -> fwdScopeMap @@ -928,7 +928,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = shouldDelConns' = shouldDelConns || cmShouldDelConns pure (fwdScopeMap', shouldDelConns') Right (ACMsg SBinary chatMsg) -> do - void (processEvent gInfo' m' tags eInfo chatMsg) `catchChatError` \e -> eToView e + void (processEvent gInfo' m' tags eInfo chatMsg) `catchAllErrors` \e -> eToView e pure (fwdScopeMap, shouldDelConns) Left e -> do atomically $ modifyTVar' tags ("error" :) @@ -1559,7 +1559,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- 3) show screen of death to the user asking to restart eInfo <- eventInfo logInfo $ label <> ": " <> eInfo - tryChatError (action eInfo) >>= \case + tryAllErrors (action eInfo) >>= \case Right (withRcpt, shouldDelConns) -> unless shouldDelConns $ withLog (eInfo <> " ok") $ ackMsg msgMeta $ if withRcpt then Just "" else Nothing -- If showCritical is True, then these errors don't result in ACK and show user visible alert @@ -1666,7 +1666,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = sendProbeHashes :: [ContactOrMember] -> Probe -> Int64 -> CM () sendProbeHashes cgms probe probeId = - forM_ cgms $ \cgm -> sendProbeHash cgm `catchChatError` \_ -> pure () + forM_ cgms $ \cgm -> sendProbeHash cgm `catchAllErrors` \_ -> pure () where probeHash = ProbeHash $ C.sha256Hash (unProbe probe) sendProbeHash :: ContactOrMember -> CM () @@ -1738,7 +1738,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- in processFDMessage some paths are programmed as errors, -- for example failure on not approved relays (CEFileNotApproved). -- we catch error, so that even if processFDMessage fails, message can still be forwarded. - processFDMessage fileId aci fileDescr `catchChatError` \_ -> pure () + processFDMessage fileId aci fileDescr `catchAllErrors` \_ -> pure () pure $ Just $ toGroupForwardScope g scopeInfo else messageError "x.msg.file.descr: file of another member" $> Nothing @@ -1900,7 +1900,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = catchCINotFound :: CM a -> (SharedMsgId -> CM a) -> CM a catchCINotFound f handle = - f `catchChatError` \case + f `catchAllErrors` \case ChatErrorStore (SEChatItemSharedMsgIdNotFound sharedMsgId) -> handle sharedMsgId e -> throwError e @@ -2497,7 +2497,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = probeMatches :: [ContactOrMember] -> ContactOrMember -> CM () probeMatches [] _ = pure () probeMatches (cgm1' : cgm1s') cgm2' = do - cgm2''_ <- probeMatch cgm1' cgm2' probe `catchChatError` \_ -> pure (Just cgm2') + cgm2''_ <- probeMatch cgm1' cgm2' probe `catchAllErrors` \_ -> pure (Just cgm2') let cgm2'' = fromMaybe cgm2' cgm2''_ probeMatches cgm1s' cgm2'' @@ -3225,7 +3225,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM () directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do - checkIntegrityCreateItem (CDDirectRcv ct) msgMeta `catchChatError` \_ -> pure () + checkIntegrityCreateItem (CDDirectRcv ct) msgMeta `catchAllErrors` \_ -> pure () forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus updateDirectItemStatus ct conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete @@ -3233,7 +3233,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM () groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m - checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchChatError` \_ -> pure () + checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchAllErrors` \_ -> pure () forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus updateGroupItemsStatus gInfo' m' conn agentMsgId (GSSRcvd msgRcptStatus) Nothing diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 7c25890bf2..3e5e25e430 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -175,13 +175,13 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do pure hostInfo handleConnectError :: RHKey -> SessionSeq -> CM a -> CM a handleConnectError rhKey sessSeq action = - action `catchChatError` \err -> do + action `catchAllErrors` \err -> do logError $ "startRemoteHost.rcConnectHost crashed: " <> tshow err cancelRemoteHostSession (Just (sessSeq, RHSRConnectionFailed err)) rhKey throwError err handleHostError :: SessionSeq -> TVar RHKey -> CM () -> CM () handleHostError sessSeq rhKeyVar action = - action `catchChatError` \err -> do + action `catchAllErrors` \err -> do logError $ "startRemoteHost.waitForHostSession crashed: " <> tshow err readTVarIO rhKeyVar >>= cancelRemoteHostSession (Just (sessSeq, RHSRCrashed err)) waitForHostSession :: Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS 'TServer, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> CM () @@ -411,7 +411,7 @@ findKnownRemoteCtrl = do atomically $ takeTMVar cmdOk (RCCtrlPairing {ctrlFingerprint}, inv@(RCVerifiedInvitation RCInvitation {app})) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) discoveryTimeout . withAgent $ \a -> rcDiscoverCtrl a pairings - ctrlAppInfo_ <- (Just <$> parseCtrlAppInfo app) `catchChatError` const (pure Nothing) + ctrlAppInfo_ <- (Just <$> parseCtrlAppInfo app) `catchAllErrors` const (pure Nothing) rc <- withStore' (`getRemoteCtrlByFingerprint` ctrlFingerprint) >>= \case Nothing -> throwChatError $ CEInternalError "connecting with a stored ctrl" @@ -500,11 +500,11 @@ parseCtrlAppInfo ctrlAppInfo = do handleRemoteCommand :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> RemoteCrypto -> TBQueue (Either ChatError ChatEvent) -> HTTP2Request -> CM' () handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do logDebug "handleRemoteCommand" - liftIO (tryRemoteError' parseRequest) >>= \case + liftIO (tryAllErrors' parseRequest) >>= \case Right (rfKN, getNext, rc) -> do chatReadVar' currentUser >>= \case Nothing -> replyError $ ChatError CENoActiveUser - Just user -> processCommand user rfKN getNext rc `catchChatError'` replyError + Just user -> processCommand user rfKN getNext rc `catchAllErrors'` replyError Left e -> reply $ RRProtocolError e where parseRequest :: ExceptT RemoteProtocolError IO (C.SbKeyNonce, GetChunk, RemoteCommand) @@ -523,7 +523,7 @@ handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBo replyWith :: Respond replyWith rr attach = do (corrId, cmdKN, sfKN) <- atomically $ getRemoteSndKeys encryption - liftIO (tryRemoteError' . encryptEncodeHTTP2Body corrId cmdKN encryption $ J.encode rr) >>= \case + liftIO (tryAllErrors' . encryptEncodeHTTP2Body corrId cmdKN encryption $ J.encode rr) >>= \case Right resp -> liftIO . sendResponse . responseStreaming N.status200 [] $ \send flush -> do send resp attach sfKN send @@ -542,14 +542,6 @@ type Respond = RemoteResponse -> (C.SbKeyNonce -> SendChunk -> IO ()) -> CM' () liftRC :: ExceptT RemoteProtocolError IO a -> CM a liftRC = liftError (ChatErrorRemoteCtrl . RCEProtocolError) -tryRemoteError :: ExceptT RemoteProtocolError IO a -> ExceptT RemoteProtocolError IO (Either RemoteProtocolError a) -tryRemoteError = tryAllErrors (RPEException . tshow) -{-# INLINE tryRemoteError #-} - -tryRemoteError' :: ExceptT RemoteProtocolError IO a -> IO (Either RemoteProtocolError a) -tryRemoteError' = tryAllErrors' (RPEException . tshow) -{-# INLINE tryRemoteError' #-} - handleSend :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> Int -> CM' RemoteResponse handleSend execCC command retryNum = do logDebug $ "Send: " <> tshow command @@ -573,7 +565,7 @@ handleStoreFile rfKN fileName fileSize fileDigest getChunk = Just ff -> takeFileName <$$> storeFileTo ff Nothing -> storeFileTo =<< getDefaultFilesFolder storeFileTo :: FilePath -> CM' (Either RemoteProtocolError FilePath) - storeFileTo dir = liftIO . tryRemoteError' $ do + storeFileTo dir = liftIO . tryAllErrors' $ do filePath <- liftIO $ dir `uniqueCombine` fileName receiveEncryptedFile rfKN getChunk fileSize fileDigest filePath pure filePath @@ -586,7 +578,7 @@ handleGetFile User {userId} RemoteFile {userId = commandUserId, fileId, sent, fi withStore $ \db -> do cf <- getLocalCryptoFile db commandUserId fileId sent unless (cf == cf') $ throwError $ SEFileNotFound fileId - liftRC (tryRemoteError $ getFileInfo path) >>= \case + liftRC (tryAllErrors $ getFileInfo path) >>= \case Left e -> lift $ reply (RRProtocolError e) $ \_ _ -> pure () Right (fileSize, fileDigest) -> lift . withFile path ReadMode $ \h -> do @@ -658,7 +650,7 @@ stopRemoteCtrl = cancelActiveRemoteCtrl Nothing handleCtrlError :: SessionSeq -> (ChatError -> RemoteCtrlStopReason) -> Text -> CM a -> CM a handleCtrlError sseq mkReason name action = - action `catchChatError` \e -> do + action `catchAllErrors` \e -> do logError $ name <> " remote ctrl error: " <> tshow e cancelActiveRemoteCtrl $ Just (sseq, mkReason e) throwError e diff --git a/src/Simplex/Chat/Remote/Types.hs b/src/Simplex/Chat/Remote/Types.hs index defbe7e72c..b7af624e9e 100644 --- a/src/Simplex/Chat/Remote/Types.hs +++ b/src/Simplex/Chat/Remote/Types.hs @@ -28,6 +28,7 @@ import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON import Simplex.Messaging.Transport (TLS (..), TSbChainKeys (..), TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client) import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Util (AnyError (..), tshow) import Simplex.RemoteControl.Client import Simplex.RemoteControl.Types @@ -155,6 +156,9 @@ data RemoteProtocolError | RPEException {someException :: Text} deriving (Show, Exception) +instance AnyError RemoteProtocolError where + fromSomeException = RPEException . tshow + type RemoteHostId = Int64 data RHKey = RHNew | RHId {remoteHostId :: RemoteHostId} diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 1caf034f2a..745f1b4b9a 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -46,6 +46,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..)) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (SubscriptionMode (..)) +import Simplex.Messaging.Util (AnyError (..)) import Simplex.Messaging.Version import UnliftIO.STM #if defined(dbPostgres) @@ -149,6 +150,10 @@ data StoreError | SEInvalidMention deriving (Show, Exception) +instance AnyError StoreError where + fromSomeException = SEInternalError . show + {-# INLINE fromSomeException #-} + $(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError) insertedRowId :: DB.Connection -> IO Int64 From 6beb6b1efca985122459f7965670c82945651823 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 2 Sep 2025 10:17:03 +0100 Subject: [PATCH 019/137] core: updated simplexmq to 6.4.5.0 --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 6618c088c5..88f8b9aede 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: beafac1f73f7d61b99c79d609731f94a05ced97f + tag: 0319addd2b6bcd1206921c844f81fea4adc791a4 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 25784e181c..c424aeb9fc 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."beafac1f73f7d61b99c79d609731f94a05ced97f" = "11f1d0h5n7lkwsjz1csyxkrrcxqsyf2rv7339qcfbpdmqhq6k3ni"; + "https://github.com/simplex-chat/simplexmq.git"."0319addd2b6bcd1206921c844f81fea4adc791a4" = "1rm0m6bch648xpa8l28m3ww6fi686g9ky68qc46gsqirpm29kh0q"; "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"; From 9d65b9eb2b24722be7f35e2d851ba397c2d12d37 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:07:28 +0000 Subject: [PATCH 020/137] ui: navigation from support chat to member info (#6237) --- apps/ios/Shared/Views/Chat/ChatView.swift | 38 +++++++++++++++++-- .../Chat/Group/GroupMemberInfoView.swift | 4 +- .../Views/Chat/Group/SecondaryChatView.swift | 6 ++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++---- apps/ios/SimpleXChat/ChatTypes.swift | 3 +- .../chat/simplex/common/model/ChatModel.kt | 3 +- .../simplex/common/views/chat/ChatView.kt | 6 ++- .../views/chat/group/GroupChatInfoView.kt | 2 +- .../views/chat/group/GroupMemberInfoView.kt | 10 ++++- .../views/chat/group/MemberSupportChatView.kt | 27 ++++++++++++- 10 files changed, 93 insertions(+), 22 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 2f8d6f2acd..83382cfe4f 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -60,6 +60,7 @@ struct ChatView: View { @State private var animatedScrollingInProgress: Bool = false @State private var showUserSupportChatSheet = false @State private var showCommandsMenu = false + @State private var supportChatMemberInfoLinkActive = false @State private var scrollView: EndlessScrollView = EndlessScrollView(frame: .zero) @@ -178,6 +179,28 @@ struct ChatView: View { if im.showLoadingProgress == chat.id { ProgressView().scaleEffect(2) } + if case let .group(groupInfo, _) = chat.chatInfo, + case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter, + case let .memberSupport(groupMember_) = groupScopeInfo, + let groupMember = groupMember_ { + NavigationLink(isActive: $supportChatMemberInfoLinkActive) { + GroupMemberInfoView( + groupInfo: groupInfo, + chat: chat, + groupMember: GMember(groupMember), + scrollToItemId: $scrollToItemId, + openedFromSupportChat: true + ) + .navigationBarHidden(false) + .modifier(BackButton(disabled: Binding.constant(false)) { + supportChatMemberInfoLinkActive = false + }) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } } .safeAreaInset(edge: .top) { VStack(spacing: .zero) { @@ -222,7 +245,9 @@ struct ChatView: View { } } } - .appSheet(item: $selectedMember) { member in + .appSheet(item: $selectedMember, onDismiss: { + chatModel.secondaryIM = nil + }) { member in if case let .group(groupInfo, _) = chat.chatInfo { GroupMemberInfoView( groupInfo: groupInfo, @@ -459,7 +484,10 @@ struct ChatView: View { ChatInfoToolbar(chat: chat) .tint(theme.colors.primary) } - .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { + chatModel.secondaryIM = nil + theme = buildTheme() + }) { GroupChatInfoView( chat: chat, groupInfo: Binding( @@ -562,7 +590,11 @@ struct ChatView: View { switch groupScopeInfo { case let .memberSupport(groupMember_): if let groupMember = groupMember_ { - MemberSupportChatToolbar(groupMember: groupMember) + Button { + supportChatMemberInfoLinkActive = true + } label: { + MemberSupportChatToolbar(groupMember: groupMember) + } } else { textChatToolbar("Chat with admins") } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 2057b9b43c..2298af614e 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -18,6 +18,7 @@ struct GroupMemberInfoView: View { @ObservedObject var groupMember: GMember @Binding var scrollToItemId: ChatItem.ID? var navigation: Bool = false + var openedFromSupportChat: Bool = false @State private var connectionStats: ConnectionStats? = nil @State private var connectionCode: String? = nil @State private var connectionLoaded: Bool = false @@ -101,7 +102,8 @@ struct GroupMemberInfoView: View { if member.memberActive { Section { - if groupInfo.membership.memberRole >= .moderator + if !openedFromSupportChat + && groupInfo.membership.memberRole >= .moderator && (member.memberRole < .moderator || member.supportChat != nil) { MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId) } diff --git a/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift index 47c5df264f..e2092f7a24 100644 --- a/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift +++ b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct SecondaryChatView: View { + @Environment(\.dismiss) var dismiss @EnvironmentObject var chatModel: ChatModel @ObservedObject var chat: Chat @Binding var scrollToItemId: ChatItem.ID? @@ -23,9 +24,10 @@ struct SecondaryChatView: View { floatingButtonModel: FloatingButtonModel(im: im), scrollToItemId: $scrollToItemId ) - .onDisappear { + .modifier(BackButton(disabled: Binding.constant(false)) { chatModel.secondaryIM = nil - } + dismiss() + }) } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 7a7f1e481d..4e7bd722c3 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index aed5373664..febe12de02 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2634,7 +2634,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { } public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { - if !canBeRemoved(groupInfo: groupInfo) { return nil } + if !canBeRemoved(groupInfo: groupInfo) || memberPending { return nil } let userRole = groupInfo.membership.memberRole return GroupMemberRole.supportedRoles.filter { $0 <= userRole } } @@ -2643,6 +2643,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { let userRole = groupInfo.membership.memberRole return memberStatus != .memRemoved && memberStatus != .memLeft && memberRole < .moderator && userRole >= .moderator && userRole >= memberRole && groupInfo.membership.memberActive + && !memberPending } public var canReceiveReports: Bool { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 047d14d9f1..7bf849f8f7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2343,7 +2343,7 @@ data class GroupMember ( } fun canChangeRoleTo(groupInfo: GroupInfo): List? = - if (!canBeRemoved(groupInfo)) null + if (!canBeRemoved(groupInfo) || memberPending) null else groupInfo.membership.memberRole.let { userRole -> GroupMemberRole.selectableRoles.filter { it <= userRole } } @@ -2352,6 +2352,7 @@ data class GroupMember ( val userRole = groupInfo.membership.memberRole return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Moderator && userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive + && !memberPending } val versionRange: VersionRange = activeConn?.peerChatVRange ?: memberChatVRange diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index de48a67988..eb858d8d5f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -453,7 +453,7 @@ fun ChatView( } ModalManager.end.showModalCloseable(true) { close -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, close, close) + GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close, close) } } } @@ -1005,7 +1005,9 @@ fun ChatLayout( Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { Box { if (selectedChatItems.value == null) { - MemberSupportChatAppBar(chatsCtx, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, { ModalManager.end.closeModal() }, onSearchValueChanged) + if (chat != null) { + MemberSupportChatAppBar(chatsCtx, remoteHostId, chat, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, scrollToItemId, { ModalManager.end.closeModal() }, onSearchValueChanged) + } } else { SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 5a749766c4..1ea3daeab1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -126,7 +126,7 @@ fun ModalData.GroupChatInfoView( } ModalManager.end.showModalCloseable(true) { closeCurrent -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, closeCurrent) { + GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, closeCurrent) { closeCurrent() close() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index e5f0b708e7..b29374390e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -49,9 +49,13 @@ fun GroupMemberInfoView( connectionStats: ConnectionStats?, connectionCode: String?, chatModel: ChatModel, + openedFromSupportChat: Boolean, close: () -> Unit, closeAll: () -> Unit, // Close all open windows up to ChatView ) { + KeyChangeEffect(chat.simplex.common.platform.chatModel.chatId.value) { + ModalManager.end.closeModals() + } BackHandler(onBack = close) val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } val connStats = remember { mutableStateOf(connectionStats) } @@ -225,7 +229,8 @@ fun GroupMemberInfoView( ) } } - } + }, + openedFromSupportChat = openedFromSupportChat ) if (progressIndicator) { @@ -291,6 +296,7 @@ fun GroupMemberInfoLayout( syncMemberConnection: () -> Unit, syncMemberConnectionForce: () -> Unit, verifyClicked: () -> Unit, + openedFromSupportChat: Boolean ) { val cStats = connStats.value fun knownDirectChat(contactId: Long): Pair? { @@ -440,6 +446,7 @@ fun GroupMemberInfoLayout( if (member.memberActive) { SectionView { if ( + !openedFromSupportChat && groupInfo.membership.memberRole >= GroupMemberRole.Moderator && (member.memberRole < GroupMemberRole.Moderator || member.supportChat != null) ) { @@ -924,6 +931,7 @@ fun PreviewGroupMemberInfoLayout() { syncMemberConnection = {}, syncMemberConnectionForce = {}, verifyClicked = {}, + openedFromSupportChat = false, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt index 99565618f9..6680ef99bc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt @@ -51,7 +51,10 @@ private fun MemberSupportChatView( @Composable fun MemberSupportChatAppBar( chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chat: Chat, scopeMember_: GroupMember?, + scrollToItemId: MutableState, close: () -> Unit, onSearchValueChanged: (String) -> Unit ) { @@ -67,11 +70,31 @@ fun MemberSupportChatAppBar( } } BackHandler(onBack = onBackClicked) - if (scopeMember_ != null) { + if (chat.chatInfo is ChatInfo.Group && scopeMember_ != null) { + val groupInfo = chat.chatInfo.groupInfo DefaultAppBar( navigationButton = { NavigationButtonBack(onBackClicked) }, title = { MemberSupportChatToolbarTitle(scopeMember_) }, - onTitleClick = null, + onTitleClick = { + withBGApi { + val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, scopeMember_.groupMemberId) + val stats = r?.second + val code = if (scopeMember_.memberActive) { + val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, scopeMember_.groupMemberId) + memCode?.second + } else { + null + } + ModalManager.end.showModalCloseable(true) { closeCurrent -> + remember { derivedStateOf { chatModel.getGroupMember(scopeMember_.groupMemberId) } }.value?.let { mem -> + GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = true, close = closeCurrent) { + closeCurrent() + close() + } + } + } + } + }, onTop = !oneHandUI.value || !chatBottomBar.value, showSearch = showSearch.value, onSearchValueChanged = onSearchValueChanged, From 9705ae15eaf069e35adf0aa2789b43a7431aaf4c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:38:26 +0000 Subject: [PATCH 021/137] ui: fix report archiving (#6239) * kotlin: fix report archivation * ios wip * surrogate reports scope * refactor --- apps/ios/Shared/Model/AppAPITypes.swift | 2 ++ apps/ios/Shared/Model/ChatModel.swift | 28 +++++++++++-------- apps/ios/Shared/Views/Chat/ChatView.swift | 17 ++++++----- .../Views/Chat/Group/GroupChatInfoView.swift | 14 ++++++---- .../Views/Chat/Group/GroupMentions.swift | 2 ++ apps/ios/SimpleX SE/ShareAPI.swift | 2 ++ apps/ios/SimpleXChat/ChatTypes.swift | 21 ++++++++++---- .../simplex/common/views/chat/ChatView.kt | 6 ---- 8 files changed, 57 insertions(+), 35 deletions(-) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 2055a0ab99..d37d64646d 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -574,6 +574,8 @@ enum ChatCommand: ChatCmdProtocol { } else { "(_support)" } + case .reports: + "(reports, prohibited)" // can't use surrogate Reports scope } } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 33b3fd8009..a08f70fd53 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -667,20 +667,24 @@ final class ChatModel: ObservableObject { func getCIItemsModel(_ cInfo: ChatInfo, _ ci: ChatItem) -> ItemsModel? { let cInfoScope = cInfo.groupChatScope() - if let cInfoScope = cInfoScope { - switch cInfoScope { - case .memberSupport: - switch secondaryIM?.secondaryIMFilter { - case .none: - return nil - case let .groupChatScopeContext(groupScopeInfo): - return (cInfo.id == chatId && sameChatScope(cInfoScope, groupScopeInfo.toChatScope())) ? secondaryIM : nil - case let .msgContentTagContext(contentTag): - return (cInfo.id == chatId && ci.isReport && contentTag == .report) ? secondaryIM : nil - } + return if let cInfoScope = cInfoScope { + switch (cInfoScope, secondaryIM?.secondaryIMFilter) { + case let (.memberSupport, .some(.groupChatScopeContext(groupScopeInfo))): + // Chat with member or Chat with admins opened (secondaryIM has .groupChatScopeContext filter), cInfo has matching scope + (cInfo.id == chatId && sameChatScope(cInfoScope, groupScopeInfo.toChatScope())) ? secondaryIM : nil + + case let (.memberSupport, .some(.msgContentTagContext(contentTag))): + // Reports view opened (secondaryIM has .msgContentTagContext(.report) filter), we process event (cInfo has proper .memberSupport scope) + (cInfo.id == chatId && ci.isReport && contentTag == .report) ? secondaryIM : nil + + case let (.reports, .some(.msgContentTagContext(contentTag))): + // Reports view opened (secondaryIM has .msgContentTagContext(.report) filter), we process user action (cInfo has surrogate .reports scope) + (cInfo.id == chatId && ci.isReport && contentTag == .report) ? secondaryIM : nil + default: + nil } } else { - return cInfo.id == chatId ? im : nil + cInfo.id == chatId ? im : nil } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 83382cfe4f..f5348c9baf 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -234,13 +234,13 @@ struct ChatView: View { .confirmationDialog(selectedChatItems?.count == 1 ? "Archive report?" : "Archive \((selectedChatItems?.count ?? 0)) reports?", isPresented: $showArchiveSelectedReports, titleVisibility: .visible) { Button("For me", role: .destructive) { if let selected = selectedChatItems { - archiveReports(chat.chatInfo, selected.sorted(), false, deletedSelectedMessages) + archiveReports(chat, selected.sorted(), false, deletedSelectedMessages) } } if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, groupInfo.membership.memberActive { Button("For all moderators", role: .destructive) { if let selected = selectedChatItems { - archiveReports(chat.chatInfo, selected.sorted(), true, deletedSelectedMessages) + archiveReports(chat, selected.sorted(), true, deletedSelectedMessages) } } } @@ -598,6 +598,8 @@ struct ChatView: View { } else { textChatToolbar("Chat with admins") } + case .reports: + textChatToolbar("Member reports") } case let .msgContentTagContext(contentTag): switch contentTag { @@ -1913,14 +1915,14 @@ struct ChatView: View { .confirmationDialog(archivingReports?.count == 1 ? "Archive report?" : "Archive \(archivingReports?.count ?? 0) reports?", isPresented: $showArchivingReports, titleVisibility: .visible) { Button("For me", role: .destructive) { if let reports = self.archivingReports { - archiveReports(chat.chatInfo, reports.sorted(), false) + archiveReports(chat, reports.sorted(), false) self.archivingReports = [] } } if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, groupInfo.membership.memberActive { Button("For all moderators", role: .destructive) { if let reports = self.archivingReports { - archiveReports(chat.chatInfo, reports.sorted(), true) + archiveReports(chat, reports.sorted(), true) self.archivingReports = [] } } @@ -2712,13 +2714,13 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe await MainActor.run { for di in deletedItems { if let toItem = di.toChatItem { - _ = ChatModel.shared.upsertChatItem(chat.chatInfo, toItem.chatItem) + _ = ChatModel.shared.upsertChatItem(chatInfo, toItem.chatItem) } else { ChatModel.shared.removeChatItem(chatInfo, di.deletedChatItem.chatItem) } let deletedItem = di.deletedChatItem.chatItem if deletedItem.isActiveReport { - ChatModel.shared.decreaseGroupReportsCounter(chat.chatInfo.id) + ChatModel.shared.decreaseGroupReportsCounter(chatInfo.id) } } if let updatedChatInfo = deletedItems.last?.deletedChatItem.chatInfo { @@ -2733,8 +2735,9 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe } } -func archiveReports(_ chatInfo: ChatInfo, _ itemIds: [Int64], _ forAll: Bool, _ onSuccess: @escaping () async -> Void = {}) { +func archiveReports(_ chat: Chat, _ itemIds: [Int64], _ forAll: Bool, _ onSuccess: @escaping () async -> Void = {}) { if itemIds.count > 0 { + let chatInfo = chat.chatInfo Task { do { let deleted = try await apiDeleteReceivedReports( diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 872e65c7a3..d8929caa3e 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -98,7 +98,7 @@ struct GroupChatInfoView: View { memberSupportButton() } if groupInfo.canModerate { - GroupReportsChatNavLink(chat: chat, scrollToItemId: $scrollToItemId) + GroupReportsChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId) } if groupInfo.membership.memberActive && (groupInfo.membership.memberRole < .moderator || groupInfo.membership.supportChat != nil) { @@ -612,15 +612,19 @@ struct GroupChatInfoView: View { } struct GroupReportsChatNavLink: View { - @EnvironmentObject var chatModel: ChatModel - @EnvironmentObject var theme: AppTheme - @State private var navLinkActive = false @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme + var groupInfo: GroupInfo + @EnvironmentObject var chatModel: ChatModel @Binding var scrollToItemId: ChatItem.ID? + @State private var navLinkActive = false var body: some View { NavigationLink(isActive: $navLinkActive) { - SecondaryChatView(chat: chat, scrollToItemId: $scrollToItemId) + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: .reports), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) } label: { HStack { Label { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift index 440ed5227d..cdbed7fe30 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -107,6 +107,8 @@ struct GroupMentionsView: View { } else { return member.memberRole >= .moderator } + case .reports: + return false } case .msgContentTagContext: return false diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift index 2b3e8068ae..4dad9d5d15 100644 --- a/apps/ios/SimpleX SE/ShareAPI.swift +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -160,6 +160,8 @@ enum SEChatCommand: ChatCmdProtocol { } else { "(_support)" } + case .reports: + "(reports, prohibited)" // can't use surrogate Reports scope } } } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index febe12de02..2e2f41ec99 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1584,6 +1584,8 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { return nil case .some(.memberSupport(groupMember_: .none)): return nil + case .some(.reports): + return ("can't send messages", nil) } } else if groupInfo.nextConnectPrepared { return nil @@ -1895,26 +1897,35 @@ public struct ChatStats: Decodable, Hashable { public enum GroupChatScope: Decodable { case memberSupport(groupMemberId_: Int64?) + case reports // surrogate scope used for matching new items to opened Reports "chat scope" in UI, this type is not present in backend } public func sameChatScope(_ scope1: GroupChatScope, _ scope2: GroupChatScope) -> Bool { - switch (scope1, scope2) { + return switch (scope1, scope2) { case let (.memberSupport(groupMemberId1_), .memberSupport(groupMemberId2_)): - return groupMemberId1_ == groupMemberId2_ + groupMemberId1_ == groupMemberId2_ + case (.reports, .reports): + true + case (.reports, .memberSupport): + false + case (.memberSupport(groupMemberId_: let groupMemberId_), .reports): + false } } public enum GroupChatScopeInfo: Decodable, Hashable { case memberSupport(groupMember_: GroupMember?) + case reports // surrogate scope used for matching new items to opened Reports "chat scope" in UI, this type is not present in backend public func toChatScope() -> GroupChatScope { - switch self { + return switch self { case let .memberSupport(groupMember_): if let groupMember = groupMember_ { - return .memberSupport(groupMemberId_: groupMember.groupMemberId) + .memberSupport(groupMemberId_: groupMember.groupMemberId) } else { - return .memberSupport(groupMemberId_: nil) + .memberSupport(groupMemberId_: nil) } + case .reports: .reports } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index eb858d8d5f..44a625fd9d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -2943,12 +2943,6 @@ private fun archiveReports(chatRh: Long?, chatInfo: ChatInfo, itemIds: List Date: Fri, 5 Sep 2025 14:00:32 +0000 Subject: [PATCH 022/137] core, ui: correct member attention stat for support chat on opening it; mark support chat read (#6240) --- apps/ios/Shared/Model/AppAPITypes.swift | 3 + apps/ios/Shared/Model/SimpleXAPI.swift | 26 +++- .../Views/Chat/Group/MemberSupportView.swift | 10 ++ apps/ios/SimpleXChat/ChatTypes.swift | 9 ++ .../chat/simplex/common/model/ChatModel.kt | 6 + .../chat/simplex/common/model/SimpleXAPI.kt | 14 +- .../simplex/common/views/chat/ChatView.kt | 3 +- .../views/chat/group/MemberSupportView.kt | 25 ++++ .../views/chatlist/ChatListNavLinkView.kt | 3 +- .../commonMain/resources/MR/base/strings.xml | 1 + bots/src/API/Docs/Responses.hs | 1 + src/Simplex/Chat/Controller.hs | 1 + src/Simplex/Chat/Library/Commands.hs | 50 +++++-- src/Simplex/Chat/Store/Groups.hs | 12 +- src/Simplex/Chat/Store/Messages.hs | 125 ++++++++++++++---- .../SQLite/Migrations/chat_query_plans.txt | 65 ++++++--- src/Simplex/Chat/View.hs | 12 +- tests/ChatTests/Groups.hs | 109 ++++++++++++++- 18 files changed, 413 insertions(+), 62 deletions(-) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index d37d64646d..d8347f156b 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -912,6 +912,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { case leftMemberUser(user: UserRef, groupInfo: GroupInfo) case groupMembers(user: UserRef, group: SimpleXChat.Group) case memberAccepted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case memberSupportChatRead(user: UserRef, groupInfo: GroupInfo, member: GroupMember) case memberSupportChatDeleted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole) case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool) @@ -961,6 +962,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { case .leftMemberUser: "leftMemberUser" case .groupMembers: "groupMembers" case .memberAccepted: "memberAccepted" + case .memberSupportChatRead: "memberSupportChatRead" case .memberSupportChatDeleted: "memberSupportChatDeleted" case .membersRoleUser: "membersRoleUser" case .membersBlockedForAllUser: "membersBlockedForAllUser" @@ -1006,6 +1008,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupMembers(u, group): return withUser(u, String(describing: group)) case let .memberAccepted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .memberSupportChatRead(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") case let .memberSupportChatDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)") case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)") diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ae9f21e34b..6aff1e9c39 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1391,8 +1391,14 @@ func apiRejectContactRequest(contactReqId: Int64) async throws -> Contact? { throw r.unexpected } -func apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?) async throws { - try await sendCommandOkResp(.apiChatRead(type: type, id: id, scope: scope)) +func apiChatRead(type: ChatType, id: Int64) async throws { + try await sendCommandOkResp(.apiChatRead(type: type, id: id, scope: nil)) +} + +func apiSupportChatRead(type: ChatType, id: Int64, scope: GroupChatScope) async throws -> (GroupInfo, GroupMember) { + let r: ChatResponse2 = try await chatSendCmd(.apiChatRead(type: type, id: id, scope: scope)) + if case let .memberSupportChatRead(_, groupInfo, member) = r { return (groupInfo, member) } + throw r.unexpected } func apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> ChatInfo { @@ -1729,7 +1735,7 @@ func markChatRead(_ im: ItemsModel, _ chat: Chat) async { do { if chat.chatStats.unreadCount > 0 { let cInfo = chat.chatInfo - try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope()) + try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId) await MainActor.run { withAnimation { ChatModel.shared.markAllChatItemsRead(im, cInfo) } } @@ -1754,6 +1760,20 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async { } } +func markSupportChatRead(_ groupInfo: GroupInfo, _ member: GroupMember) async { + do { + if member.supportChatNotRead { + let (updatedGroupInfo, updatedMember) = try await apiSupportChatRead(type: .group, id: groupInfo.apiId, scope: .memberSupport(groupMemberId_: member.groupMemberId)) + await MainActor.run { + _ = ChatModel.shared.upsertGroupMember(updatedGroupInfo, updatedMember) + ChatModel.shared.updateGroup(updatedGroupInfo) + } + } + } catch { + logger.error("markSupportChatRead apiChatRead error: \(responseError(error))") + } +} + func apiMarkChatItemsRead(_ im: ItemsModel, _ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async { do { let updatedChatInfo = try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemIds: itemIds) diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift index 7f3672ea17..2101202ed8 100644 --- a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift @@ -92,6 +92,16 @@ struct MemberSupportView: View { .frame(width: 1, height: 1) .hidden() } + .if(!memberWithChat.wrapped.memberPending && memberWithChat.wrapped.supportChatNotRead) { v in + v.swipeActions(edge: .leading, allowsFullSwipe: true) { + Button { + Task { await markSupportChatRead(groupInfo, memberWithChat.wrapped) } + } label: { + Label("Read", systemImage: "checkmark") + } + .tint(theme.colors.primary) + } + } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if memberWithChat.wrapped.memberPending { Button { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 2e2f41ec99..9695e8e911 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2661,6 +2661,15 @@ public struct GroupMember: Identifiable, Decodable, Hashable { memberRole >= .moderator && versionRange.maxVersion >= REPORTS_VERSION } + public var supportChatNotRead: Bool { + if let supportChat = supportChat, + supportChat.memberAttention > 0 || supportChat.mentions > 0 || supportChat.unread > 0 { + true + } else { + false + } + } + public var versionRange: VersionRange { if let activeConn { activeConn.peerChatVRange diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 7bf849f8f7..628e768d57 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -2355,6 +2355,12 @@ data class GroupMember ( && !memberPending } + val supportChatNotRead: Boolean get() = + if (supportChat != null) + supportChat.memberAttention > 0 || supportChat.mentions > 0 || supportChat.unread > 0 + else + false + val versionRange: VersionRange = activeConn?.peerChatVRange ?: memberChatVRange val memberIncognito = memberProfile.profileId != memberContactProfileId diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 9b60a968b7..17ebaa6c96 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1849,13 +1849,20 @@ object ChatController { return null } - suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): Boolean { - val r = sendCmd(rh, CC.ApiChatRead(type, id, scope)) + suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean { + val r = sendCmd(rh, CC.ApiChatRead(type, id, scope = null)) if (r.result is CR.CmdOk) return true Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}") return false } + suspend fun apiSupportChatRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope): Pair? { + val r = sendCmd(rh, CC.ApiChatRead(type, id, scope)) + if (r is API.Result && r.res is CR.MemberSupportChatRead) return r.res.groupInfo to r.res.member + apiErrorAlert("apiSupportChatRead", generalGetString(MR.strings.error_marking_member_support_chat_read), r) + return null + } + suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List): ChatInfo? { val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, scope, itemIds)) if (r is API.Result && r.res is CR.ItemsReadForChat) return r.res.chatInfo @@ -6211,6 +6218,7 @@ sealed class CR { @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() @Serializable @SerialName("memberAccepted") class MemberAccepted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("memberSupportChatRead") class MemberSupportChatRead(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("memberSupportChatDeleted") class MemberSupportChatDeleted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("memberAcceptedByOther") class MemberAcceptedByOther(val user: UserRef, val groupInfo: GroupInfo, val acceptingMember: GroupMember, val member: GroupMember): CR() @Serializable @SerialName("memberRole") class MemberRole(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() @@ -6395,6 +6403,7 @@ sealed class CR { is GroupDeletedUser -> "groupDeletedUser" is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting" is MemberAccepted -> "memberAccepted" + is MemberSupportChatRead -> "memberSupportChatRead" is MemberSupportChatDeleted -> "memberSupportChatDeleted" is MemberAcceptedByOther -> "memberAcceptedByOther" is MemberRole -> "memberRole" @@ -6572,6 +6581,7 @@ sealed class CR { is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo)) is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member") is MemberAccepted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is MemberSupportChatRead -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is MemberSupportChatDeleted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is MemberAcceptedByOther -> withUser(user, "groupInfo: $groupInfo\nacceptingMember: $acceptingMember\nmember: $member") is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 44a625fd9d..b69c98887d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -707,8 +707,7 @@ fun ChatView( chatModel.controller.apiChatRead( chatRh, chatInfo.chatType, - chatInfo.apiId, - chatInfo.groupChatScope() + chatInfo.apiId ) } withContext(Dispatchers.Main) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt index 298a545c8c..e696128288 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt @@ -270,6 +270,12 @@ private fun DropDownMenuForSupportChat(rhId: Long?, member: GroupMember, groupIn showMenu.value = false }) } else { + if (member.supportChatNotRead) { + ItemAction(stringResource(MR.strings.mark_read), painterResource(MR.images.ic_check), color = MaterialTheme.colors.primary, onClick = { + markSupportChatRead(rhId, groupInfo, member) + showMenu.value = false + }) + } ItemAction(stringResource(MR.strings.delete_member_support_chat_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { deleteMemberSupportChatDialog(rhId, groupInfo, member) showMenu.value = false @@ -300,3 +306,22 @@ private fun deleteMemberSupportChat(rhId: Long?, groupInfo: GroupInfo, member: G } } } + +private fun markSupportChatRead(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) { + withBGApi { + if (member.supportChatNotRead) { + val r = chatModel.controller.apiSupportChatRead( + rh = rhId, + type = ChatType.Group, + id = groupInfo.apiId, + scope = GroupChatScope.MemberSupport(member.groupMemberId) + ) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second) + chatModel.chatsContext.updateGroup(rhId, r.first) + } + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 2128d1991b..4a8e9e5193 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -648,8 +648,7 @@ fun markChatRead(c: Chat) { chatModel.controller.apiChatRead( chat.remoteHostId, chat.chatInfo.chatType, - chat.chatInfo.apiId, - chat.chatInfo.groupChatScope() + chat.chatInfo.apiId ) chat = chatModel.getChat(chat.id) ?: return@withApi } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 151a2019be..401f69f9c0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -167,6 +167,7 @@ Error adding member(s) Error joining group Error accepting member + Error marking chat with member as read Error deleting chat with member Cannot receive file Sender cancelled file transfer. diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 1c2e4baec3..8d5cb9f348 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -162,6 +162,7 @@ undocumentedResponses = "CRGroupUserChanged", "CRItemsReadForChat", "CRJoinedGroupMember", + "CRMemberSupportChatRead", "CRMemberSupportChatDeleted", "CRMemberSupportChats", "CRNetworkConfig", diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 919274f4d2..d8a6f7a30a 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -726,6 +726,7 @@ data ChatResponse | CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]} | CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRMemberAccepted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRMemberSupportChatRead {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRMemberSupportChatDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRMembersRoleUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], toRole :: GroupMemberRole} | CRMembersBlockedForAllUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], blocked :: Bool} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index b6b7dbe87c..e1b0779e1f 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -517,13 +517,36 @@ processChatCommand vr nm = \case pure $ CRApiChat user (AChat SCTDirect directChat) navInfo CTGroup -> do (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId scope_ contentFilter pagination search) - pure $ CRApiChat user (AChat SCTGroup groupChat) navInfo + 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) pure $ CRApiChat user (AChat SCTLocal localChat) navInfo CTContactRequest -> throwCmdError "not implemented" CTContactConnection -> throwCmdError "not supported" + where + checkSupportChatAttention :: User -> Chat 'CTGroup -> CM (Chat 'CTGroup) + checkSupportChatAttention user groupChat@Chat {chatInfo, chatItems} = + case chatInfo of + GroupChat gInfo (Just GCSIMemberSupport {groupMember_ = Just scopeMem@GroupMember {supportChat = Just suppChat}}) -> do + case correctedMemAttention (groupMemberId' scopeMem) suppChat chatItems of + Just newMemAttention -> do + (gInfo', scopeMem') <- + withFastStore' $ \db -> setSupportChatMemberAttention db vr user gInfo scopeMem newMemAttention + pure $ groupChat {chatInfo = GroupChat gInfo' (Just $ GCSIMemberSupport (Just scopeMem'))} + Nothing -> pure groupChat + _ -> pure groupChat + where + correctedMemAttention :: GroupMemberId -> GroupSupportChat -> [CChatItem 'CTGroup] -> Maybe Int64 + correctedMemAttention scopeGMId GroupSupportChat {memberAttention} items = + let numNewFromMember = fromIntegral . length . takeWhile newFromMember $ reverse items + in if numNewFromMember == memberAttention then Nothing else Just numNewFromMember + where + newFromMember :: CChatItem 'CTGroup -> Bool + newFromMember (CChatItem _ ChatItem {chatDir = CIGroupRcv m, meta = CIMeta {itemStatus = CISRcvNew}}) = + groupMemberId' m == scopeGMId + newFromMember _ = False APIGetChatItems pagination search -> withUser $ \user -> do chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search pure $ CRChatItems user Nothing chatItems @@ -998,7 +1021,7 @@ processChatCommand vr nm = \case pure $ prefix <> formattedDate <> ext APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUserRead userId - APIChatRead chatRef@(ChatRef cType chatId scope) -> withUser $ \_ -> case cType of + APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of CTDirect -> do user <- withFastStore $ \db -> getUserByContactId db chatId ts <- liftIO getCurrentTime @@ -1014,12 +1037,23 @@ processChatCommand vr nm = \case gInfo <- getGroupInfo db vr user chatId pure (user, gInfo) ts <- liftIO getCurrentTime - timedItems <- withFastStore' $ \db -> do - timedItems <- getGroupUnreadTimedItems db user chatId - updateGroupChatItemsRead db user gInfo scope - setGroupChatItemsDeleteAt db user chatId timedItems ts - forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt - ok user + case scope_ of + Nothing -> do + timedItems <- withFastStore' $ \db -> do + timedItems <- getGroupUnreadTimedItems db user chatId Nothing + updateGroupChatItemsRead db user gInfo + setGroupChatItemsDeleteAt db user chatId timedItems ts + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt + ok user + Just scope -> do + scopeInfo <- getChatScopeInfo vr user scope + (gInfo', m', timedItems) <- withFastStore' $ \db -> do + timedItems <- getGroupUnreadTimedItems db user chatId (Just scope) + (gInfo', m') <- updateSupportChatItemsRead db vr user gInfo scopeInfo + timedItems' <- setGroupChatItemsDeleteAt db user chatId timedItems ts + pure (gInfo', m', timedItems') + forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt + pure $ CRMemberSupportChatRead user gInfo' m' CTLocal -> do user <- withFastStore $ \db -> getUserByNoteFolderId db chatId withFastStore' $ \db -> updateLocalChatItemsRead db user chatId diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index f7b07daad7..1b218df56d 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1495,13 +1495,21 @@ decreaseGroupMembersRequireAttention :: DB.Connection -> User -> GroupInfo -> IO decreaseGroupMembersRequireAttention db User {userId} g@GroupInfo {groupId, membersRequireAttention} = do DB.execute db +#if defined(dbPostgres) [sql| UPDATE groups - SET members_require_attention = members_require_attention - 1 + SET members_require_attention = GREATEST(0, members_require_attention - 1) WHERE user_id = ? AND group_id = ? |] +#else + [sql| + UPDATE groups + SET members_require_attention = MAX(0, members_require_attention - 1) + WHERE user_id = ? AND group_id = ? + |] +#endif (userId, groupId) - pure g {membersRequireAttention = membersRequireAttention - 1} + pure g {membersRequireAttention = max 0 (membersRequireAttention - 1)} increaseGroupMembersRequireAttention :: DB.Connection -> User -> GroupInfo -> IO GroupInfo increaseGroupMembersRequireAttention db User {userId} g@GroupInfo {groupId, membersRequireAttention} = do diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index d1c147d43c..c152bdc4bf 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -38,6 +38,7 @@ module Simplex.Chat.Store.Messages MemberAttention (..), updateChatTsStats, setSupportChatTs, + setSupportChatMemberAttention, createNewSndChatItem, createNewRcvChatItem, createNewChatItemNoMsg, @@ -79,6 +80,7 @@ module Simplex.Chat.Store.Messages setDirectChatItemRead, setDirectChatItemsDeleteAt, updateGroupChatItemsRead, + updateSupportChatItemsRead, getGroupUnreadTimedItems, updateGroupChatItemsReadList, updateGroupScopeUnreadStats, @@ -423,14 +425,23 @@ updateChatTsStats db vr user@User {userId} chatDirection chatTs chatStats_ = cas | not nowRequires && didRequire -> do DB.execute db +#if defined(dbPostgres) [sql| UPDATE groups SET chat_ts = ?, - members_require_attention = members_require_attention - 1 + members_require_attention = GREATEST(0, members_require_attention - 1) WHERE user_id = ? AND group_id = ? |] +#else + [sql| + UPDATE groups + SET chat_ts = ?, + members_require_attention = MAX(0, members_require_attention - 1) + WHERE user_id = ? AND group_id = ? + |] +#endif (chatTs, userId, groupId) - pure $ GroupChat g {membersRequireAttention = membersRequireAttention - 1, chatTs = Just chatTs} (Just $ GCSIMemberSupport (Just member')) + pure $ GroupChat g {membersRequireAttention = max 0 (membersRequireAttention - 1), chatTs = Just chatTs} (Just $ GCSIMemberSupport (Just member')) | otherwise -> do DB.execute db @@ -496,6 +507,21 @@ setSupportChatTs :: DB.Connection -> GroupMemberId -> UTCTime -> IO () setSupportChatTs db groupMemberId chatTs = DB.execute db "UPDATE group_members SET support_chat_ts = ? WHERE group_member_id = ?" (chatTs, groupMemberId) +setSupportChatMemberAttention :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> Int64 -> IO (GroupInfo, GroupMember) +setSupportChatMemberAttention db vr user g m memberAttention = do + m' <- updateGMAttention m + g' <- updateGroupMembersRequireAttention db user g m m' + pure (g', m') + where + updateGMAttention m@GroupMember {groupMemberId} = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE group_members SET support_chat_items_member_attention = ?, updated_at = ? WHERE group_member_id = ?" + (memberAttention, currentTs, groupMemberId' m) + m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId + pure $ either (const m) id m_ -- Left shouldn't happen, but types require it + createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt = createNewChatItem_ db user chatDirection False createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt @@ -2010,20 +2036,46 @@ setDirectChatItemsDeleteAt db User {userId} contactId itemIds currentTs = forM i (deleteAt, userId, contactId, chatItemId) pure (chatItemId, deleteAt) -updateGroupChatItemsRead :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScope -> IO () -updateGroupChatItemsRead db User {userId} GroupInfo {groupId, membership} scope = do +updateGroupChatItemsRead :: DB.Connection -> User -> GroupInfo -> IO () +updateGroupChatItemsRead db User {userId} GroupInfo {groupId, membership} = do currentTs <- getCurrentTime DB.execute db [sql| UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND item_status = ? + WHERE user_id = ? AND group_id = ? + AND item_status = ? |] (CISRcvRead, currentTs, userId, groupId, CISRcvNew) - case scope of - Nothing -> pure () - Just GCSMemberSupport {groupMemberId_} -> do - let gmId = fromMaybe (groupMemberId' membership) groupMemberId_ + +updateSupportChatItemsRead :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScopeInfo -> IO (GroupInfo, GroupMember) +updateSupportChatItemsRead db vr user@User {userId} g@GroupInfo {groupId, membership} scopeInfo = do + currentTs <- getCurrentTime + case scopeInfo of + GCSIMemberSupport {groupMember_} -> do + DB.execute + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? + AND item_status = ? + |] + (CISRcvRead, currentTs, userId, groupId, GCSTMemberSupport_, groupMemberId' <$> groupMember_, CISRcvNew) + case groupMember_ of + Nothing -> do + membership' <- updateGMStats membership + pure (g {membership = membership'}, membership') + Just member -> do + member' <- updateGMStats member + let didRequire = gmRequiresAttention member + nowRequires = gmRequiresAttention member' + if (not nowRequires && didRequire) + then (,member') <$> decreaseGroupMembersRequireAttention db user g + else pure (g, member') + where + updateGMStats m@GroupMember {groupMemberId} = do + currentTs <- getCurrentTime DB.execute db [sql| @@ -2033,18 +2085,34 @@ updateGroupChatItemsRead db User {userId} GroupInfo {groupId, membership} scope support_chat_items_mentions = 0 WHERE group_member_id = ? |] - (Only gmId) + (Only groupMemberId) + m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId + pure $ either (const m) id m_ -- Left shouldn't happen, but types require it -getGroupUnreadTimedItems :: DB.Connection -> User -> GroupId -> IO [(ChatItemId, Int)] -getGroupUnreadTimedItems db User {userId} groupId = - DB.query - db - [sql| - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - |] - (userId, groupId, CISRcvNew) +getGroupUnreadTimedItems :: DB.Connection -> User -> GroupId -> Maybe GroupChatScope -> IO [(ChatItemId, Int)] +getGroupUnreadTimedItems db User {userId} groupId scope = + case scope of + Nothing -> + DB.query + db + [sql| + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, groupId, CISRcvNew) + Just GCSMemberSupport {groupMemberId_} -> + DB.query + db + [sql| + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? + AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + |] + (userId, groupId, GCSTMemberSupport_, groupMemberId_, CISRcvNew) updateGroupChatItemsReadList :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> NonEmpty ChatItemId -> ExceptT StoreError IO ([(ChatItemId, Int)], GroupInfo) updateGroupChatItemsReadList db vr user@User {userId} g@GroupInfo {groupId} scopeInfo_ itemIds = do @@ -2110,14 +2178,25 @@ updateGroupScopeUnreadStats db vr user g@GroupInfo {membership} scopeInfo (unrea currentTs <- getCurrentTime DB.execute db +#if defined(dbPostgres) [sql| UPDATE group_members - SET support_chat_items_unread = support_chat_items_unread - ?, - support_chat_items_member_attention = support_chat_items_member_attention - ?, - support_chat_items_mentions = support_chat_items_mentions - ?, + SET support_chat_items_unread = GREATEST(0, support_chat_items_unread - ?), + support_chat_items_member_attention = GREATEST(0, support_chat_items_member_attention - ?), + support_chat_items_mentions = GREATEST(0, support_chat_items_mentions - ?), updated_at = ? WHERE group_member_id = ? |] +#else + [sql| + UPDATE group_members + SET support_chat_items_unread = MAX(0, support_chat_items_unread - ?), + support_chat_items_member_attention = MAX(0, support_chat_items_member_attention - ?), + support_chat_items_mentions = MAX(0, support_chat_items_mentions - ?), + updated_at = ? + WHERE group_member_id = ? + |] +#endif (unread, unanswered, mentions, currentTs, groupMemberId) m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId pure $ either (const m) id m_ -- Left shouldn't happen, but types require it diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 7eacc2337b..5a76567db0 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -25,7 +25,7 @@ SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE groups SET chat_ts = ?, - members_require_attention = members_require_attention + 1 + members_require_attention = MAX(0, members_require_attention - 1) WHERE user_id = ? AND group_id = ? Plan: @@ -34,7 +34,7 @@ SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE groups SET chat_ts = ?, - members_require_attention = members_require_attention - 1 + members_require_attention = members_require_attention + 1 WHERE user_id = ? AND group_id = ? Plan: @@ -1189,6 +1189,25 @@ Plan: SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?) USE TEMP B-TREE FOR ORDER BY +Query: + SELECT chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? + AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + +Plan: +SEARCH chat_items USING 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 chat_item_id, timed_ttl + FROM chat_items + WHERE user_id = ? AND group_id = ? + AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) + Query: SELECT chat_item_moderation_id, moderator_member_id, created_by_msg_id, moderated_at FROM chat_item_moderations @@ -1431,6 +1450,15 @@ SEARCH r USING INDEX idx_received_probes_user_id (user_id=?) SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN +Query: + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? + AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? + AND item_status = ? + +Plan: +SEARCH chat_items USING 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: UPDATE connections SET via_contact_uri = NULL, via_contact_uri_hash = NULL, xcontact_id = NULL WHERE user_id = ? AND via_group_link = 1 AND contact_id IN ( @@ -1482,9 +1510,9 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE group_members - SET support_chat_items_unread = support_chat_items_unread - ?, - support_chat_items_member_attention = support_chat_items_member_attention - ?, - support_chat_items_mentions = support_chat_items_mentions - ?, + SET support_chat_items_unread = MAX(0, support_chat_items_unread - ?), + support_chat_items_member_attention = MAX(0, support_chat_items_member_attention - ?), + support_chat_items_mentions = MAX(0, support_chat_items_mentions - ?), updated_at = ? WHERE group_member_id = ? @@ -4267,14 +4295,6 @@ Query: Plan: SEARCH chat_items USING INDEX idx_chat_items_contacts (user_id=? AND contact_id=? AND item_status=?) -Query: - SELECT chat_item_id, timed_ttl - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - -Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) - Query: SELECT group_snd_item_status, COUNT(1) FROM group_snd_item_statuses @@ -4361,7 +4381,8 @@ SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND item_status = ? + WHERE user_id = ? AND group_id = ? + AND item_status = ? Plan: SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) @@ -4623,7 +4644,7 @@ SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE groups - SET members_require_attention = members_require_attention + 1 + SET members_require_attention = MAX(0, members_require_attention - 1) WHERE user_id = ? AND group_id = ? Plan: @@ -4631,7 +4652,7 @@ SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE groups - SET members_require_attention = members_require_attention - 1 + SET members_require_attention = members_require_attention + 1 WHERE user_id = ? AND group_id = ? Plan: @@ -6277,6 +6298,14 @@ Query: UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_memb Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE group_members SET support_chat_items_member_attention = ?, updated_at = ? WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_members SET support_chat_items_member_attention=100 WHERE group_member_id=? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET support_chat_ts = ? WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -6317,6 +6346,10 @@ Query: UPDATE groups SET local_display_name = ?, updated_at = ? WHERE user_id = Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET members_require_attention=1 WHERE group_id=? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET request_shared_msg_id = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 6fef6cc204..f31daf233c 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -231,6 +231,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else [] CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m CRMemberAccepted u g m -> ttyUser u $ viewMemberAccepted g m + CRMemberSupportChatRead u g m -> ttyUser u $ viewSupportChatRead g m CRMemberSupportChatDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " support chat deleted"] CRMembersRoleUser u g members r' -> ttyUser u $ viewMemberRoleUserChanged g members r' CRMembersBlockedForAllUser u g members blocked -> ttyUser u $ viewMembersBlockedForAllUser g members blocked @@ -1229,6 +1230,11 @@ viewMemberAccepted g m@GroupMember {memberStatus} = case memberStatus of GSMemPendingReview -> [ttyGroup' g <> ": " <> ttyMember m <> " accepted and pending review (will introduce moderators)"] _ -> [ttyGroup' g <> ": " <> ttyMember m <> " accepted"] +viewSupportChatRead :: GroupInfo -> GroupMember -> [StyledString] +viewSupportChatRead g@GroupInfo {membership = GroupMember {groupMemberId = membershipId}} m + | groupMemberId' m == membershipId = [ttyGroup' g <> ": support chat read"] + | otherwise = [ttyGroup' g <> ": " <> ttyMember m <> " support chat read"] + viewMemberAcceptedByOther :: GroupInfo -> GroupMember -> GroupMember -> [StyledString] viewMemberAcceptedByOther g acceptingMember m@GroupMember {memberCategory, memberStatus} = case memberCategory of GCUserMember -> case memberStatus of @@ -1324,8 +1330,12 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt | otherwise = [] viewMemberSupportChats :: GroupInfo -> [GroupMember] -> [StyledString] -viewMemberSupportChats GroupInfo {membership} ms = support <> map groupMember ms +viewMemberSupportChats GroupInfo {membership = membership@GroupMember {memberRole = membershipRole}, membersRequireAttention} ms = + memsAttention <> support <> map groupMember ms where + memsAttention + | membershipRole >= GRModerator = ["members require attention: " <> sShow membersRequireAttention] + | otherwise = [] support = case supportChat membership of Just sc -> ["support: " <> chatStats sc] Nothing -> [] diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 365bd16f71..7f8761c1e1 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -18,6 +18,7 @@ import Control.Concurrent.Async (concurrently_) import Control.Monad (forM_, void, when) import Data.Bifunctor (second) import qualified Data.ByteString.Char8 as B +import Data.Int (Int64) import Data.List (intercalate, isInfixOf) import qualified Data.Map.Strict as M import qualified Data.Text as T @@ -219,6 +220,7 @@ chatGroupTests = do it "should send messages to admins and members" testSupportCLISendCommand it "should correctly maintain unread stats for support chats on reading chat items" testScopedSupportUnreadStatsOnRead it "should correctly maintain unread stats for support chats on deleting chat items" testScopedSupportUnreadStatsOnDelete + it "should correct member attention stat for support chat on opening it" testScopedSupportUnreadStatsCorrectOnOpen testGroupCheckMessages :: HasCallStack => TestParams -> IO () testGroupCheckMessages = @@ -7432,8 +7434,10 @@ testScopedSupportManyModerators = cath #$> ("/_get chat #1(_support:3) count=100", chat, []) alice ##> "/member support chats #team" + alice <## "members require attention: 0" alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" dan ##> "/member support chats #team" + dan <## "members require attention: 0" dan <## "bob (Bob) (id 3): unread: 0, require attention: 0, mentions: 0" bob ##> "/member support chats #team" bob <## "support: unread: 0, require attention: 0, mentions: 0" @@ -7888,8 +7892,10 @@ testScopedSupportUnreadStatsOnRead = dan <# "#team (support: bob) alice> 3" alice ##> "/member support chats #team" + alice <## "members require attention: 0" alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" dan ##> "/member support chats #team" + dan <## "members require attention: 0" dan <## "bob (Bob) (id 3): unread: 1, require attention: 0, mentions: 0" bob ##> "/member support chats #team" bob <## "support: unread: 1, require attention: 0, mentions: 0" @@ -7899,8 +7905,10 @@ testScopedSupportUnreadStatsOnRead = [alice, dan] *<# "#team (support: bob) bob> 4" alice ##> "/member support chats #team" + alice <## "members require attention: 1" alice <## "bob (Bob) (id 2): unread: 1, require attention: 1, mentions: 0" dan ##> "/member support chats #team" + dan <## "members require attention: 1" dan <## "bob (Bob) (id 3): unread: 2, require attention: 1, mentions: 0" bob ##> "/member support chats #team" bob <## "support: unread: 1, require attention: 0, mentions: 0" @@ -7913,9 +7921,11 @@ testScopedSupportUnreadStatsOnRead = bob <# "#team (support) dan> 5" alice ##> "/member support chats #team" + alice <## "members require attention: 0" alice <## "bob (Bob) (id 2): unread: 2, require attention: 0, mentions: 0" -- In test "answering" doesn't reset unanswered, but in UI items would be marked read on opening chat dan ##> "/member support chats #team" + dan <## "members require attention: 1" dan <## "bob (Bob) (id 3): unread: 2, require attention: 1, mentions: 0" bob ##> "/member support chats #team" bob <## "support: unread: 2, require attention: 0, mentions: 0" @@ -7928,8 +7938,10 @@ testScopedSupportUnreadStatsOnRead = bob <# "#team (support) dan> @alice 6" alice ##> "/member support chats #team" + alice <## "members require attention: 1" alice <## "bob (Bob) (id 2): unread: 3, require attention: 0, mentions: 1" dan ##> "/member support chats #team" + dan <## "members require attention: 1" dan <## "bob (Bob) (id 3): unread: 2, require attention: 1, mentions: 0" bob ##> "/member support chats #team" bob <## "support: unread: 3, require attention: 0, mentions: 0" @@ -7944,8 +7956,10 @@ testScopedSupportUnreadStatsOnRead = dan <# "#team (support: bob) bob> @alice 7" alice ##> "/member support chats #team" + alice <## "members require attention: 1" alice <## "bob (Bob) (id 2): unread: 4, require attention: 1, mentions: 2" dan ##> "/member support chats #team" + dan <## "members require attention: 1" dan <## "bob (Bob) (id 3): unread: 3, require attention: 2, mentions: 0" bob ##> "/member support chats #team" bob <## "support: unread: 3, require attention: 0, mentions: 0" @@ -7958,8 +7972,10 @@ testScopedSupportUnreadStatsOnRead = dan <# "#team (support: bob) bob!> @dan 8" alice ##> "/member support chats #team" + alice <## "members require attention: 1" alice <## "bob (Bob) (id 2): unread: 5, require attention: 2, mentions: 2" dan ##> "/member support chats #team" + dan <## "members require attention: 1" dan <## "bob (Bob) (id 3): unread: 4, require attention: 3, mentions: 1" bob ##> "/member support chats #team" bob <## "support: unread: 3, require attention: 0, mentions: 0" @@ -7967,11 +7983,13 @@ testScopedSupportUnreadStatsOnRead = alice #$> ("/_read chat items #1(_support:2) " <> aliceMentionedByDanItemId, id, "items read for chat") alice ##> "/member support chats #team" + alice <## "members require attention: 1" alice <## "bob (Bob) (id 2): unread: 4, require attention: 2, mentions: 1" alice #$> ("/_read chat items #1(_support:2) " <> aliceMentionedByBobItemId, id, "items read for chat") alice ##> "/member support chats #team" + alice <## "members require attention: 1" alice <## "bob (Bob) (id 2): unread: 3, require attention: 1, mentions: 0" threadDelay 1000000 @@ -7982,23 +8000,30 @@ testScopedSupportUnreadStatsOnRead = bob <# "#team (support) dan!> @bob 9" alice ##> "/member support chats #team" + alice <## "members require attention: 0" alice <## "bob (Bob) (id 2): unread: 4, require attention: 0, mentions: 0" dan ##> "/member support chats #team" + dan <## "members require attention: 1" dan <## "bob (Bob) (id 3): unread: 4, require attention: 3, mentions: 1" bob ##> "/member support chats #team" bob <## "support: unread: 4, require attention: 0, mentions: 1" - alice #$> ("/_read chat #1(_support:2)", id, "ok") + alice ##> "/_read chat #1(_support:2)" + alice <## "#team: bob support chat read" alice ##> "/member support chats #team" + alice <## "members require attention: 0" alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" - dan #$> ("/_read chat #1(_support:3)", id, "ok") + dan ##> "/_read chat #1(_support:3)" + dan <## "#team: bob support chat read" dan ##> "/member support chats #team" + dan <## "members require attention: 0" dan <## "bob (Bob) (id 3): unread: 0, require attention: 0, mentions: 0" - bob #$> ("/_read chat #1(_support)", id, "ok") + bob ##> "/_read chat #1(_support)" + bob <## "#team: support chat read" bob ##> "/member support chats #team" bob <## "support: unread: 0, require attention: 0, mentions: 0" @@ -8029,12 +8054,90 @@ testScopedSupportUnreadStatsOnDelete = msgIdBob <- lastItemId bob alice ##> "/member support chats #team" + alice <## "members require attention: 1" alice <## "bob (Bob) (id 2): unread: 1, require attention: 1, mentions: 0" bob #$> ("/_delete item #1(_support) " <> msgIdBob <> " broadcast", id, "message deleted") alice <# "#team (support: bob) bob> [deleted] 1" alice ##> "/member support chats #team" + alice <## "members require attention: 0" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" + where + opts = + testOpts + { markRead = False + } + +testScopedSupportUnreadStatsCorrectOnOpen :: HasCallStack => TestParams -> IO () +testScopedSupportUnreadStatsCorrectOnOpen = + testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + + bob #> "#team (support) 1" + alice <# "#team (support: bob) bob> 1" + + bob #> "#team (support) 2" + alice <# "#team (support: bob) bob> 2" + + alice ##> "/member support chats #team" + alice <## "members require attention: 1" + alice <## "bob (Bob) (id 2): unread: 2, require attention: 2, mentions: 0" + + alice ##> "/_read chat #1(_support:2)" + alice <## "#team: bob support chat read" + + alice ##> "/member support chats #team" + alice <## "members require attention: 0" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" + + bob #> "#team (support) 3" + alice <# "#team (support: bob) bob> 3" + + bob #> "#team (support) 4" + alice <# "#team (support: bob) bob> 4" + + bob #> "#team (support) 5" + alice <# "#team (support: bob) bob> 5" + + alice ##> "/member support chats #team" + alice <## "members require attention: 1" + alice <## "bob (Bob) (id 2): unread: 3, require attention: 3, mentions: 0" + + -- opening chat should correct group_members.support_chat_items_member_attention value if it got out of sync + void $ withCCTransaction alice $ \db -> + DB.execute db "UPDATE group_members SET support_chat_items_member_attention=100 WHERE group_member_id=?" (Only (2 :: Int64)) + + alice ##> "/member support chats #team" + alice <## "members require attention: 1" + alice <## "bob (Bob) (id 2): unread: 3, require attention: 100, mentions: 0" + + alice #$> ("/_get chat #1(_support:2) count=100", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")]) + + alice ##> "/member support chats #team" + alice <## "members require attention: 1" + alice <## "bob (Bob) (id 2): unread: 3, require attention: 3, mentions: 0" + + alice ##> "/_read chat #1(_support:2)" + alice <## "#team: bob support chat read" + + alice ##> "/member support chats #team" + alice <## "members require attention: 0" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" + + -- opening chat should also correct groups.members_require_attention value if corrected member no longer requires attention + void $ withCCTransaction alice $ \db -> do + DB.execute db "UPDATE group_members SET support_chat_items_member_attention=100 WHERE group_member_id=?" (Only (2 :: Int64)) + DB.execute db "UPDATE groups SET members_require_attention=1 WHERE group_id=?" (Only (1 :: Int64)) + + alice ##> "/member support chats #team" + alice <## "members require attention: 1" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 100, mentions: 0" + + alice #$> ("/_get chat #1(_support:2) count=100", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")]) + + alice ##> "/member support chats #team" + alice <## "members require attention: 0" alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" where opts = From 764ea39cdbf611912b8df05cac15d876bf5c8fbd Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Sun, 7 Sep 2025 07:30:59 +0000 Subject: [PATCH 023/137] ui: delay closing support view on acceptance for items to be marked read (#6244) * ui: delay closing support view on acceptance for items to be marked read * move to coroutine * increase delay --- apps/ios/Shared/Views/Chat/ChatView.swift | 5 ++++- .../kotlin/chat/simplex/common/model/SimpleXAPI.kt | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index f5348c9baf..fa53045391 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -360,7 +360,10 @@ struct ChatView: View { } .onChange(of: chatModel.secondaryPendingInviteeChatOpened) { opened in if im.secondaryIMFilter != nil && !opened { - dismiss() + Task { + try? await Task.sleep(nanoseconds: 650_000000) + dismiss() + } } } .onChange(of: chatModel.openAroundItemId) { openAround in diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 17ebaa6c96..30f3edb055 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2913,10 +2913,13 @@ object ChatController { && ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT) && chatModel.secondaryChatsContext.value?.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext ) { - withContext(Dispatchers.Main) { - chatModel.secondaryChatsContext.value = null + CoroutineScope(Dispatchers.Default).launch { + delay(1000L) + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value = null + } + ModalManager.end.closeModals() } - ModalManager.end.closeModals() } } is CR.JoinedGroupMember -> From 8798ede50a77f06ee3c3b7dfb97e5c92397cce96 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 7 Sep 2025 15:07:21 +0100 Subject: [PATCH 024/137] core: extended network errors information (simplexmq) (#6247) * core: extended network errors information (simplexmq) * fix test * docs: add NetworkError to bot API types * update simplexmq - JSON encoding for missing NetworkError --- .../src/Directory/Events.hs | 2 +- bots/api/TYPES.md | 30 +++++++++++ bots/src/API/Docs/Types.hs | 4 +- cabal.project | 2 +- .../types/typescript/src/types.ts | 50 +++++++++++++++++++ scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Library/Commands.hs | 2 +- src/Simplex/Chat/Library/Subscriber.hs | 2 +- src/Simplex/Chat/View.hs | 10 ++-- tests/ChatTests/Direct.hs | 8 +-- 10 files changed, 97 insertions(+), 15 deletions(-) diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 290742ce03..8ae7f60b3d 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -66,7 +66,7 @@ crDirectoryEvent :: Either ChatError ChatEvent -> Maybe DirectoryEvent crDirectoryEvent = \case Right evt -> crDirectoryEvent_ evt Left e -> case e of - ChatErrorAgent {agentError = BROKER _ NETWORK} -> Nothing + ChatErrorAgent {agentError = BROKER _ (NETWORK _)} -> Nothing ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing _ -> Just $ DELogChatResponse $ "chat error: " <> tshow e diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 4743ccb881..8353359484 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -121,6 +121,7 @@ This file is generated automatically. - [MsgFilter](#msgfilter) - [MsgReaction](#msgreaction) - [MsgReceiptStatus](#msgreceiptstatus) +- [NetworkError](#networkerror) - [NewUser](#newuser) - [NoteFolder](#notefolder) - [PendingContactConnection](#pendingcontactconnection) @@ -343,6 +344,7 @@ UNEXPECTED: NETWORK: - type: "NETWORK" +- networkError: [NetworkError](#networkerror) HOST: - type: "HOST" @@ -2635,6 +2637,34 @@ Unknown: - "badMsgHash" +--- + +## NetworkError + +**Discriminated union type**: + +ConnectError: +- type: "connectError" +- connectError: string + +TLSError: +- type: "tLSError" +- tlsError: string + +UnknownCAError: +- type: "unknownCAError" + +FailedError: +- type: "failedError" + +TimeoutError: +- type: "timeoutError" + +SubscribeError: +- type: "subscribeError" +- subscribeError: string + + --- ## NewUser diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 758fc62d53..83675798af 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -42,7 +42,7 @@ import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Client import Simplex.Messaging.Crypto.File import Simplex.Messaging.Parsers (dropPrefix, fstToLower) -import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), ProxyError (..)) +import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NetworkError (..), ProxyError (..)) import Simplex.Messaging.Transport import Simplex.RemoteControl.Types import System.Console.ANSI.Types (Color (..)) @@ -299,6 +299,7 @@ chatTypesDocsData = (sti @MsgFilter, STEnum, "MF", [], "", ""), (sti @MsgReaction, STUnion, "MR", [], "", ""), (sti @MsgReceiptStatus, STEnum, "MR", [], "", ""), + (sti @NetworkError, STUnion, "NE", [], "", ""), (sti @NewUser, STRecord, "", [], "", ""), (sti @NoteFolder, STRecord, "", [], "", ""), (sti @PendingContactConnection, STRecord, "", [], "", ""), @@ -492,6 +493,7 @@ deriving instance Generic MsgErrorType deriving instance Generic MsgFilter deriving instance Generic MsgReaction deriving instance Generic MsgReceiptStatus +deriving instance Generic NetworkError deriving instance Generic NewUser deriving instance Generic NoteFolder deriving instance Generic PendingContactConnection diff --git a/cabal.project b/cabal.project index 88f8b9aede..69982949dd 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 0319addd2b6bcd1206921c844f81fea4adc791a4 + tag: 2a61085f078417c6a0767d3e2d8162bac83c963c source-repository-package type: git diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 1511c3851f..3a056293c5 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -216,6 +216,7 @@ export namespace BrokerErrorType { export interface NETWORK extends Interface { type: "NETWORK" + networkError: NetworkError } export interface HOST extends Interface { @@ -2915,6 +2916,55 @@ export enum MsgReceiptStatus { BadMsgHash = "badMsgHash", } +export type NetworkError = + | NetworkError.ConnectError + | NetworkError.TLSError + | NetworkError.UnknownCAError + | NetworkError.FailedError + | NetworkError.TimeoutError + | NetworkError.SubscribeError + +export namespace NetworkError { + export type Tag = + | "connectError" + | "tLSError" + | "unknownCAError" + | "failedError" + | "timeoutError" + | "subscribeError" + + interface Interface { + type: Tag + } + + export interface ConnectError extends Interface { + type: "connectError" + connectError: string + } + + export interface TLSError extends Interface { + type: "tLSError" + tlsError: string + } + + export interface UnknownCAError extends Interface { + type: "unknownCAError" + } + + export interface FailedError extends Interface { + type: "failedError" + } + + export interface TimeoutError extends Interface { + type: "timeoutError" + } + + export interface SubscribeError extends Interface { + type: "subscribeError" + subscribeError: string + } +} + export interface NewUser { profile?: Profile pastTimestamp: boolean diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index c424aeb9fc..945805e68f 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."0319addd2b6bcd1206921c844f81fea4adc791a4" = "1rm0m6bch648xpa8l28m3ww6fi686g9ky68qc46gsqirpm29kh0q"; + "https://github.com/simplex-chat/simplexmq.git"."2a61085f078417c6a0767d3e2d8162bac83c963c" = "1zqssgz9d3bydcrnqlfy6aj7hfi65wb1mjcjpl3q744s82lrj68w"; "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"; diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index e1b0779e1f..d452255d71 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -4243,7 +4243,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do netStatus = maybe NSConnected $ NSError . errorNetworkStatus errorNetworkStatus :: ChatError -> String errorNetworkStatus = \case - ChatErrorAgent (BROKER _ NETWORK) _ -> "network" + ChatErrorAgent (BROKER _ (NETWORK _)) _ -> "network" ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted" e -> show e -- TODO possibly below could be replaced with less noisy events for API diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 3109fd5fe9..9ecc09004e 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1601,7 +1601,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = e -> SndErrOther $ tshow e where brokerError srvErr = \case - NETWORK -> SndErrExpired + NETWORK _ -> SndErrExpired TIMEOUT -> SndErrExpired HOST -> srvErr SrvErrHost SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index f31daf233c..89bc58171c 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -68,7 +68,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, BlockingInfo (..), BlockingReason (..), ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, BlockingInfo (..), BlockingReason (..), NetworkError (..), ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (safeDecodeUtf8, tshow) @@ -1515,11 +1515,11 @@ viewServerTestResult (AProtoServerWithAuth p _) = \case result <> [pName <> " server requires authorization to create queues, check password" | testStep == TSCreateQueue && (case testError of SMP _ SMP.AUTH -> True; _ -> False)] <> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && (case testError of XFTP _ XFTP.AUTH -> True; _ -> False)] - <> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr] + <> ["Certificate fingerprint in " <> pName <> " server address does not match server certificate" | testStep == TSConnect && unknownCA] where result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> sShow testError] - brokerErr = case testError of - BROKER _ NETWORK -> True + unknownCA = case testError of + BROKER _ (NETWORK NEUnknownCAError) -> True _ -> False _ -> [pName <> " server test passed"] where @@ -2546,7 +2546,7 @@ viewChatError isCmd logLevel testView = \case reasonStr = case reason of BRSpam -> "spam" BRContent -> "content violates conditions of use" - BROKER _ NETWORK | not isCmd -> [] + BROKER _ (NETWORK _) | not isCmd -> [] BROKER _ TIMEOUT | not isCmd -> [] AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd] AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning || isCmd] diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index cac90936a8..5d64514277 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1161,8 +1161,8 @@ testTestSMPServerConnection = alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" alice <## "SMP server test passed" alice ##> "/smp test smp://LcJU@localhost:7001" - alice <## "SMP server test failed at Connect, error: BROKER {brokerAddress = \"smp://LcJU@localhost:7001\", brokerErr = NETWORK}" - alice <## "Possibly, certificate fingerprint in SMP server address is incorrect" + alice <## "SMP server test failed at Connect, error: BROKER {brokerAddress = \"smp://LcJU@localhost:7001\", brokerErr = NETWORK {networkError = NEUnknownCAError}}" + alice <## "Certificate fingerprint in SMP server address does not match server certificate" testGetSetXFTPServers :: HasCallStack => TestParams -> IO () testGetSetXFTPServers = @@ -1203,8 +1203,8 @@ testTestXFTPServer = alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice <## "XFTP server test passed" alice ##> "/xftp test xftp://LcJU@localhost:7002" - alice <## "XFTP server test failed at Connect, error: BROKER {brokerAddress = \"xftp://LcJU@localhost:7002\", brokerErr = NETWORK}" - alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect" + alice <## "XFTP server test failed at Connect, error: BROKER {brokerAddress = \"xftp://LcJU@localhost:7002\", brokerErr = NETWORK {networkError = NEUnknownCAError}}" + alice <## "Certificate fingerprint in XFTP server address does not match server certificate" testOperators :: HasCallStack => TestParams -> IO () testOperators = From 634fe96db6353364c791ec20c239d96531a6ffe6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 7 Sep 2025 15:27:14 +0100 Subject: [PATCH 025/137] core: 6.4.5.0 (simplexmq 6.4.5.2) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cabal.project b/cabal.project index 69982949dd..8a861a1909 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 2a61085f078417c6a0767d3e2d8162bac83c963c + tag: 7e98b3103f4eb9a6a9a99604afb6f3a32ffc013d source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 945805e68f..a5f2790418 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."2a61085f078417c6a0767d3e2d8162bac83c963c" = "1zqssgz9d3bydcrnqlfy6aj7hfi65wb1mjcjpl3q744s82lrj68w"; + "https://github.com/simplex-chat/simplexmq.git"."7e98b3103f4eb9a6a9a99604afb6f3a32ffc013d" = "0g6lm65hs2kp2rsk9lqzj42nq51i5xynxrf16axma80cq0jqzxzl"; "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"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 8029915ace..463612cb69 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.4.4.2 +version: 6.4.5.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 3e5e25e430..47dce826ce 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 4, 3, 0] +minRemoteCtrlVersion = AppVersion [6, 4, 4, 2] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 4, 3, 0] +minRemoteHostVersion = AppVersion [6, 4, 4, 2] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version From 5ebd8cc193da9ed0d5c8137e2663176f4b256ab4 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 7 Sep 2025 22:20:27 +0100 Subject: [PATCH 026/137] core: fix GHC 8.10.7 (#6248) --- src/Simplex/Chat/Library/Commands.hs | 2 +- tests/ChatClient.hs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index d452255d71..b668a1d793 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -534,7 +534,7 @@ processChatCommand vr nm = \case Just newMemAttention -> do (gInfo', scopeMem') <- withFastStore' $ \db -> setSupportChatMemberAttention db vr user gInfo scopeMem newMemAttention - pure $ groupChat {chatInfo = GroupChat gInfo' (Just $ GCSIMemberSupport (Just scopeMem'))} + pure (groupChat {chatInfo = GroupChat gInfo' (Just $ GCSIMemberSupport (Just scopeMem'))} :: Chat 'CTGroup) Nothing -> pure groupChat _ -> pure groupChat where diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index b868671f3b..acf7903003 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -533,6 +533,7 @@ smpServerCfg = dailyBlockQueueQuota = 20, messageExpiration = Just defaultMessageExpiration, expireMessagesOnStart = False, + expireMessagesOnSend = False, idleQueueInterval = defaultIdleQueueInterval, notificationExpiration = defaultNtfExpiration, inactiveClientExpiration = Just defaultInactiveClientExpiration, From 9c6b9f7e4db318e7e565b471e5ee2e4ce1590308 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 7 Sep 2025 22:21:53 +0100 Subject: [PATCH 027/137] core: 6.4.5.1 --- simplex-chat.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 463612cb69..88a92e6bb4 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.4.5.0 +version: 6.4.5.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 478710e81ce5863598ac7245a140f4ddd2b4e591 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 7 Sep 2025 22:38:01 +0100 Subject: [PATCH 028/137] ios: update core library --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 4e7bd722c3..98f27ac2ed 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.5.1-B5MqGjdGsDB2dMXvZ9bdUJ.a */, ); path = Libraries; sourceTree = ""; From 99c919427fc084bb55c08739c1d82695dd333ca8 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 8 Sep 2025 10:11:02 +0100 Subject: [PATCH 029/137] ui: extended network error information (#6250) --- apps/ios/Shared/Model/AppAPITypes.swift | 10 +++--- apps/ios/Shared/Model/SimpleXAPI.swift | 3 ++ .../ar.xcloc/Localized Contents/ar.xliff | 12 +++---- .../bg.xcloc/Localized Contents/bg.xliff | 12 +++---- .../bn.xcloc/Localized Contents/bn.xliff | 12 +++---- .../cs.xcloc/Localized Contents/cs.xliff | 12 +++---- .../de.xcloc/Localized Contents/de.xliff | 12 +++---- .../el.xcloc/Localized Contents/el.xliff | 12 +++---- .../en.xcloc/Localized Contents/en.xliff | 18 +++++----- .../es.xcloc/Localized Contents/es.xliff | 12 +++---- .../fi.xcloc/Localized Contents/fi.xliff | 12 +++---- .../fr.xcloc/Localized Contents/fr.xliff | 12 +++---- .../he.xcloc/Localized Contents/he.xliff | 12 +++---- .../hr.xcloc/Localized Contents/hr.xliff | 8 ++--- .../hu.xcloc/Localized Contents/hu.xliff | 12 +++---- .../it.xcloc/Localized Contents/it.xliff | 12 +++---- .../ja.xcloc/Localized Contents/ja.xliff | 12 +++---- .../ko.xcloc/Localized Contents/ko.xliff | 8 ++--- .../lt.xcloc/Localized Contents/lt.xliff | 8 ++--- .../nl.xcloc/Localized Contents/nl.xliff | 12 +++---- .../pl.xcloc/Localized Contents/pl.xliff | 12 +++---- .../Localized Contents/pt-BR.xliff | 12 +++---- .../pt.xcloc/Localized Contents/pt.xliff | 12 +++---- .../ru.xcloc/Localized Contents/ru.xliff | 12 +++---- .../th.xcloc/Localized Contents/th.xliff | 12 +++---- .../tr.xcloc/Localized Contents/tr.xliff | 12 +++---- .../uk.xcloc/Localized Contents/uk.xliff | 12 +++---- .../Localized Contents/zh-Hans.xliff | 12 +++---- .../Localized Contents/zh-Hant.xliff | 12 +++---- apps/ios/SimpleXChat/APITypes.swift | 11 +++++- apps/ios/SimpleXChat/ErrorAlert.swift | 6 ++++ apps/ios/bg.lproj/Localizable.strings | 7 ++-- apps/ios/cs.lproj/Localizable.strings | 7 ++-- apps/ios/de.lproj/Localizable.strings | 7 ++-- apps/ios/es.lproj/Localizable.strings | 7 ++-- apps/ios/fi.lproj/Localizable.strings | 7 ++-- apps/ios/fr.lproj/Localizable.strings | 7 ++-- apps/ios/hu.lproj/Localizable.strings | 7 ++-- apps/ios/it.lproj/Localizable.strings | 7 ++-- apps/ios/ja.lproj/Localizable.strings | 7 ++-- apps/ios/nl.lproj/Localizable.strings | 7 ++-- apps/ios/pl.lproj/Localizable.strings | 7 ++-- apps/ios/ru.lproj/Localizable.strings | 7 ++-- apps/ios/th.lproj/Localizable.strings | 7 ++-- apps/ios/tr.lproj/Localizable.strings | 7 ++-- apps/ios/uk.lproj/Localizable.strings | 7 ++-- apps/ios/zh-Hans.lproj/Localizable.strings | 7 ++-- .../chat/simplex/common/model/SimpleXAPI.kt | 36 ++++++++++++++----- .../commonMain/resources/MR/base/strings.xml | 9 +++-- 49 files changed, 264 insertions(+), 241 deletions(-) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index d8347f156b..35b9bf033e 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -1990,13 +1990,13 @@ struct ProtocolTestFailure: Decodable, Error, Equatable { let err = String.localizedStringWithFormat(NSLocalizedString("Test failed at step %@.", comment: "server test failure"), testStep.text) switch testError { case .SMP(_, .AUTH): - return err + " " + NSLocalizedString("Server requires authorization to create queues, check password", comment: "server test error") + return err + " " + NSLocalizedString("Server requires authorization to create queues, check password.", comment: "server test error") case .XFTP(.AUTH): - return err + " " + NSLocalizedString("Server requires authorization to upload, check password", comment: "server test error") - case .BROKER(_, .NETWORK): - return err + " " + NSLocalizedString("Possibly, certificate fingerprint in server address is incorrect", comment: "server test error") + return err + " " + NSLocalizedString("Server requires authorization to upload, check password.", comment: "server test error") + case .BROKER(_, .NETWORK(.unknownCAError)): + return err + " " + NSLocalizedString("Fingerprint in server address does not match certificate.", comment: "server test error") default: - return err + return err + " " + String.localizedStringWithFormat(NSLocalizedString("Error: %@.", comment: "server test error"), String(describing: testError)) } } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6aff1e9c39..f95e6ac2dd 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -169,6 +169,7 @@ func retryableNetworkErrorAlert(_ e: ChatError) -> (title: String, message: Stri title: NSLocalizedString("Connection timeout", comment: "alert title"), message: serverErrorAlertMessage(addr) ) + case let .errorAgent(.BROKER(addr, .NETWORK(.unknownCAError))): nil case let .errorAgent(.BROKER(addr, .NETWORK)): ( title: NSLocalizedString("Connection error", comment: "alert title"), message: serverErrorAlertMessage(addr) @@ -177,6 +178,7 @@ func retryableNetworkErrorAlert(_ e: ChatError) -> (title: String, message: Stri title: NSLocalizedString("Private routing timeout", comment: "alert title"), message: proxyErrorAlertMessage(serverAddress) ) + case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.NETWORK(.unknownCAError))))): nil case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.NETWORK)))): ( title: NSLocalizedString("Private routing error", comment: "alert title"), message: proxyErrorAlertMessage(serverAddress) @@ -185,6 +187,7 @@ func retryableNetworkErrorAlert(_ e: ChatError) -> (title: String, message: Stri title: NSLocalizedString("Private routing timeout", comment: "alert title"), message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.NETWORK(.unknownCAError)))))): nil case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.NETWORK))))): ( title: NSLocalizedString("Private routing error", comment: "alert title"), message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff index 2331fc9a32..427430b833 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -2231,8 +2231,8 @@ We will be adding server redundancy to prevent lost messages. Please store passphrase securely, you will NOT be able to change it if you lose it. No comment provided by engineer.
- - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -2559,8 +2559,8 @@ We will be adding server redundancy to prevent lost messages. Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور server test error @@ -4249,8 +4249,8 @@ SimpleX servers cannot see your profile. Server type نوع الخادم - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. يتطلب الخادم إذنًا للرفع، تحقق من كلمة المرور diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 9ae115b867..cbcec3fbb3 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -5766,8 +5766,8 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен server test error @@ -6889,13 +6889,13 @@ chat item action Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Сървърът изисква оторизация за създаване на опашки, проверете паролата server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Сървърът изисква оторизация за качване, проверете паролата server test error diff --git a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff index c6420fb34d..fbda1abd29 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -2597,8 +2597,8 @@ Polish interface No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -3073,12 +3073,12 @@ Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. server test error diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index f94bc682e1..77b15adbbf 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -5558,8 +5558,8 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Je možné, že otisk certifikátu v adrese serveru je nesprávný server test error @@ -6661,13 +6661,13 @@ chat item action Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo server test error diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 46d854b31e..2eeb067648 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -6112,8 +6112,8 @@ Fehler: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig server test error @@ -7332,13 +7332,13 @@ chat item action Das Server-Protokoll wurde geändert. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung server test error diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index cb7a61636e..7a560bb41b 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -2350,8 +2350,8 @@ Available in v5.1 Polish interface No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -2726,12 +2726,12 @@ Available in v5.1 Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. server test error diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index a4c4cdecf4..eebc5ef100 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -6113,9 +6113,9 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. + Fingerprint in server address does not match certificate. server test error @@ -7333,14 +7333,14 @@ chat item action Server protocol changed. alert title - - Server requires authorization to create queues, check password - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. + Server requires authorization to upload, check password. server test error diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index ca7a31d5c3..c505c38c81 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -6112,8 +6112,8 @@ Error: %@ Puerto No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Posiblemente la huella del certificado en la dirección del servidor es incorrecta server test error @@ -7332,13 +7332,13 @@ chat item action El protocolo del servidor ha cambiado. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. El servidor requiere autorización para crear colas, comprueba la contraseña server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. El servidor requiere autorización para subir, comprueba la contraseña server test error diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index f5b29745cc..8c2afed258 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -5524,8 +5524,8 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen server test error @@ -6626,13 +6626,13 @@ chat item action Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana server test error diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 27c73ddc54..406bd0bf75 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -6022,8 +6022,8 @@ Erreur : %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte server test error @@ -7207,13 +7207,13 @@ chat item action Le protocole du serveur a été modifié. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe server test error diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index a2170ab941..f94d6cefd8 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -2876,8 +2876,8 @@ Available in v5.1 Polish interface No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -3252,12 +3252,12 @@ Available in v5.1 Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. server test error diff --git a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff index 5f491fe76e..2aa945f603 100644 --- a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff +++ b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff @@ -2043,8 +2043,8 @@ We will be adding server redundancy to prevent lost messages. Please store passphrase securely, you will NOT be able to change it if you lose it. No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -2371,8 +2371,8 @@ We will be adding server redundancy to prevent lost messages. Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 71bf7d382b..91161f6c6c 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -6112,8 +6112,8 @@ Hiba: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen server test error @@ -7332,13 +7332,13 @@ chat item action A kiszolgálóprotokoll módosult. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát server test error diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index af5fced837..a46fab948d 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -6112,8 +6112,8 @@ Errore: %@ Porta No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata server test error @@ -7332,13 +7332,13 @@ chat item action Il protocollo del server è cambiato. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Il server richiede l'autorizzazione di creare code, controlla la password server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Il server richiede l'autorizzazione per il caricamento, controllare la password server test error diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index ec3f33433d..bcb302c233 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -5610,8 +5610,8 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. サーバアドレスの証明証IDが正しくないかもしれません server test error @@ -6705,13 +6705,13 @@ chat item action Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. キューを作成するにはサーバーの認証が必要です。パスワードを確認してください server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. アップロードにはサーバーの認証が必要です。パスワードを確認してください server test error diff --git a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff index 655b01dfe3..ca51a875c7 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -2250,8 +2250,8 @@ We will be adding server redundancy to prevent lost messages. Please store passphrase securely, you will NOT be able to change it if you lose it. No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -2603,8 +2603,8 @@ We will be adding server redundancy to prevent lost messages. Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error diff --git a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff index 87823abeca..4b51d66a34 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -2047,8 +2047,8 @@ We will be adding server redundancy to prevent lost messages. Please store passphrase securely, you will NOT be able to change it if you lose it. No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -2375,8 +2375,8 @@ We will be adding server redundancy to prevent lost messages. Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 83656910fe..81928bf80d 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -6069,8 +6069,8 @@ Fout: %@ Poort No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Mogelijk is de certificaat vingerafdruk in het server adres onjuist server test error @@ -7278,13 +7278,13 @@ chat item action Serverprotocol gewijzigd. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Server vereist autorisatie om te uploaden, wachtwoord controleren server test error diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 6133961dfd..e68db5988d 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -5922,8 +5922,8 @@ Błąd: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy server test error @@ -7100,13 +7100,13 @@ chat item action Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Serwer wymaga autoryzacji do przesłania, sprawdź hasło server test error diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index 31b220a17d..d9af0624bf 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -2352,8 +2352,8 @@ We will be adding server redundancy to prevent lost messages. Guarde a senha em um local seguro, você NÃO poderá alterá-la se a perder. No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Possivelmente, a impressão digital do certificado no endereço do servidor está incorreta server test error @@ -2727,8 +2727,8 @@ We will be adding server redundancy to prevent lost messages. Mensagens enviadas serão excluídas depois do tempo definido. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. O servidor requer autorização para criar filas, verifique a senha server test error @@ -4610,8 +4610,8 @@ Disponível na 5.1 Salvar e atualizar perfil do grupo No comment provided by engineer. - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. O servidor requer autorização para fazer upload, verifique a senha server test error diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index 01b8f7e244..e4fac55bcb 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -2453,8 +2453,8 @@ Available in v5.1 Polish interface No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. server test error @@ -2829,12 +2829,12 @@ Available in v5.1 Sent messages will be deleted after set time. No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. server test error diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 7afe44aa1f..71c574d0cc 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -6111,8 +6111,8 @@ Error: %@ Порт No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Возможно, хэш сертификата в адресе сервера неверный server test error @@ -7331,13 +7331,13 @@ chat item action Протокол сервера изменен. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Сервер требует авторизации для создания очередей, проверьте пароль server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Сервер требует авторизации для загрузки, проверьте пароль server test error diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index fbea584f76..44eb673c6c 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -5503,8 +5503,8 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง server test error @@ -6601,13 +6601,13 @@ chat item action Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน server test error diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 870b633488..2842245a7f 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -6098,8 +6098,8 @@ Hata: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil server test error @@ -7317,13 +7317,13 @@ chat item action Sunucu protokolü değişti. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Sunucunun yükleme yapması için yetki gereklidir, şifreyi kontrol edin server test error diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 16457e05cb..7e8c4cf8df 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -6098,8 +6098,8 @@ Error: %@ Порт No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. Можливо, в адресі сервера неправильно вказано відбиток сертифіката server test error @@ -7317,13 +7317,13 @@ chat item action Протокол сервера змінено. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. Сервер вимагає авторизації для створення черг, перевірте пароль server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. Сервер вимагає авторизації для завантаження, перевірте пароль server test error diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 852d1f0583..4b506a0b08 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -6046,8 +6046,8 @@ Error: %@ Port No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. 服务器地址中的证书指纹可能不正确 server test error @@ -7223,13 +7223,13 @@ chat item action Server protocol changed. alert title - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. 服务器需要授权才能创建队列,检查密码 server test error - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. 服务器需要授权来上传,检查密码 server test error diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index ac3a01c965..0e4e383b52 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -2367,8 +2367,8 @@ We will be adding server redundancy to prevent lost messages. 請放置你的密碼於安全的地方,如果你遺失了密碼,將不可能修改你的密碼。 No comment provided by engineer. - - Possibly, certificate fingerprint in server address is incorrect + + Fingerprint in server address does not match certificate. 伺服器地址的憑證指紋可能不正確 server test error @@ -2757,8 +2757,8 @@ We will be adding server redundancy to prevent lost messages. 已傳送的訊息將在設定的時間後被刪除。 No comment provided by engineer. - - Server requires authorization to create queues, check password + + Server requires authorization to create queues, check password. 伺服器需要授權才能建立佇列,請檢查密碼 server test error @@ -5130,8 +5130,8 @@ Available in v5.1 儲存歡迎訊息? No comment provided by engineer. - - Server requires authorization to upload, check password + + Server requires authorization to upload, check password. 伺服器需要認證後才能上傳,檢查密碼 server test error diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 5335a04ae3..34ce1cf84f 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -858,7 +858,7 @@ public enum ConnectionErrorType: Decodable, Hashable { public enum BrokerErrorType: Decodable, Hashable { case RESPONSE(smpErr: String) case UNEXPECTED - case NETWORK + case NETWORK(networkError: NetworkError) case HOST case TRANSPORT(transportErr: ProtocolTransportError) case TIMEOUT @@ -954,6 +954,15 @@ public enum ProtocolCommandError: Decodable, Hashable { case NO_ENTITY } +public enum NetworkError: Decodable, Hashable { + case connectError(connectError: String) + case tLSError(tlsError: String) + case unknownCAError + case failedError + case timeoutError + case subscribeError(subscribeError: String) +} + public enum ProtocolTransportError: Decodable, Hashable { case badBlock case version diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift index 2f2dd31a83..2920c2383c 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -87,6 +87,8 @@ public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? { switch e { case let .errorAgent(.BROKER(addr, .TIMEOUT)): ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + case let .errorAgent(.BROKER(addr, .NETWORK(.unknownCAError))): + ErrorAlert(title: "Connection error", message: "Fingerprint in server address does not match certificate: \(serverHostname(addr)).") case let .errorAgent(.BROKER(addr, .NETWORK)): ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") case let .errorAgent(.BROKER(addr, .HOST)): @@ -105,6 +107,8 @@ private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> Er switch proxyErr { case .BROKER(brokerErr: .TIMEOUT): return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + case .BROKER(brokerErr: .NETWORK(.unknownCAError)): + return ErrorAlert(title: "Private routing error", message: "Fingerprint in forwarding server address does not match certificate: \(serverHostname(srvAddr)).") case .BROKER(brokerErr: .NETWORK): return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") case .BROKER(brokerErr: .HOST): @@ -120,6 +124,8 @@ private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: S switch proxyErr { case .BROKER(brokerErr: .TIMEOUT): return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .BROKER(brokerErr: .NETWORK(.unknownCAError)): + return ErrorAlert(title: "Private routing error", message: "Fingerprint in destination server address does not match certificate: \(serverHostname(relayServer)).") case .BROKER(brokerErr: .NETWORK): return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") case .NO_SESSION: diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 186d7e4224..0c89d57dc4 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -2981,7 +2981,7 @@ new chat action */ "Polish interface" = "Полски интерфейс"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; +"Fingerprint in server address does not match certificate." = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Запазете последната чернова на съобщението с прикачени файлове."; @@ -3470,10 +3470,10 @@ chat item action */ "Sent messages will be deleted after set time." = "Изпратените съобщения ще бъдат изтрити след зададеното време."; /* server test error */ -"Server requires authorization to create queues, check password" = "Сървърът изисква оторизация за създаване на опашки, проверете паролата"; +"Server requires authorization to create queues, check password." = "Сървърът изисква оторизация за създаване на опашки, проверете паролата"; /* server test error */ -"Server requires authorization to upload, check password" = "Сървърът изисква оторизация за качване, проверете паролата"; +"Server requires authorization to upload, check password." = "Сървърът изисква оторизация за качване, проверете паролата"; /* No comment provided by engineer. */ "Server test failed!" = "Тестът на сървъра е неуспешен!"; @@ -4453,4 +4453,3 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "Вашият адрес в SimpleX"; - diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 47bfd2aa12..a401c4bcf9 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -2337,7 +2337,7 @@ new chat action */ "Polish interface" = "Polské rozhraní"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Je možné, že otisk certifikátu v adrese serveru je nesprávný"; +"Fingerprint in server address does not match certificate." = "Je možné, že otisk certifikátu v adrese serveru je nesprávný"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachování posledního návrhu zprávy s přílohami."; @@ -2751,10 +2751,10 @@ chat item action */ "Sent messages will be deleted after set time." = "Odeslané zprávy se po uplynutí nastavené doby odstraní."; /* server test error */ -"Server requires authorization to create queues, check password" = "Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo"; +"Server requires authorization to create queues, check password." = "Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo"; /* server test error */ -"Server requires authorization to upload, check password" = "Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo"; +"Server requires authorization to upload, check password." = "Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo"; /* No comment provided by engineer. */ "Server test failed!" = "Test serveru se nezdařil!"; @@ -3506,4 +3506,3 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "Vaše SimpleX adresa"; - diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 40194684e6..d6193a324f 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -4073,7 +4073,7 @@ new chat action */ "Port" = "Port"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig"; +"Fingerprint in server address does not match certificate." = "Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren."; @@ -4877,10 +4877,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "Server-Warteschlangen-Information: %1$@\n\nZuletzt empfangene Nachricht: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort"; +"Server requires authorization to create queues, check password." = "Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort"; /* server test error */ -"Server requires authorization to upload, check password" = "Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung"; +"Server requires authorization to upload, check password." = "Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung"; /* No comment provided by engineer. */ "Server test failed!" = "Server Test ist fehlgeschlagen!"; @@ -6350,4 +6350,3 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Ihre SimpleX-Adresse"; - diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 8b8331e097..4293429d64 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -4073,7 +4073,7 @@ new chat action */ "Port" = "Puerto"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Posiblemente la huella del certificado en la dirección del servidor es incorrecta"; +"Fingerprint in server address does not match certificate." = "Posiblemente la huella del certificado en la dirección del servidor es incorrecta"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva el último borrador del mensaje con los datos adjuntos."; @@ -4877,10 +4877,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "información cola del servidor: %1$@\n\núltimo mensaje recibido: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "El servidor requiere autorización para crear colas, comprueba la contraseña"; +"Server requires authorization to create queues, check password." = "El servidor requiere autorización para crear colas, comprueba la contraseña"; /* server test error */ -"Server requires authorization to upload, check password" = "El servidor requiere autorización para subir, comprueba la contraseña"; +"Server requires authorization to upload, check password." = "El servidor requiere autorización para subir, comprueba la contraseña"; /* No comment provided by engineer. */ "Server test failed!" = "¡Prueba no superada!"; @@ -6350,4 +6350,3 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Mi dirección SimpleX"; - diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 670607daa9..5bda2a0438 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -2241,7 +2241,7 @@ new chat action */ "Polish interface" = "Puolalainen käyttöliittymä"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; +"Fingerprint in server address does not match certificate." = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Säilytä viimeinen viestiluonnos liitteineen."; @@ -2652,10 +2652,10 @@ chat item action */ "Sent messages will be deleted after set time." = "Lähetetyt viestit poistetaan asetetun ajan kuluttua."; /* server test error */ -"Server requires authorization to create queues, check password" = "Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana"; +"Server requires authorization to create queues, check password." = "Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana"; /* server test error */ -"Server requires authorization to upload, check password" = "Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana"; +"Server requires authorization to upload, check password." = "Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana"; /* No comment provided by engineer. */ "Server test failed!" = "Palvelintesti epäonnistui!"; @@ -3401,4 +3401,3 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "SimpleX-osoitteesi"; - diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 09bebeddd9..90ac30f87d 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -3752,7 +3752,7 @@ new chat action */ "Port" = "Port"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; +"Fingerprint in server address does not match certificate." = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserver le brouillon du dernier message, avec les pièces jointes."; @@ -4427,10 +4427,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "info sur la file d'attente du serveur : %1$@\n\ndernier message reçu : %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe"; +"Server requires authorization to create queues, check password." = "Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe"; /* server test error */ -"Server requires authorization to upload, check password" = "Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe"; +"Server requires authorization to upload, check password." = "Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe"; /* No comment provided by engineer. */ "Server test failed!" = "Échec du test du serveur !"; @@ -5743,4 +5743,3 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "Votre adresse SimpleX"; - diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index a137d03d8c..f46353e1ad 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -4073,7 +4073,7 @@ new chat action */ "Port" = "Port"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen"; +"Fingerprint in server address does not match certificate." = "Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt."; @@ -4877,10 +4877,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló várólista információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát"; +"Server requires authorization to create queues, check password." = "A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze a jelszavát"; /* server test error */ -"Server requires authorization to upload, check password" = "A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát"; +"Server requires authorization to upload, check password." = "A kiszolgálónak hitelesítésre van szüksége a feltöltéshez, ellenőrizze jelszavát"; /* No comment provided by engineer. */ "Server test failed!" = "Sikertelen kiszolgáló teszt!"; @@ -6350,4 +6350,3 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Profil SimpleX-címe"; - diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 02920a4714..57954be773 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -4073,7 +4073,7 @@ new chat action */ "Port" = "Porta"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata"; +"Fingerprint in server address does not match certificate." = "Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva la bozza dell'ultimo messaggio, con gli allegati."; @@ -4877,10 +4877,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "info coda server: %1$@\n\nultimo msg ricevuto: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Il server richiede l'autorizzazione di creare code, controlla la password"; +"Server requires authorization to create queues, check password." = "Il server richiede l'autorizzazione di creare code, controlla la password"; /* server test error */ -"Server requires authorization to upload, check password" = "Il server richiede l'autorizzazione per il caricamento, controllare la password"; +"Server requires authorization to upload, check password." = "Il server richiede l'autorizzazione per il caricamento, controllare la password"; /* No comment provided by engineer. */ "Server test failed!" = "Test del server fallito!"; @@ -6350,4 +6350,3 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Il tuo indirizzo SimpleX"; - diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index b6c658c7f6..33e8a650bc 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -2487,7 +2487,7 @@ new chat action */ "Polish interface" = "ポーランド語UI"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "サーバアドレスの証明証IDが正しくないかもしれません"; +"Fingerprint in server address does not match certificate." = "サーバアドレスの証明証IDが正しくないかもしれません"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "添付を含めて、下書きを保存する。"; @@ -2877,10 +2877,10 @@ chat item action */ "Sent messages will be deleted after set time." = "一定時間が経ったら送信されたメッセージが削除されます。"; /* server test error */ -"Server requires authorization to create queues, check password" = "キューを作成するにはサーバーの認証が必要です。パスワードを確認してください"; +"Server requires authorization to create queues, check password." = "キューを作成するにはサーバーの認証が必要です。パスワードを確認してください"; /* server test error */ -"Server requires authorization to upload, check password" = "アップロードにはサーバーの認証が必要です。パスワードを確認してください"; +"Server requires authorization to upload, check password." = "アップロードにはサーバーの認証が必要です。パスワードを確認してください"; /* No comment provided by engineer. */ "Server test failed!" = "サーバテスト失敗!"; @@ -3629,4 +3629,3 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "あなたのSimpleXアドレス"; - diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 147b12d7ab..3bc8ec7493 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -3938,7 +3938,7 @@ new chat action */ "Port" = "Poort"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Mogelijk is de certificaat vingerafdruk in het server adres onjuist"; +"Fingerprint in server address does not match certificate." = "Mogelijk is de certificaat vingerafdruk in het server adres onjuist"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Bewaar het laatste berichtconcept, met bijlagen."; @@ -4700,10 +4700,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "informatie over serverwachtrij: %1$@\n\nlaatst ontvangen bericht: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord"; +"Server requires authorization to create queues, check password." = "Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord"; /* server test error */ -"Server requires authorization to upload, check password" = "Server vereist autorisatie om te uploaden, wachtwoord controleren"; +"Server requires authorization to upload, check password." = "Server vereist autorisatie om te uploaden, wachtwoord controleren"; /* No comment provided by engineer. */ "Server test failed!" = "Servertest mislukt!"; @@ -6080,4 +6080,3 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Uw SimpleX adres"; - diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 3a706400e0..44530d9217 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -3464,7 +3464,7 @@ new chat action */ "Port" = "Port"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy"; +"Fingerprint in server address does not match certificate." = "Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami."; @@ -4115,10 +4115,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "Informacje kolejki serwera: %1$@\n\nostatnia otrzymana wiadomość: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło"; +"Server requires authorization to create queues, check password." = "Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło"; /* server test error */ -"Server requires authorization to upload, check password" = "Serwer wymaga autoryzacji do przesłania, sprawdź hasło"; +"Server requires authorization to upload, check password." = "Serwer wymaga autoryzacji do przesłania, sprawdź hasło"; /* No comment provided by engineer. */ "Server test failed!" = "Test serwera nie powiódł się!"; @@ -5332,4 +5332,3 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "Twój adres SimpleX"; - diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index e3188c762f..225b85e467 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -4073,7 +4073,7 @@ new chat action */ "Port" = "Порт"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Возможно, хэш сертификата в адресе сервера неверный"; +"Fingerprint in server address does not match certificate." = "Возможно, хэш сертификата в адресе сервера неверный"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Сохранить последний черновик, вместе с вложениями."; @@ -4877,10 +4877,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "информация сервера об очереди: %1$@\n\nпоследнее полученное сообщение: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Сервер требует авторизации для создания очередей, проверьте пароль"; +"Server requires authorization to create queues, check password." = "Сервер требует авторизации для создания очередей, проверьте пароль"; /* server test error */ -"Server requires authorization to upload, check password" = "Сервер требует авторизации для загрузки, проверьте пароль"; +"Server requires authorization to upload, check password." = "Сервер требует авторизации для загрузки, проверьте пароль"; /* No comment provided by engineer. */ "Server test failed!" = "Ошибка теста сервера!"; @@ -6347,4 +6347,3 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваш адрес SimpleX"; - diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index ef5d68b0fb..888a1bfa25 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -2181,7 +2181,7 @@ new chat action */ "Polish interface" = "อินเตอร์เฟซภาษาโปแลนด์"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; +"Fingerprint in server address does not match certificate." = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ"; @@ -2580,10 +2580,10 @@ chat item action */ "Sent messages will be deleted after set time." = "ข้อความที่ส่งจะถูกลบหลังเกินเวลาที่กําหนด"; /* server test error */ -"Server requires authorization to create queues, check password" = "เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน"; +"Server requires authorization to create queues, check password." = "เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน"; /* server test error */ -"Server requires authorization to upload, check password" = "เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน"; +"Server requires authorization to upload, check password." = "เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน"; /* No comment provided by engineer. */ "Server test failed!" = "การทดสอบเซิร์ฟเวอร์ล้มเหลว!"; @@ -3305,4 +3305,3 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "ที่อยู่ SimpleX ของคุณ"; - diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index b91b6e3dd1..39a3d0e0d3 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -4031,7 +4031,7 @@ new chat action */ "Port" = "Port"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; +"Fingerprint in server address does not match certificate." = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Son mesaj taslağını ekleriyle birlikte koru."; @@ -4826,10 +4826,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "sunucu kuyruk bilgisi: %1$@\n\nson alınan msj: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin"; +"Server requires authorization to create queues, check password." = "Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin"; /* server test error */ -"Server requires authorization to upload, check password" = "Sunucunun yükleme yapması için yetki gereklidir, şifreyi kontrol edin"; +"Server requires authorization to upload, check password." = "Sunucunun yükleme yapması için yetki gereklidir, şifreyi kontrol edin"; /* No comment provided by engineer. */ "Server test failed!" = "Sunucu testinde hata oluştu!"; @@ -6239,4 +6239,3 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "SimpleX adresin"; - diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index bf8966bd08..7fb9f932a0 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -4031,7 +4031,7 @@ new chat action */ "Port" = "Порт"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; +"Fingerprint in server address does not match certificate." = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Зберегти чернетку останнього повідомлення з вкладеннями."; @@ -4826,10 +4826,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "інформація про чергу на сервері: %1$@\n\nостаннє отримане повідомлення: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "Сервер вимагає авторизації для створення черг, перевірте пароль"; +"Server requires authorization to create queues, check password." = "Сервер вимагає авторизації для створення черг, перевірте пароль"; /* server test error */ -"Server requires authorization to upload, check password" = "Сервер вимагає авторизації для завантаження, перевірте пароль"; +"Server requires authorization to upload, check password." = "Сервер вимагає авторизації для завантаження, перевірте пароль"; /* No comment provided by engineer. */ "Server test failed!" = "Тест сервера завершився невдало!"; @@ -6287,4 +6287,3 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваша адреса SimpleX"; - diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index afd9ac8dc0..ac5bad738c 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -3827,7 +3827,7 @@ new chat action */ "Polish interface" = "波兰语界面"; /* server test error */ -"Possibly, certificate fingerprint in server address is incorrect" = "服务器地址中的证书指纹可能不正确"; +"Fingerprint in server address does not match certificate." = "服务器地址中的证书指纹可能不正确"; /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "保留最后的消息草稿及其附件。"; @@ -4478,10 +4478,10 @@ chat item action */ "server queue info: %@\n\nlast received msg: %@" = "服务器队列信息: %1$@\n\n上次收到的消息: %2$@"; /* server test error */ -"Server requires authorization to create queues, check password" = "服务器需要授权才能创建队列,检查密码"; +"Server requires authorization to create queues, check password." = "服务器需要授权才能创建队列,检查密码"; /* server test error */ -"Server requires authorization to upload, check password" = "服务器需要授权来上传,检查密码"; +"Server requires authorization to upload, check password." = "服务器需要授权来上传,检查密码"; /* No comment provided by engineer. */ "Server test failed!" = "服务器测试失败!"; @@ -5668,4 +5668,3 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "您的 SimpleX 地址"; - diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 30f3edb055..ea740f238b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -740,7 +740,7 @@ object ChatController { val message = { String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.brokerAddress)) } return when (e.brokerErr) { is BrokerErrorType.TIMEOUT -> MR.strings.connection_timeout to message() - is BrokerErrorType.NETWORK -> MR.strings.connection_error to message() + is BrokerErrorType.NETWORK -> if (e.brokerErr.networkError is NetworkError.UnknownCAError) null else MR.strings.connection_error to message() else -> null } } @@ -749,7 +749,7 @@ object ChatController { val message = { String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(e.serverAddress)) } return when (e.smpErr.proxyErr.brokerErr) { is BrokerErrorType.TIMEOUT -> MR.strings.private_routing_timeout to message() - is BrokerErrorType.NETWORK -> MR.strings.private_routing_error to message() + is BrokerErrorType.NETWORK -> if (e.smpErr.proxyErr.brokerErr.networkError is NetworkError.UnknownCAError) null else MR.strings.private_routing_error to message() else -> null } } @@ -760,7 +760,7 @@ object ChatController { is ProxyError.BROKER -> when (e.proxyErr.protocolErr.proxyErr.brokerErr) { is BrokerErrorType.TIMEOUT -> MR.strings.private_routing_timeout to message() - is BrokerErrorType.NETWORK -> MR.strings.private_routing_error to message() + is BrokerErrorType.NETWORK -> if (e.proxyErr.protocolErr.proxyErr.brokerErr.networkError is NetworkError.UnknownCAError) null else MR.strings.private_routing_error to message() else -> null } is ProxyError.NO_SESSION -> MR.strings.private_routing_no_session to message() @@ -2385,7 +2385,10 @@ object ChatController { && e.agentError.brokerErr is BrokerErrorType.NETWORK -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.connection_error), - String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.agentError.brokerAddress)) + String.format( + generalGetString(if (e.agentError.brokerErr.networkError is NetworkError.UnknownCAError) MR.strings.network_error_unknown_ca else MR.strings.network_error_desc), + serverHostname(e.agentError.brokerAddress) + ) ) true } @@ -2439,7 +2442,10 @@ object ChatController { && pe.brokerErr is BrokerErrorType.NETWORK -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.private_routing_error), - String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(srvAddr)) + String.format( + generalGetString(if (pe.brokerErr.networkError is NetworkError.UnknownCAError) MR.strings.smp_proxy_error_unknown_ca else MR.strings.smp_proxy_error_connecting), + serverHostname(srvAddr) + ) ) true } @@ -2478,7 +2484,8 @@ object ChatController { && pe.brokerErr is BrokerErrorType.NETWORK -> { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.private_routing_error), - String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + if (pe.brokerErr.networkError is NetworkError.UnknownCAError) String.format(generalGetString(MR.strings.proxy_destination_error_unknown_ca), serverHostname(relayServer)) + else String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) ) true } @@ -4601,9 +4608,10 @@ data class ProtocolTestFailure( err + " " + generalGetString(MR.strings.error_smp_test_server_auth) testError is AgentErrorType.XFTP && testError.xftpErr is XFTPErrorType.AUTH -> err + " " + generalGetString(MR.strings.error_xftp_test_server_auth) - testError is AgentErrorType.BROKER && testError.brokerErr is BrokerErrorType.NETWORK -> + testError is AgentErrorType.BROKER && testError.brokerErr is BrokerErrorType.NETWORK && testError.brokerErr.networkError is NetworkError.UnknownCAError -> err + " " + generalGetString(MR.strings.error_smp_test_certificate) - else -> err + else -> + err + " " + String.format(generalGetString(MR.strings.error_with_info), testError.toString()) } } } @@ -7408,7 +7416,7 @@ sealed class BrokerErrorType { } @Serializable @SerialName("RESPONSE") class RESPONSE(val smpErr: String): BrokerErrorType() @Serializable @SerialName("UNEXPECTED") object UNEXPECTED: BrokerErrorType() - @Serializable @SerialName("NETWORK") object NETWORK: BrokerErrorType() + @Serializable @SerialName("NETWORK") class NETWORK(val networkError: NetworkError): BrokerErrorType() @Serializable @SerialName("HOST") object HOST: BrokerErrorType() @Serializable @SerialName("TRANSPORT") class TRANSPORT(val transportErr: SMPTransportError): BrokerErrorType() @Serializable @SerialName("TIMEOUT") object TIMEOUT: BrokerErrorType() @@ -7495,6 +7503,16 @@ sealed class ProtocolCommandError { @Serializable @SerialName("NO_QUEUE") object NO_QUEUE: ProtocolCommandError() } +@Serializable +sealed class NetworkError { + @Serializable @SerialName("connectError") class ConnectError(val connectError: String): NetworkError() + @Serializable @SerialName("tLSError") class TLSError(val tlsError: String): NetworkError() + @Serializable @SerialName("unknownCAError") object UnknownCAError: NetworkError() + @Serializable @SerialName("failedError") object FailedError: NetworkError() + @Serializable @SerialName("timeoutError") object TimeoutError: NetworkError() + @Serializable @SerialName("subscribeError") class SubscribeError(val subscribeError: String): NetworkError() +} + @Serializable sealed class SMPTransportError { val string: String get() = when (this) { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 401f69f9c0..3c34d744f4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -146,15 +146,18 @@ Connection timeout Connection error + Fingerprint in server address does not match certificate: %1$s. Please check your network connection with %1$s and try again. Server address is incompatible with network settings: %1$s. Server version is incompatible with your app: %1$s. Private routing timeout Private routing error No private routing session + Fingerprint in forwarding server address does not match certificate: %1$s. Error connecting to forwarding server %1$s. Please try later. Forwarding server address is incompatible with network settings: %1$s. Forwarding server version is incompatible with network settings: %1$s. + Fingerprint in destination server address does not match certificate: %1$s. Forwarding server %1$s failed to connect to destination server %2$s. Please try later. Destination server address of %1$s is incompatible with forwarding server %2$s settings. Destination server version of %1$s is incompatible with forwarding server %2$s. @@ -201,9 +204,9 @@ Error aborting address change Error synchronizing connection Test failed at step %s. - Server requires authorization to create queues, check password - Server requires authorization to upload, check password - Possibly, certificate fingerprint in server address is incorrect + Server requires authorization to create queues, check password. + Server requires authorization to upload, check password. + Fingerprint in server address does not match certificate. Error setting address Error Connect From b3bf9842e0b452bf462cd25f5e0be0ea0e66a7b4 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 8 Sep 2025 11:07:13 +0100 Subject: [PATCH 030/137] android, desktop: show alert when server test fails (#6251) --- .../views/usersettings/networkAndServers/NewServerView.kt | 7 +++++++ .../usersettings/networkAndServers/ProtocolServerView.kt | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt index 1ec2534ab1..6a999aa89d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt @@ -43,6 +43,13 @@ fun ModalData.NewServerView( if (isActive) { newServer.value = res.first testing.value = false + val failure = res.second + if (failure != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_test_failed), + text = failure.localizedDescription + ) + } } } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt index 8626fd3143..ccad962313 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt @@ -93,6 +93,13 @@ fun ProtocolServerView( if (isActive) { draftServer.value = res.first testing.value = false + val failure = res.second + if (failure != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.smp_servers_test_failed), + text = failure.localizedDescription + ) + } } } }, From 14733a195bd98c2027263be745a78e9b8c40e33b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 8 Sep 2025 11:28:09 +0100 Subject: [PATCH 031/137] ui: shorter alert titles (#6252) --- apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift | 2 +- .../bg.xcloc/Localized Contents/bg.xliff | 4 ++-- .../cs.xcloc/Localized Contents/cs.xliff | 4 ++-- .../de.xcloc/Localized Contents/de.xliff | 4 ++-- .../en.xcloc/Localized Contents/en.xliff | 6 +++--- .../es.xcloc/Localized Contents/es.xliff | 4 ++-- .../fi.xcloc/Localized Contents/fi.xliff | 4 ++-- .../fr.xcloc/Localized Contents/fr.xliff | 4 ++-- .../hu.xcloc/Localized Contents/hu.xliff | 4 ++-- .../it.xcloc/Localized Contents/it.xliff | 4 ++-- .../ja.xcloc/Localized Contents/ja.xliff | 4 ++-- .../nl.xcloc/Localized Contents/nl.xliff | 4 ++-- .../pl.xcloc/Localized Contents/pl.xliff | 4 ++-- .../ru.xcloc/Localized Contents/ru.xliff | 4 ++-- .../th.xcloc/Localized Contents/th.xliff | 4 ++-- .../tr.xcloc/Localized Contents/tr.xliff | 4 ++-- .../uk.xcloc/Localized Contents/uk.xliff | 4 ++-- .../zh-Hans.xcloc/Localized Contents/zh-Hans.xliff | 4 ++-- apps/ios/de.lproj/Localizable.strings | 2 +- apps/ios/es.lproj/Localizable.strings | 2 +- apps/ios/hu.lproj/Localizable.strings | 2 +- apps/ios/it.lproj/Localizable.strings | 2 +- apps/ios/nl.lproj/Localizable.strings | 2 +- apps/ios/ru.lproj/Localizable.strings | 2 +- apps/ios/tr.lproj/Localizable.strings | 2 +- apps/ios/uk.lproj/Localizable.strings | 2 +- .../common/src/commonMain/resources/MR/base/strings.xml | 4 ++-- 27 files changed, 46 insertions(+), 46 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift index 2101202ed8..75a6840c4e 100644 --- a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift @@ -281,7 +281,7 @@ func deleteMemberSupportChat(_ groupInfo: GroupInfo, _ member: GroupMember) { logger.error("apiDeleteMemberSupportChat error: \(responseError(error))") await MainActor.run { showAlert( - NSLocalizedString("Error deleting chat with member", comment: "alert title"), + NSLocalizedString("Error deleting chat", comment: "alert title"), message: responseError(error) ) } diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index cbcec3fbb3..c428486009 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -3200,8 +3200,8 @@ chat item action Грешка при изтриване на базата данни alert title - - Error deleting chat with member + + Error deleting chat alert title diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 77b15adbbf..a3053d882a 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -3071,8 +3071,8 @@ chat item action Chyba při mazání databáze chatu alert title - - Error deleting chat with member + + Error deleting chat alert title diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 2eeb067648..34e70512c1 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -3360,8 +3360,8 @@ chat item action Fehler beim Löschen der Chat-Datenbank alert title - - Error deleting chat with member + + Error deleting chat Fehler beim Löschen des Chats mit dem Mitglied alert title diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index eebc5ef100..aab989ddc0 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -3360,9 +3360,9 @@ chat item action Error deleting chat database alert title - - Error deleting chat with member - Error deleting chat with member + + Error deleting chat + Error deleting chat alert title diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index c505c38c81..11731c37ea 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -3360,8 +3360,8 @@ chat item action Error al eliminar base de datos alert title - - Error deleting chat with member + + Error deleting chat Error al eliminar el chat con el miembro alert title diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 8c2afed258..af6b3c39b5 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -3040,8 +3040,8 @@ chat item action Virhe keskustelujen tietokannan poistamisessa alert title - - Error deleting chat with member + + Error deleting chat alert title diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 406bd0bf75..c9d21e51ec 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -3338,8 +3338,8 @@ chat item action Erreur lors de la suppression de la base de données du chat alert title - - Error deleting chat with member + + Error deleting chat alert title diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 91161f6c6c..3ffd898bbc 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -3360,8 +3360,8 @@ chat item action Hiba történt a csevegési adatbázis törlésekor alert title - - Error deleting chat with member + + Error deleting chat Hiba a taggal való csevegés törlésekor alert title diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index a46fab948d..7a838f38ee 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -3360,8 +3360,8 @@ chat item action Errore nell'eliminazione del database della chat alert title - - Error deleting chat with member + + Error deleting chat Errore di eliminazione della chat con il membro alert title diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index bcb302c233..1d1ef8301d 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -3123,8 +3123,8 @@ chat item action チャットデータベース削除にエラー発生 alert title - - Error deleting chat with member + + Error deleting chat alert title diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 81928bf80d..163e1d4621 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -3340,8 +3340,8 @@ chat item action Fout bij het verwijderen van de chat database alert title - - Error deleting chat with member + + Error deleting chat Fout bij het verwijderen van chat met lid alert title diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index e68db5988d..abf0eb33a5 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -3285,8 +3285,8 @@ chat item action Błąd usuwania bazy danych czatu alert title - - Error deleting chat with member + + Error deleting chat alert title diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 71c574d0cc..588bc62451 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -3360,8 +3360,8 @@ chat item action Ошибка при удалении данных чата alert title - - Error deleting chat with member + + Error deleting chat Ошибка при удалении чата с членом группы alert title diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 44eb673c6c..c2971593d5 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -3025,8 +3025,8 @@ chat item action เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท alert title - - Error deleting chat with member + + Error deleting chat alert title diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 2842245a7f..c9f2e0ea6e 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -3355,8 +3355,8 @@ chat item action Sohbet veritabanı silinirken sorun oluştu alert title - - Error deleting chat with member + + Error deleting chat Üye ile sohbet silme hatası alert title diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 7e8c4cf8df..7abe965329 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -3354,8 +3354,8 @@ chat item action Помилка видалення бази даних чату alert title - - Error deleting chat with member + + Error deleting chat Помилка при видаленні чату з учасником alert title diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 4b506a0b08..0b188d7c1a 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -3330,8 +3330,8 @@ chat item action 删除聊天数据库错误 alert title - - Error deleting chat with member + + Error deleting chat alert title diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index d6193a324f..1d034f0eb1 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -2269,7 +2269,7 @@ chat item action */ "Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank"; /* alert title */ -"Error deleting chat with member" = "Fehler beim Löschen des Chats mit dem Mitglied"; +"Error deleting chat" = "Fehler beim Löschen des Chats mit dem Mitglied"; /* alert title */ "Error deleting chat!" = "Fehler beim Löschen des Chats!"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 4293429d64..ecd69f675a 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -2269,7 +2269,7 @@ chat item action */ "Error deleting chat database" = "Error al eliminar base de datos"; /* alert title */ -"Error deleting chat with member" = "Error al eliminar el chat con el miembro"; +"Error deleting chat" = "Error al eliminar el chat con el miembro"; /* alert title */ "Error deleting chat!" = "¡Error al eliminar chat!"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index f46353e1ad..2e2ce8bb68 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -2269,7 +2269,7 @@ chat item action */ "Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor"; /* alert title */ -"Error deleting chat with member" = "Hiba a taggal való csevegés törlésekor"; +"Error deleting chat" = "Hiba a taggal való csevegés törlésekor"; /* alert title */ "Error deleting chat!" = "Hiba történt a csevegés törlésekor!"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 57954be773..bb19130ae4 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -2269,7 +2269,7 @@ chat item action */ "Error deleting chat database" = "Errore nell'eliminazione del database della chat"; /* alert title */ -"Error deleting chat with member" = "Errore di eliminazione della chat con il membro"; +"Error deleting chat" = "Errore di eliminazione della chat con il membro"; /* alert title */ "Error deleting chat!" = "Errore nell'eliminazione della chat!"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 3bc8ec7493..457e9200b4 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -2206,7 +2206,7 @@ chat item action */ "Error deleting chat database" = "Fout bij het verwijderen van de chat database"; /* alert title */ -"Error deleting chat with member" = "Fout bij het verwijderen van chat met lid"; +"Error deleting chat" = "Fout bij het verwijderen van chat met lid"; /* alert title */ "Error deleting chat!" = "Fout bij verwijderen gesprek!"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 225b85e467..fcec9cf789 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -2269,7 +2269,7 @@ chat item action */ "Error deleting chat database" = "Ошибка при удалении данных чата"; /* alert title */ -"Error deleting chat with member" = "Ошибка при удалении чата с членом группы"; +"Error deleting chat" = "Ошибка при удалении чата с членом группы"; /* alert title */ "Error deleting chat!" = "Ошибка при удалении чата!"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 39a3d0e0d3..554324b623 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -2254,7 +2254,7 @@ chat item action */ "Error deleting chat database" = "Sohbet veritabanı silinirken sorun oluştu"; /* alert title */ -"Error deleting chat with member" = "Üye ile sohbet silme hatası"; +"Error deleting chat" = "Üye ile sohbet silme hatası"; /* alert title */ "Error deleting chat!" = "Sohbet silinirken hata oluştu!"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 7fb9f932a0..532d2ab744 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -2251,7 +2251,7 @@ chat item action */ "Error deleting chat database" = "Помилка видалення бази даних чату"; /* alert title */ -"Error deleting chat with member" = "Помилка при видаленні чату з учасником"; +"Error deleting chat" = "Помилка при видаленні чату з учасником"; /* alert title */ "Error deleting chat!" = "Помилка видалення чату!"; diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 3c34d744f4..b13e3f4046 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -170,8 +170,8 @@ Error adding member(s) Error joining group Error accepting member - Error marking chat with member as read - Error deleting chat with member + Error marking read + Error deleting chat Cannot receive file Sender cancelled file transfer. Unknown servers! From 535ab41c3b6b75b0925ec0320541fc55459b8a63 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 8 Sep 2025 11:43:41 +0100 Subject: [PATCH 032/137] ios: export localizations --- .../bg.xcloc/Localized Contents/bg.xliff | 34 +++++++++++----- .../cs.xcloc/Localized Contents/cs.xliff | 34 +++++++++++----- .../de.xcloc/Localized Contents/de.xliff | 36 ++++++++++++----- .../en.xcloc/Localized Contents/en.xliff | 40 ++++++++++++++----- .../es.xcloc/Localized Contents/es.xliff | 36 ++++++++++++----- .../fi.xcloc/Localized Contents/fi.xliff | 34 +++++++++++----- .../fr.xcloc/Localized Contents/fr.xliff | 34 +++++++++++----- .../hu.xcloc/Localized Contents/hu.xliff | 36 ++++++++++++----- .../it.xcloc/Localized Contents/it.xliff | 36 ++++++++++++----- .../ja.xcloc/Localized Contents/ja.xliff | 34 +++++++++++----- .../nl.xcloc/Localized Contents/nl.xliff | 36 ++++++++++++----- .../pl.xcloc/Localized Contents/pl.xliff | 34 +++++++++++----- .../ru.xcloc/Localized Contents/ru.xliff | 36 ++++++++++++----- .../th.xcloc/Localized Contents/th.xliff | 34 +++++++++++----- .../tr.xcloc/Localized Contents/tr.xliff | 36 ++++++++++++----- .../uk.xcloc/Localized Contents/uk.xliff | 36 ++++++++++++----- .../Localized Contents/zh-Hans.xliff | 34 +++++++++++----- 17 files changed, 438 insertions(+), 162 deletions(-) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index c428486009..04229ce385 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -3195,15 +3195,15 @@ chat item action Грешка при декриптирането на файла No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Грешка при изтриване на базата данни alert title - - Error deleting chat - alert title - Error deleting chat! Грешка при изтриването на чата! @@ -3465,6 +3465,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Грешка: URL адресът е невалиден @@ -3671,6 +3675,23 @@ snd error text Намирайте чатове по-бързо No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Поправи @@ -5766,11 +5787,6 @@ Error: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен - server test error - Preserve the last message draft, with attachments. Запазете последната чернова на съобщението с прикачени файлове. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index a3053d882a..42d58e0223 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -3066,15 +3066,15 @@ chat item action Chyba dešifrování souboru No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Chyba při mazání databáze chatu alert title - - Error deleting chat - alert title - Error deleting chat! Chyba při mazání chatu! @@ -3330,6 +3330,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Chyba: Adresa URL je neplatná @@ -3530,6 +3534,23 @@ snd error text Najděte chaty rychleji No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Je možné, že otisk certifikátu v adrese serveru je nesprávný + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Opravit @@ -5558,11 +5579,6 @@ Error: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Je možné, že otisk certifikátu v adrese serveru je nesprávný - server test error - Preserve the last message draft, with attachments. Zachování posledního návrhu zprávy s přílohami. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 34e70512c1..306e725388 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -3355,16 +3355,16 @@ chat item action Fehler beim Entschlüsseln der Datei No comment provided by engineer. - - Error deleting chat database - Fehler beim Löschen der Chat-Datenbank - alert title - Error deleting chat Fehler beim Löschen des Chats mit dem Mitglied alert title + + Error deleting chat database + Fehler beim Löschen der Chat-Datenbank + alert title + Error deleting chat! Fehler beim Löschen des Chats! @@ -3642,6 +3642,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Fehler: URL ist ungültig @@ -3866,6 +3870,23 @@ snd error text Chats schneller finden No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Reparieren @@ -6112,11 +6133,6 @@ Fehler: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig - server test error - Preserve the last message draft, with attachments. Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index aab989ddc0..6761ff6fce 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -3355,16 +3355,16 @@ chat item action Error decrypting file No comment provided by engineer. - - Error deleting chat database - Error deleting chat database - alert title - Error deleting chat Error deleting chat alert title + + Error deleting chat database + Error deleting chat database + alert title + Error deleting chat! Error deleting chat! @@ -3642,6 +3642,11 @@ chat item action file error text snd error text + + Error: %@. + Error: %@. + server test error + Error: URL is invalid Error: URL is invalid @@ -3866,6 +3871,26 @@ snd error text Find chats faster No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Fingerprint in server address does not match certificate. + server test error + + + Fingerprint in server address does not match certificate: %@. + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Fix @@ -6113,11 +6138,6 @@ Error: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Fingerprint in server address does not match certificate. - server test error - Preserve the last message draft, with attachments. Preserve the last message draft, with attachments. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 11731c37ea..469aced2f1 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -3355,16 +3355,16 @@ chat item action Error al descifrar el archivo No comment provided by engineer. - - Error deleting chat database - Error al eliminar base de datos - alert title - Error deleting chat Error al eliminar el chat con el miembro alert title + + Error deleting chat database + Error al eliminar base de datos + alert title + Error deleting chat! ¡Error al eliminar chat! @@ -3642,6 +3642,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Error: la URL no es válida @@ -3866,6 +3870,23 @@ snd error text Encuentra chats mas rápido No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Posiblemente la huella del certificado en la dirección del servidor es incorrecta + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Reparar @@ -6112,11 +6133,6 @@ Error: %@ Puerto No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Posiblemente la huella del certificado en la dirección del servidor es incorrecta - server test error - Preserve the last message draft, with attachments. Conserva el último borrador del mensaje con los datos adjuntos. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index af6b3c39b5..6e400a6078 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -3035,15 +3035,15 @@ chat item action Virhe tiedoston salauksen purussa No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Virhe keskustelujen tietokannan poistamisessa alert title - - Error deleting chat - alert title - Error deleting chat! Virhe keskutelun poistamisessa! @@ -3298,6 +3298,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Virhe: URL on virheellinen @@ -3498,6 +3502,23 @@ snd error text Löydä keskustelut nopeammin No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Korjaa @@ -5524,11 +5545,6 @@ Error: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen - server test error - Preserve the last message draft, with attachments. Säilytä viimeinen viestiluonnos liitteineen. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index c9d21e51ec..d008696a14 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -3333,15 +3333,15 @@ chat item action Erreur lors du déchiffrement du fichier No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Erreur lors de la suppression de la base de données du chat alert title - - Error deleting chat - alert title - Error deleting chat! Erreur lors de la suppression du chat ! @@ -3616,6 +3616,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Erreur : URL invalide @@ -3839,6 +3843,23 @@ snd error text Recherche de message plus rapide No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Réparer @@ -6022,11 +6043,6 @@ Erreur : %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte - server test error - Preserve the last message draft, with attachments. Conserver le brouillon du dernier message, avec les pièces jointes. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 3ffd898bbc..ec5ed40121 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -3355,16 +3355,16 @@ chat item action Hiba történt a fájl visszafejtésekor No comment provided by engineer. - - Error deleting chat database - Hiba történt a csevegési adatbázis törlésekor - alert title - Error deleting chat Hiba a taggal való csevegés törlésekor alert title + + Error deleting chat database + Hiba történt a csevegési adatbázis törlésekor + alert title + Error deleting chat! Hiba történt a csevegés törlésekor! @@ -3642,6 +3642,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Hiba: a webcím érvénytelen @@ -3866,6 +3870,23 @@ snd error text Csevegési üzenetek gyorsabb megtalálása No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Javítás @@ -6112,11 +6133,6 @@ Hiba: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen - server test error - Preserve the last message draft, with attachments. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 7a838f38ee..821ae03f8f 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -3355,16 +3355,16 @@ chat item action Errore decifrando il file No comment provided by engineer. - - Error deleting chat database - Errore nell'eliminazione del database della chat - alert title - Error deleting chat Errore di eliminazione della chat con il membro alert title + + Error deleting chat database + Errore nell'eliminazione del database della chat + alert title + Error deleting chat! Errore nell'eliminazione della chat! @@ -3642,6 +3642,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Errore: l'URL non è valido @@ -3866,6 +3870,23 @@ snd error text Trova le chat più velocemente No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Correggi @@ -6112,11 +6133,6 @@ Errore: %@ Porta No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata - server test error - Preserve the last message draft, with attachments. Conserva la bozza dell'ultimo messaggio, con gli allegati. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 1d1ef8301d..0ca54bb3d9 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -3118,15 +3118,15 @@ chat item action ファイルの復号エラー No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database チャットデータベース削除にエラー発生 alert title - - Error deleting chat - alert title - Error deleting chat! チャット削除にエラー発生! @@ -3380,6 +3380,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid エラー: 無効なURL @@ -3580,6 +3584,23 @@ snd error text チャットを素早く検索 No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + サーバアドレスの証明証IDが正しくないかもしれません + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix 修正 @@ -5610,11 +5631,6 @@ Error: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - サーバアドレスの証明証IDが正しくないかもしれません - server test error - Preserve the last message draft, with attachments. 添付を含めて、下書きを保存する。 diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 163e1d4621..a13e4cd80d 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -3335,16 +3335,16 @@ chat item action Fout bij het ontsleutelen van bestand No comment provided by engineer. - - Error deleting chat database - Fout bij het verwijderen van de chat database - alert title - Error deleting chat Fout bij het verwijderen van chat met lid alert title + + Error deleting chat database + Fout bij het verwijderen van de chat database + alert title + Error deleting chat! Fout bij verwijderen gesprek! @@ -3619,6 +3619,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Fout: URL is ongeldig @@ -3842,6 +3846,23 @@ snd error text Vind chats sneller No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Mogelijk is de certificaat vingerafdruk in het server adres onjuist + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Herstel @@ -6069,11 +6090,6 @@ Fout: %@ Poort No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Mogelijk is de certificaat vingerafdruk in het server adres onjuist - server test error - Preserve the last message draft, with attachments. Bewaar het laatste berichtconcept, met bijlagen. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index abf0eb33a5..d08a7d86e5 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -3280,15 +3280,15 @@ chat item action Błąd odszyfrowania pliku No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database Błąd usuwania bazy danych czatu alert title - - Error deleting chat - alert title - Error deleting chat! Błąd usuwania czatu! @@ -3556,6 +3556,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Błąd: URL jest nieprawidłowy @@ -3772,6 +3776,23 @@ snd error text Szybciej znajduj czaty No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Napraw @@ -5922,11 +5943,6 @@ Błąd: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy - server test error - Preserve the last message draft, with attachments. Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 588bc62451..9f5ab3a18c 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -3355,16 +3355,16 @@ chat item action Ошибка расшифровки файла No comment provided by engineer. - - Error deleting chat database - Ошибка при удалении данных чата - alert title - Error deleting chat Ошибка при удалении чата с членом группы alert title + + Error deleting chat database + Ошибка при удалении данных чата + alert title + Error deleting chat! Ошибка при удалении чата! @@ -3642,6 +3642,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Ошибка: неверная ссылка @@ -3866,6 +3870,23 @@ snd error text Быстро найти чаты No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Возможно, хэш сертификата в адресе сервера неверный + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Починить @@ -6111,11 +6132,6 @@ Error: %@ Порт No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Возможно, хэш сертификата в адресе сервера неверный - server test error - Preserve the last message draft, with attachments. Сохранить последний черновик, вместе с вложениями. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index c2971593d5..a0ab509388 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -3020,15 +3020,15 @@ chat item action Error decrypting file No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท alert title - - Error deleting chat - alert title - Error deleting chat! เกิดข้อผิดพลาดในการลบแชท! @@ -3283,6 +3283,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid เกิดข้อผิดพลาด: URL ไม่ถูกต้อง @@ -3483,6 +3487,23 @@ snd error text ค้นหาแชทได้เร็วขึ้น No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix แก้ไข @@ -5503,11 +5524,6 @@ Error: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง - server test error - Preserve the last message draft, with attachments. เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index c9f2e0ea6e..cfd9fbc22a 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -3350,16 +3350,16 @@ chat item action Dosya şifresi çözülürken hata oluştu No comment provided by engineer. - - Error deleting chat database - Sohbet veritabanı silinirken sorun oluştu - alert title - Error deleting chat Üye ile sohbet silme hatası alert title + + Error deleting chat database + Sohbet veritabanı silinirken sorun oluştu + alert title + Error deleting chat! Sohbet silinirken hata oluştu! @@ -3636,6 +3636,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Hata: URL geçersiz @@ -3859,6 +3863,23 @@ snd error text Sohbetleri daha hızlı bul No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Düzelt @@ -6098,11 +6119,6 @@ Hata: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil - server test error - Preserve the last message draft, with attachments. Son mesaj taslağını ekleriyle birlikte koru. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 7abe965329..bfb565fd65 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -3349,16 +3349,16 @@ chat item action Помилка розшифрування файлу No comment provided by engineer. - - Error deleting chat database - Помилка видалення бази даних чату - alert title - Error deleting chat Помилка при видаленні чату з учасником alert title + + Error deleting chat database + Помилка видалення бази даних чату + alert title + Error deleting chat! Помилка видалення чату! @@ -3635,6 +3635,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid Помилка: URL-адреса невірна @@ -3858,6 +3862,23 @@ snd error text Швидше знаходьте чати No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + Можливо, в адресі сервера неправильно вказано відбиток сертифіката + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix Виправити @@ -6098,11 +6119,6 @@ Error: %@ Порт No comment provided by engineer. - - Fingerprint in server address does not match certificate. - Можливо, в адресі сервера неправильно вказано відбиток сертифіката - server test error - Preserve the last message draft, with attachments. Зберегти чернетку останнього повідомлення з вкладеннями. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 0b188d7c1a..0a2a252826 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -3325,15 +3325,15 @@ chat item action 解密文件时出错 No comment provided by engineer. + + Error deleting chat + alert title + Error deleting chat database 删除聊天数据库错误 alert title - - Error deleting chat - alert title - Error deleting chat! 删除聊天错误! @@ -3608,6 +3608,10 @@ chat item action file error text snd error text + + Error: %@. + server test error + Error: URL is invalid 错误:URL 无效 @@ -3831,6 +3835,23 @@ snd error text 更快地查找聊天记录 No comment provided by engineer. + + Fingerprint in destination server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in forwarding server address does not match certificate: %@. + No comment provided by engineer. + + + Fingerprint in server address does not match certificate. + 服务器地址中的证书指纹可能不正确 + server test error + + + Fingerprint in server address does not match certificate: %@. + No comment provided by engineer. + Fix 修复 @@ -6046,11 +6067,6 @@ Error: %@ Port No comment provided by engineer. - - Fingerprint in server address does not match certificate. - 服务器地址中的证书指纹可能不正确 - server test error - Preserve the last message draft, with attachments. 保留最后的消息草稿及其附件。 From 03b5599db736a3326892ca8f55b02ea0a946731e Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 8 Sep 2025 12:35:24 +0100 Subject: [PATCH 033/137] website: translations (#6254) * Added translation using Weblate (Romanian) * Added translation using Weblate (Romanian) --- website/langs/ro.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 website/langs/ro.json diff --git a/website/langs/ro.json b/website/langs/ro.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/website/langs/ro.json @@ -0,0 +1 @@ +{} From d8a2f5b1dbc5c70a7108faf4255bd27b8f3c42d7 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 8 Sep 2025 13:37:39 +0100 Subject: [PATCH 034/137] ui: translations (#6253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (German) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/ * Translated using Weblate (Italian) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Turkish) Currently translated at 98.8% (2140 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 98.4% (2462 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Czech) Currently translated at 97.4% (2436 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Turkish) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2499 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Czech) Currently translated at 97.6% (2440 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Czech) Currently translated at 57.6% (1248 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2498 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2498 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Catalan) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ca/ * Translated using Weblate (German) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Italian) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Bulgarian) Currently translated at 72.5% (1570 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/bg/ * Translated using Weblate (Bulgarian) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/bg/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2500 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2500 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Romanian) Currently translated at 99.7% (2497 of 2504 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (German) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/ * Translated using Weblate (Italian) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/it/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Turkish) Currently translated at 98.8% (2140 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Turkish) Currently translated at 98.4% (2462 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/ * Translated using Weblate (Indonesian) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/id/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Czech) Currently translated at 97.4% (2436 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Turkish) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2499 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Czech) Currently translated at 97.6% (2440 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/cs/ * Translated using Weblate (Czech) Currently translated at 57.6% (1248 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/cs/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2498 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2498 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Catalan) Currently translated at 100.0% (2500 of 2500 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ca/ * Translated using Weblate (German) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/ * Translated using Weblate (Italian) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/it/ * Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/ * Translated using Weblate (Arabic) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ar/ * Translated using Weblate (Bulgarian) Currently translated at 72.5% (1570 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/bg/ * Translated using Weblate (Bulgarian) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/bg/ * Translated using Weblate (Hungarian) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2500 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2501 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/es/ * Translated using Weblate (Spanish) Currently translated at 100.0% (2164 of 2164 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/es/ * Translated using Weblate (Romanian) Currently translated at 99.9% (2500 of 2501 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Romanian) Currently translated at 99.7% (2497 of 2504 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Romanian) Currently translated at 99.7% (2497 of 2504 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Romanian) Currently translated at 99.6% (2495 of 2504 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Russian) Currently translated at 100.0% (2504 of 2504 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ru/ * Translated using Weblate (Romanian) Currently translated at 99.6% (2495 of 2504 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * Translated using Weblate (Russian) Currently translated at 100.0% (2168 of 2168 strings) Translation: SimpleX Chat/SimpleX Chat iOS Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/ru/ * Translated using Weblate (Hungarian) Currently translated at 99.8% (2500 of 2504 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/ * Translated using Weblate (Romanian) Currently translated at 99.6% (2495 of 2504 strings) Translation: SimpleX Chat/SimpleX Chat Android Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/ * process --------- Co-authored-by: mlanp Co-authored-by: Random Co-authored-by: No name Co-authored-by: Egehan KAHRAMAN Co-authored-by: summoner001 Co-authored-by: Rafi Co-authored-by: Carlos Perez Co-authored-by: zenobit Co-authored-by: Nicolae Fericitu Co-authored-by: fran secs Co-authored-by: 大王叫我来巡山 Co-authored-by: jonnysemon Co-authored-by: elgratea --- .../bg.xcloc/Localized Contents/bg.xliff | 87 ++++ .../cs.xcloc/Localized Contents/cs.xliff | 81 ++++ .../de.xcloc/Localized Contents/de.xliff | 1 + .../es.xcloc/Localized Contents/es.xliff | 7 +- .../hu.xcloc/Localized Contents/hu.xliff | 9 +- .../it.xcloc/Localized Contents/it.xliff | 1 + .../ru.xcloc/Localized Contents/ru.xliff | 6 + .../tr.xcloc/Localized Contents/tr.xliff | 38 ++ apps/ios/bg.lproj/Localizable.strings | 259 ++++++++++- apps/ios/cs.lproj/Localizable.strings | 241 ++++++++++- apps/ios/de.lproj/Localizable.strings | 14 +- apps/ios/es.lproj/Localizable.strings | 20 +- apps/ios/fi.lproj/Localizable.strings | 7 +- apps/ios/fr.lproj/Localizable.strings | 7 +- apps/ios/hu.lproj/Localizable.strings | 22 +- apps/ios/it.lproj/Localizable.strings | 14 +- apps/ios/ja.lproj/Localizable.strings | 7 +- apps/ios/nl.lproj/Localizable.strings | 11 +- apps/ios/pl.lproj/Localizable.strings | 7 +- apps/ios/ru.lproj/Localizable.strings | 29 +- apps/ios/th.lproj/Localizable.strings | 7 +- apps/ios/tr.lproj/Localizable.strings | 125 +++++- apps/ios/uk.lproj/Localizable.strings | 11 +- apps/ios/zh-Hans.lproj/Localizable.strings | 7 +- .../commonMain/resources/MR/ar/strings.xml | 1 + .../commonMain/resources/MR/bg/strings.xml | 25 +- .../commonMain/resources/MR/ca/strings.xml | 3 +- .../commonMain/resources/MR/cs/strings.xml | 19 +- .../commonMain/resources/MR/de/strings.xml | 1 + .../commonMain/resources/MR/es/strings.xml | 11 +- .../commonMain/resources/MR/hu/strings.xml | 15 +- .../commonMain/resources/MR/in/strings.xml | 29 +- .../commonMain/resources/MR/it/strings.xml | 1 + .../commonMain/resources/MR/ro/strings.xml | 409 +++++++++--------- .../commonMain/resources/MR/ru/strings.xml | 11 +- .../commonMain/resources/MR/tr/strings.xml | 6 + .../resources/MR/zh-rCN/strings.xml | 1 + 37 files changed, 1240 insertions(+), 310 deletions(-) diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 04229ce385..e3ca0e6a43 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -362,6 +362,7 @@ **Scan / Paste link**: to connect via a link you received. + **Сканирай / Постави линк**: за свързване чрез получения линк. No comment provided by engineer. @@ -475,6 +476,7 @@ time interval 1-time link can be used *with one contact only* - share in person or via any messenger. + Еднократният линк може да се използва само веднъж *с един контакт* - споделете го лично или чрез някой месинджър. No comment provided by engineer. @@ -602,6 +604,7 @@ swipe action Accept member + Приеми член alert title @@ -621,6 +624,7 @@ swipe action Active + Активен token status text @@ -640,10 +644,12 @@ swipe action Add list + Добави списък No comment provided by engineer. Add message + Добави съобщение placeholder for sending contact request @@ -673,6 +679,7 @@ swipe action Add to list + Добави към списъка No comment provided by engineer. @@ -752,6 +759,7 @@ swipe action All + Всички No comment provided by engineer. @@ -766,6 +774,7 @@ swipe action All chats will be removed from the list %@, and the list deleted. + Всички чатове ще бъдат премахнати от списъка %@, а списъкът ще бъде изтрит. alert message @@ -785,6 +794,7 @@ swipe action All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + Всички съобщения и файлове се изпращат с **криптиране от край до край**, с постквантова сигурност в директните съобщения. No comment provided by engineer. @@ -809,10 +819,12 @@ swipe action All reports will be archived for you. + Всички доклади за нарушения ще бъдат архивирани за вас. No comment provided by engineer. All servers + Всички сървъри No comment provided by engineer. @@ -857,6 +869,7 @@ swipe action Allow files and media only if your contact allows them. + Разреши файлове и медия само ако вашият контакт ги разрешава. No comment provided by engineer. @@ -896,6 +909,7 @@ swipe action Allow to report messsages to moderators. + Позволи докладването на съобщения на модераторите. No comment provided by engineer. @@ -945,6 +959,7 @@ swipe action Allow your contacts to send files and media. + Позволи на вашите контактите да изпращат файлове и медия. No comment provided by engineer. @@ -984,6 +999,7 @@ swipe action Another reason + Друга причина report reason @@ -1013,6 +1029,7 @@ swipe action App group: + Група приложения: No comment provided by engineer. @@ -1062,14 +1079,17 @@ swipe action Archive + Архивирай No comment provided by engineer. Archive %lld reports? + Архивирай %lld доклад(а)? No comment provided by engineer. Archive all reports? + Архивиране на всички доклади за нарушения? No comment provided by engineer. @@ -1084,14 +1104,17 @@ swipe action Archive report + Архивирай доклад за нарушения No comment provided by engineer. Archive report? + Архивирай доклад за нарушения? No comment provided by engineer. Archive reports + Архивирай докладите за нарушения swipe action @@ -1201,6 +1224,7 @@ swipe action Better groups performance + По-добра производителност на групите No comment provided by engineer. @@ -1225,6 +1249,7 @@ swipe action Better privacy and security + По-добра поверителност и сигурност No comment provided by engineer. @@ -1239,10 +1264,12 @@ swipe action Bio + Био No comment provided by engineer. Bio too large + Биографията е твърде дълга alert title @@ -1297,6 +1324,7 @@ swipe action Bot + Бот No comment provided by engineer. @@ -1321,6 +1349,7 @@ swipe action Both you and your contact can send files and media. + И вие, и вашият контакт можете да изпращате файлове и медия. No comment provided by engineer. @@ -1345,10 +1374,12 @@ swipe action Business connection + Бизнес връзка No comment provided by engineer. Businesses + Бизнеси No comment provided by engineer. @@ -1360,6 +1391,9 @@ swipe action By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam. + С използването на SimpleX Chat вие се съгласявате със: +- изпращане само на легално съдържание в публични групи. +- уважение към другите потребители – без спам. No comment provided by engineer. @@ -1394,6 +1428,7 @@ swipe action Can't change profile + Промяната на профила е невъзможна alert title @@ -1408,6 +1443,7 @@ swipe action Can't message member + Изпращането на съобщения на груповия член не е налично No comment provided by engineer. @@ -1429,6 +1465,7 @@ new chat action Cannot forward message + Съобщение не може да бъде препратено No comment provided by engineer. @@ -1453,6 +1490,7 @@ new chat action Change automatic message deletion? + Промяна на автоматичното изтриване на съобщения? alert title @@ -1508,18 +1546,22 @@ set passcode view Chat + Чат No comment provided by engineer. Chat already exists + Чатът вече съществува No comment provided by engineer. Chat already exists! + Чатът вече съществува! new chat sheet title Chat colors + Цветове на чата No comment provided by engineer. @@ -1539,6 +1581,7 @@ set passcode view Chat database exported + Базата данни е експортирана No comment provided by engineer. @@ -1563,6 +1606,7 @@ set passcode view Chat list + Списък с чатове No comment provided by engineer. @@ -1577,6 +1621,7 @@ set passcode view Chat preferences were changed. + Настройките на чата бяха променени. alert message @@ -1586,26 +1631,32 @@ set passcode view Chat theme + Тема на чата No comment provided by engineer. Chat will be deleted for all members - this cannot be undone! + Чатът ще бъде изтрит за всички членове - това не може да бъде отменено! No comment provided by engineer. Chat will be deleted for you - this cannot be undone! + Чатът ще бъде изтрит за вас - това не може да бъде отменено! No comment provided by engineer. Chat with admins + Чат с администраторите chat toolbar Chat with member + Чат с член No comment provided by engineer. Chat with members before they join. + Разговаряйте с членовете, преди да се присъединят. No comment provided by engineer. @@ -1615,14 +1666,17 @@ set passcode view Chats with members + Чатове с членовете No comment provided by engineer. Check messages every 20 min. + Проверявай за съобщенията на всеки 20 минути. No comment provided by engineer. Check messages when allowed. + Проверявай за съобщенията, когато е разрешено. No comment provided by engineer. @@ -1652,14 +1706,17 @@ set passcode view Chunks deleted + Изтрити парчета No comment provided by engineer. Chunks downloaded + Изтеглени парчета No comment provided by engineer. Chunks uploaded + Качени парчета No comment provided by engineer. @@ -1679,10 +1736,12 @@ set passcode view Clear group? + Изчисти група? No comment provided by engineer. Clear or delete group? + Изчисти или изтрий група? No comment provided by engineer. @@ -1697,14 +1756,17 @@ set passcode view Color chats with the new themes. + Цветни чатове с нови теми. No comment provided by engineer. Color mode + Цветен режим No comment provided by engineer. Community guidelines violation + Нарушение на правилата на общността report reason @@ -1719,34 +1781,42 @@ set passcode view Completed + Завършен No comment provided by engineer. Conditions accepted on: %@. + Условия, приети на: %@. No comment provided by engineer. Conditions are accepted for the operator(s): **%@**. + Условията са приети за оператора(ите): **%@**. No comment provided by engineer. Conditions are already accepted for these operator(s): **%@**. + Условията вече са приети за тези оператори: **%@**. No comment provided by engineer. Conditions of use + Условия за ползване No comment provided by engineer. Conditions will be accepted for the operator(s): **%@**. + Условията ще бъдат приети за операторите: **%@**. No comment provided by engineer. Conditions will be accepted on: %@. + Условията ще бъдат приети на: %@. No comment provided by engineer. Conditions will be automatically accepted for enabled operators on: %@. + Условията ще бъдат автоматично приети за активираните оператори на: %@. No comment provided by engineer. @@ -1756,6 +1826,7 @@ set passcode view Configure server operators + Конфигуриране на сървърни оператори No comment provided by engineer. @@ -1770,6 +1841,7 @@ set passcode view Confirm contact deletion? + Потвърди изтриването на контакта? No comment provided by engineer. @@ -1779,6 +1851,7 @@ set passcode view Confirm files from unknown servers. + Потвърди файлове от неизвестни сървъри. No comment provided by engineer. @@ -1808,6 +1881,7 @@ set passcode view Confirmed + Потвърдено token status text @@ -1822,6 +1896,7 @@ set passcode view Connect faster! 🚀 + Свържете се по-бързо! 🚀 No comment provided by engineer. @@ -1831,6 +1906,7 @@ set passcode view Connect to your friends faster. + Свържете се с приятелите си по-бързо. No comment provided by engineer. @@ -1869,6 +1945,7 @@ This is your own one-time link! Connected + Свързан No comment provided by engineer. @@ -1878,6 +1955,7 @@ This is your own one-time link! Connected servers + Свързани сървъри No comment provided by engineer. @@ -1887,6 +1965,7 @@ This is your own one-time link! Connecting + Свързване No comment provided by engineer. @@ -1901,6 +1980,7 @@ This is your own one-time link! Connecting to contact, please wait or check later! + Тече свързване с контакт, моля изчакайте или проверете по-късно! No comment provided by engineer. @@ -1915,10 +1995,12 @@ This is your own one-time link! Connection and servers status. + Състояние на връзката и сървърите. No comment provided by engineer. Connection blocked + Връзката е блокирана No comment provided by engineer. @@ -1934,14 +2016,18 @@ This is your own one-time link! Connection is blocked by server operator: %@ + Връзката е блокирана от оператора на сървъра: +%@ No comment provided by engineer. Connection not ready. + Връзката не е готова. No comment provided by engineer. Connection notifications + Известия за връзката No comment provided by engineer. @@ -1951,6 +2037,7 @@ This is your own one-time link! Connection requires encryption renegotiation. + Връзката изисква предоговаряне на криптирането. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 42d58e0223..244cbdf946 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -410,6 +410,9 @@ - optionally notify deleted contacts. - profile names with spaces. - and more! + - volitelně informuje smazané kontakty. +- profilová jména s mezerami. +- a více! No comment provided by engineer. @@ -423,6 +426,7 @@ 0 sec + 0 sek time to disappear @@ -460,6 +464,7 @@ time interval 1 year + 1 rok delete after time @@ -468,6 +473,7 @@ time interval 1-time link can be used *with one contact only* - share in person or via any messenger. + Jednorázový odkaz lze použít *pouze s jedním kontaktem* - sdílejte osobně nebo prostřednictvím libovolného komunikátoru. No comment provided by engineer. @@ -541,6 +547,7 @@ time interval About operators + O operátorech No comment provided by engineer. @@ -557,14 +564,17 @@ swipe action Accept as member + Přijmout za člena alert action Accept as observer + Přijmout jako pozorovatele alert action Accept conditions + Přijmout podmínky No comment provided by engineer. @@ -574,6 +584,7 @@ swipe action Accept contact request + Přijmout žádost o kontakt alert title @@ -589,10 +600,12 @@ swipe action Accept member + Přijmout člena alert title Accepted conditions + Přijaté podmínky No comment provided by engineer. @@ -605,10 +618,12 @@ swipe action Active + Aktivní token status text Active connections + Aktivní spojení No comment provided by engineer. @@ -618,6 +633,7 @@ swipe action Add friends + Přidat přátele No comment provided by engineer. @@ -645,6 +661,7 @@ swipe action Add team members + Přidat členy týmu No comment provided by engineer. @@ -654,6 +671,7 @@ swipe action Add to list + Přidat do seznamu No comment provided by engineer. @@ -663,6 +681,7 @@ swipe action Add your team members to the conversations. + Přidat členy týmu do konverzace. No comment provided by engineer. @@ -671,14 +690,17 @@ swipe action Added message servers + Přidané servery zpráv No comment provided by engineer. Additional accent + Další zbarvení No comment provided by engineer. Additional accent 2 + Další zbarvení 2 No comment provided by engineer. @@ -697,14 +719,17 @@ swipe action Address or 1-time link? + Adresa nebo jednorázový odkaz? No comment provided by engineer. Address settings + Nastavení adresy No comment provided by engineer. Admins can block a member for all. + Správci mohou blokovat člena pro všechny. No comment provided by engineer. @@ -719,10 +744,12 @@ swipe action Advanced settings + Pokročilá nastavení No comment provided by engineer. All + Vše No comment provided by engineer. @@ -737,6 +764,7 @@ swipe action All chats will be removed from the list %@, and the list deleted. + Všechny chaty budou odstraněny ze seznamu %@ a seznam bude odstraněn. alert message @@ -746,6 +774,7 @@ swipe action All data is kept private on your device. + Všechna data jsou uchována ve vašem zařízení. No comment provided by engineer. @@ -768,10 +797,12 @@ swipe action All new messages from %@ will be hidden! + Všechny nové zprávy od %@ budou skryté! No comment provided by engineer. All profiles + Všechny profily profile dropdown @@ -780,6 +811,7 @@ swipe action All servers + Všechny servery No comment provided by engineer. @@ -808,6 +840,7 @@ swipe action Allow calls? + Povolit volání? No comment provided by engineer. @@ -850,6 +883,7 @@ swipe action Allow sharing + Povolit sdílení No comment provided by engineer. @@ -859,10 +893,12 @@ swipe action Allow to report messsages to moderators. + Povolit nahlášení zpráv moderátorům. No comment provided by engineer. Allow to send SimpleX links. + Povolit odesílání SimpleX odkazů. No comment provided by engineer. @@ -907,6 +943,7 @@ swipe action Allow your contacts to send files and media. + Povolit vašim kontaktům odesílání souborů a médii. No comment provided by engineer. @@ -921,14 +958,17 @@ swipe action Already connecting! + Již připojováno! new chat sheet title Already joining the group! + Již se ke skupině připojujete! new chat sheet title Always use private routing. + Vždy používat soukromé směrování. No comment provided by engineer. @@ -943,6 +983,7 @@ swipe action Another reason + Jiný důvod report reason @@ -962,6 +1003,7 @@ swipe action App data migration + Přenos dat aplikace No comment provided by engineer. @@ -1009,6 +1051,7 @@ swipe action Apply + Použít No comment provided by engineer. @@ -1017,6 +1060,7 @@ swipe action Archive + Archiv No comment provided by engineer. @@ -1025,10 +1069,12 @@ swipe action Archive all reports? + Archivovat všechny hlášení? No comment provided by engineer. Archive and upload + Archivovat a nahrát No comment provided by engineer. @@ -1049,6 +1095,7 @@ swipe action Archived contacts + Archivované kontakty No comment provided by engineer. @@ -1122,6 +1169,7 @@ swipe action Background + Pozadí No comment provided by engineer. @@ -1140,18 +1188,22 @@ swipe action Better calls + Lepší volání No comment provided by engineer. Better groups + Lepší skupiny No comment provided by engineer. Better groups performance + Lepší výkon skupin No comment provided by engineer. Better message dates. + Lepší datumy zpráv. No comment provided by engineer. @@ -1161,18 +1213,22 @@ swipe action Better networking + Lepší sítě No comment provided by engineer. Better notifications + Lepší upozornění No comment provided by engineer. Better privacy and security + Lepší soukromí a zabezpečení No comment provided by engineer. Better security ✅ + Lepší zabezpečení ✅ No comment provided by engineer. @@ -1205,6 +1261,7 @@ swipe action Block member + Blokovat člena No comment provided by engineer. @@ -1213,10 +1270,12 @@ swipe action Block member? + Blokovat člena? No comment provided by engineer. Blocked by admin + Blokován správcem No comment provided by engineer. @@ -1225,6 +1284,7 @@ swipe action Blur media + Rozmazat média No comment provided by engineer. @@ -1253,6 +1313,7 @@ swipe action Both you and your contact can send files and media. + Vy i vaše kontakty můžete posílat soubory a média. No comment provided by engineer. @@ -1267,6 +1328,7 @@ swipe action Business address + Obchodní adresa No comment provided by engineer. @@ -1304,22 +1366,27 @@ swipe action Calls prohibited! + Volání zakázáno! No comment provided by engineer. Camera not available + Kamera není k dispozici No comment provided by engineer. Can't call contact + Kontaktu nelze volat No comment provided by engineer. Can't call member + Členu nelze volat No comment provided by engineer. Can't change profile + Nelze změnit profil alert title @@ -1334,6 +1401,7 @@ swipe action Can't message member + Členu nelze poslat zprávu No comment provided by engineer. @@ -1345,6 +1413,7 @@ new chat action Cancel migration + Zrušit přesun No comment provided by engineer. @@ -1354,6 +1423,7 @@ new chat action Cannot forward message + Zprávu nelze přeposlat No comment provided by engineer. @@ -1363,10 +1433,12 @@ new chat action Capacity exceeded - recipient did not receive previously sent messages. + Kapacita překročena - příjemce neobdrží dříve poslané zprávy. snd error text Cellular + Mobilní No comment provided by engineer. @@ -1376,10 +1448,12 @@ new chat action Change automatic message deletion? + Změnit automatické mazání zpráv? alert title Change chat profiles + Změnit chat profily authentication reason @@ -1442,6 +1516,7 @@ set passcode view Chat colors + Barvy chatu No comment provided by engineer. @@ -1461,6 +1536,7 @@ set passcode view Chat database exported + Chat databáze exportována No comment provided by engineer. @@ -1488,6 +1564,7 @@ set passcode view Chat migrated! + Chat přesunut! No comment provided by engineer. @@ -1719,6 +1796,7 @@ set passcode view Confirm upload + Potvrdit nahrání No comment provided by engineer. @@ -1732,6 +1810,7 @@ set passcode view Connect automatically + Připojit automaticky No comment provided by engineer. @@ -1754,6 +1833,8 @@ This is your own SimpleX address! Connect to yourself? This is your own one-time link! + Připojit se k sobě? +Toto je váš vlastní jednorázový odkaz! new chat sheet title diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 306e725388..e6131264af 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -4915,6 +4915,7 @@ Das ist Ihr Link für die Gruppe %@! Member %@ + Mitglied %@ past/unknown group member diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 469aced2f1..f11ad704ac 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2486,7 +2486,7 @@ swipe action Delete chat with member? - ¿Eliminar chat con el miembro? + ¿Eliminar chat con miembro? alert title @@ -3062,7 +3062,7 @@ chat item action Enable instant notifications? - ¿Activar notificación instantánea? + ¿Activar notificaciones instantáneas? No comment provided by engineer. @@ -4915,6 +4915,7 @@ This is your link for group %@! Member %@ + Miembro %@ past/unknown group member @@ -7984,7 +7985,7 @@ report reason Take picture - Tomar foto + Hacer foto No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index ec5ed40121..b542c6eabf 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -1014,7 +1014,7 @@ swipe action App build: %@ - Az alkalmazás összeállítási száma: %@ + Alkalmazás összeállítási száma: %@ No comment provided by engineer. @@ -1054,12 +1054,12 @@ swipe action App version - Az alkalmazás verziója + Alkalmazás verziója No comment provided by engineer. App version: v%@ - Az alkalmazás verziója: v%@ + Alkalmazás verziója: v%@ No comment provided by engineer. @@ -4915,6 +4915,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Member %@ + %@ ismeretlen vagy már nem tag past/unknown group member @@ -7611,7 +7612,7 @@ chat item action Show preview - Értesítések előnézetének megjelenítése + Értesítésekben megjelenő információk No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 821ae03f8f..6ddcf7d6d1 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -4915,6 +4915,7 @@ Questo è il tuo link per il gruppo %@! Member %@ + Membro %@ past/unknown group member diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 9f5ab3a18c..568ca53946 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -3644,6 +3644,7 @@ snd error text Error: %@. + Ошибка: %@. server test error @@ -3872,10 +3873,12 @@ snd error text Fingerprint in destination server address does not match certificate: %@. + Хэш в адресе сервера назначения не соответствует сертификату: %@. No comment provided by engineer. Fingerprint in forwarding server address does not match certificate: %@. + Хэш в адресе пересылающего сервера не соответствует сертификату: %@. No comment provided by engineer. @@ -3885,6 +3888,7 @@ snd error text Fingerprint in server address does not match certificate: %@. + Хэш в адресе сервера не соответствует сертификату: %@. No comment provided by engineer. @@ -4914,6 +4918,7 @@ This is your link for group %@! Member %@ + Член группы %@ past/unknown group member @@ -7730,6 +7735,7 @@ chat item action SimpleX relay link + Ссылка SimpleX relay simplex link type diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index cfd9fbc22a..8fb8e4ac51 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -869,6 +869,7 @@ swipe action Allow files and media only if your contact allows them. + Dosyalara ve medyaya yalnızca iletişiminiz izin verdiğinde izin verin. No comment provided by engineer. @@ -958,6 +959,7 @@ swipe action Allow your contacts to send files and media. + Kişilerinizin dosya ve medya göndermesine izin verin. No comment provided by engineer. @@ -1322,6 +1324,7 @@ swipe action Bot + Bot No comment provided by engineer. @@ -1346,6 +1349,7 @@ swipe action Both you and your contact can send files and media. + Sen de kişilerin de medya ve dosya gönderebilir. No comment provided by engineer. @@ -2682,6 +2686,7 @@ swipe action Deprecated options + Kullanımdan kaldırılan seçenekler No comment provided by engineer. @@ -3552,6 +3557,7 @@ chat item action Error setting auto-accept + Otomatik kabul ayarında hata No comment provided by engineer. @@ -3821,6 +3827,7 @@ snd error text Files and media are prohibited in this chat. + Bu sohbette dosyalar ve medya yasaktır. No comment provided by engineer. @@ -4708,6 +4715,7 @@ Bu senin grup için bağlantın %@! Keep your chats clean + Sohbetlerinizi temiz tutun No comment provided by engineer. @@ -4907,6 +4915,7 @@ Bu senin grup için bağlantın %@! Member %@ + Üye%@ past/unknown group member @@ -4921,6 +4930,7 @@ Bu senin grup için bağlantın %@! Member is deleted - can't accept request + Üye silinmiş - istek kabul edilemez No comment provided by engineer. @@ -5735,6 +5745,7 @@ VPN'nin etkinleştirilmesi gerekir. Only you can send files and media. + Sadece sen dosya ve medya gönderebilirsin. No comment provided by engineer. @@ -5764,6 +5775,7 @@ VPN'nin etkinleştirilmesi gerekir. Only your contact can send files and media. + Sadece kişilerin dosya ve medya gönderebilir. No comment provided by engineer. @@ -5798,6 +5810,7 @@ VPN'nin etkinleştirilmesi gerekir. Open clean link + Temiz linki aç alert action @@ -5807,6 +5820,7 @@ VPN'nin etkinleştirilmesi gerekir. Open full link + Tam linki aç alert action @@ -5851,6 +5865,7 @@ VPN'nin etkinleştirilmesi gerekir. Open to use bot + Botu kullanmak için aç No comment provided by engineer. @@ -6590,6 +6605,7 @@ swipe action Remove link tracking + Bağlantı izlemeyi kaldır No comment provided by engineer. @@ -7440,6 +7456,7 @@ chat item action Set profile bio and welcome message. + Profil biyografisi ve hoşgeldiniz mesajı düzenle. No comment provided by engineer. @@ -7515,10 +7532,12 @@ chat item action Share old address + Eski adresi paylaş alert button Share old link + Eski bağlantıyı paylaş alert button @@ -7543,10 +7562,12 @@ chat item action Share your address + Adresini paylaş No comment provided by engineer. Short SimpleX address + Kısa SimpleX adresi No comment provided by engineer. @@ -7711,6 +7732,7 @@ chat item action SimpleX relay link + SimpleX aktarıcı bağlantısı simplex link type @@ -7978,6 +8000,7 @@ report reason Tap Connect to use bot + Botu kullanmak için Bağlan tuşuna bas No comment provided by engineer. @@ -8079,6 +8102,7 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. The address will be short, and your profile will be shared via the address. + Adres kısa olacak ve profiliniz bu adres üzerinden paylaşılacaktır. alert message @@ -8143,6 +8167,7 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. The link will be short, and group profile will be shared via the link. + Bağlantı kısa olacak ve grup profili bağlantı üzerinden paylaşılacaktır. alert message @@ -8302,10 +8327,12 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. This setting is for your current profile **%@**. + Bu ayar, mevcut profiliniz içindir. No comment provided by engineer. Time to disappear is set only for new contacts. + Kaybolma süresi yalnızca yeni kişiler için ayarlanır. No comment provided by engineer. @@ -8392,6 +8419,7 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec To send commands you must be connected. + Komut göndermek için bağlı olmanız gerekir. alert message @@ -8643,14 +8671,17 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Upgrade + Yükseltme alert button Upgrade address + Adres güncelleme No comment provided by engineer. Upgrade address? + Adres güncellensin mi? alert message @@ -8660,14 +8691,17 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Upgrade group link? + Grub linki güncellensin mi? alert message Upgrade link + Linki güncelle No comment provided by engineer. Upgrade your address + Adresini yükselt No comment provided by engineer. @@ -8767,6 +8801,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Use incognito profile + Gizli profil kullan No comment provided by engineer. @@ -9006,6 +9041,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Welcome your contacts 👋 + Kişilerine hoş geldin👋 No comment provided by engineer. @@ -10374,10 +10410,12 @@ time to disappear requested connection + istenilen bağlantı rcv group event chat item requested connection from group %@ + %@ grubundan bağlantı isteği rcv direct event chat item diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 0c89d57dc4..1ce7b53767 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -61,6 +61,9 @@ /* No comment provided by engineer. */ "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." = "**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е."; +/* No comment provided by engineer. */ +"**Scan / Paste link**: to connect via a link you received." = "**Сканирай / Постави линк**: за свързване чрез получения линк."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: Незабавните push известия изискват парола, запазена в Keychain."; @@ -289,6 +292,9 @@ time interval */ /* No comment provided by engineer. */ "1-time link" = "Еднократен линк"; +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Еднократният линк може да се използва само веднъж *с един контакт* - споделете го лично или чрез някой месинджър."; + /* No comment provided by engineer. */ "5 minutes" = "5 минути"; @@ -362,6 +368,9 @@ swipe action */ swipe action */ "Accept incognito" = "Приеми инкогнито"; +/* alert title */ +"Accept member" = "Приеми член"; + /* call status */ "accepted call" = "обаждането прието"; @@ -374,6 +383,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledgement errors" = "Грешки при потвърждението"; +/* token status text */ +"Active" = "Активен"; + /* No comment provided by engineer. */ "Active connections" = "Активни връзки"; @@ -383,6 +395,12 @@ swipe action */ /* No comment provided by engineer. */ "Add friends" = "Добави приятели"; +/* No comment provided by engineer. */ +"Add list" = "Добави списък"; + +/* placeholder for sending contact request */ +"Add message" = "Добави съобщение"; + /* No comment provided by engineer. */ "Add profile" = "Добави профил"; @@ -398,6 +416,9 @@ swipe action */ /* No comment provided by engineer. */ "Add to another device" = "Добави към друго устройство"; +/* No comment provided by engineer. */ +"Add to list" = "Добави към списъка"; + /* No comment provided by engineer. */ "Add welcome message" = "Добави съобщение при посрещане"; @@ -455,12 +476,18 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "съгласуване на криптиране…"; +/* No comment provided by engineer. */ +"All" = "Всички"; + /* No comment provided by engineer. */ "All app data is deleted." = "Всички данни от приложението бяха изтрити."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Всички чатове и съобщения ще бъдат изтрити - това не може да бъде отменено!"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Всички чатове ще бъдат премахнати от списъка %@, а списъкът ще бъде изтрит."; + /* No comment provided by engineer. */ "All data is erased when it is entered." = "Всички данни се изтриват при въвеждане."; @@ -473,6 +500,9 @@ swipe action */ /* feature role */ "all members" = "всички членове"; +/* No comment provided by engineer. */ +"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Всички съобщения и файлове се изпращат с **криптиране от край до край**, с постквантова сигурност в директните съобщения."; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Всички съобщения ще бъдат изтрити - това не може да бъде отменено!"; @@ -485,6 +515,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Всички профили"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Всички доклади за нарушения ще бъдат архивирани за вас."; + +/* No comment provided by engineer. */ +"All servers" = "Всички сървъри"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Всички ваши контакти ще останат свързани."; @@ -509,6 +545,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow downgrade" = "Позволи понижаване"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Разреши файлове и медия само ако вашият контакт ги разрешава."; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа)"; @@ -530,6 +569,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Позволи необратимо изтриване на изпратените съобщения. (24 часа)"; +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Позволи докладването на съобщения на модераторите."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Позволи изпращане на файлове и медия."; @@ -557,6 +599,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Позволи на вашите контакти да изпращат изчезващи съобщения."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Позволи на вашите контактите да изпращат файлове и медия."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Позволи на вашите контакти да изпращат гласови съобщения."; @@ -584,6 +629,9 @@ swipe action */ /* No comment provided by engineer. */ "and %lld other events" = "и %lld други събития"; +/* report reason */ +"Another reason" = "Друга причина"; + /* No comment provided by engineer. */ "Answer call" = "Отговор на повикване"; @@ -599,6 +647,9 @@ swipe action */ /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Приложението криптира нови локални файлове (с изключение на видеоклипове)."; +/* No comment provided by engineer. */ +"App group:" = "Група приложения:"; + /* No comment provided by engineer. */ "App icon" = "Икона на приложението"; @@ -626,12 +677,30 @@ swipe action */ /* No comment provided by engineer. */ "Apply to" = "Приложи към"; +/* No comment provided by engineer. */ +"Archive" = "Архивирай"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Архивирай %lld доклад(а)?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Архивиране на всички доклади за нарушения?"; + /* No comment provided by engineer. */ "Archive and upload" = "Архивиране и качване"; /* No comment provided by engineer. */ "Archive contacts to chat later." = "Архивирайте контактите, за да разговаряте по-късно."; +/* No comment provided by engineer. */ +"Archive report" = "Архивирай доклад за нарушения"; + +/* No comment provided by engineer. */ +"Archive report?" = "Архивирай доклад за нарушения?"; + +/* swipe action */ +"Archive reports" = "Архивирай докладите за нарушения"; + /* No comment provided by engineer. */ "Archived contacts" = "Архивирани контакти"; @@ -707,6 +776,9 @@ swipe action */ /* No comment provided by engineer. */ "Better groups" = "По-добри групи"; +/* No comment provided by engineer. */ +"Better groups performance" = "По-добра производителност на групите"; + /* No comment provided by engineer. */ "Better message dates." = "По-добри дати на съобщението."; @@ -719,12 +791,21 @@ swipe action */ /* No comment provided by engineer. */ "Better notifications" = "Подобрени известия"; +/* No comment provided by engineer. */ +"Better privacy and security" = "По-добра поверителност и сигурност"; + /* No comment provided by engineer. */ "Better security ✅" = "По-добра сигурност ✅"; /* No comment provided by engineer. */ "Better user experience" = "Подобрен интерфейс"; +/* No comment provided by engineer. */ +"Bio" = "Био"; + +/* alert title */ +"Bio too large" = "Биографията е твърде дълга"; + /* No comment provided by engineer. */ "Black" = "Черна"; @@ -768,6 +849,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "bold" = "удебелен"; +/* No comment provided by engineer. */ +"Bot" = "Бот"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "И вие, и вашият контакт можете да добавяте реакции към съобщението."; @@ -780,6 +864,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "И вие, и вашият контакт можете да изпращате изчезващи съобщения."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "И вие, и вашият контакт можете да изпращате файлове и медия."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "И вие, и вашият контакт можете да изпращате гласови съобщения."; @@ -792,9 +879,18 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Business chats" = "Бизнес чатове"; +/* No comment provided by engineer. */ +"Business connection" = "Бизнес връзка"; + +/* No comment provided by engineer. */ +"Businesses" = "Бизнеси"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА)."; +/* No comment provided by engineer. */ +"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "С използването на SimpleX Chat вие се съгласявате със:\n- изпращане само на легално съдържание в публични групи.\n- уважение към другите потребители – без спам."; + /* No comment provided by engineer. */ "Call already ended!" = "Разговорът вече приключи!"; @@ -822,12 +918,18 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't call member" = "Обаждането на члена не е позволено"; +/* alert title */ +"Can't change profile" = "Промяната на профила е невъзможна"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Не може да покани контакта!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Не може да поканят контактите!"; +/* No comment provided by engineer. */ +"Can't message member" = "Изпращането на съобщения на груповия член не е налично"; + /* alert action alert button new chat action */ @@ -842,6 +944,9 @@ new chat action */ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Няма достъп до Keychain за запазване на паролата за базата данни"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Съобщение не може да бъде препратено"; + /* alert title */ "Cannot receive file" = "Файлът не може да бъде получен"; @@ -854,6 +959,9 @@ new chat action */ /* No comment provided by engineer. */ "Change" = "Промени"; +/* alert title */ +"Change automatic message deletion?" = "Промяна на автоматичното изтриване на съобщения?"; + /* authentication reason */ "Change chat profiles" = "Промени чат профилите"; @@ -900,6 +1008,18 @@ set passcode view */ /* chat item text */ "changing address…" = "промяна на адреса…"; +/* No comment provided by engineer. */ +"Chat" = "Чат"; + +/* No comment provided by engineer. */ +"Chat already exists" = "Чатът вече съществува"; + +/* new chat sheet title */ +"Chat already exists!" = "Чатът вече съществува!"; + +/* No comment provided by engineer. */ +"Chat colors" = "Цветове на чата"; + /* No comment provided by engineer. */ "Chat console" = "Конзола"; @@ -909,6 +1029,9 @@ set passcode view */ /* No comment provided by engineer. */ "Chat database deleted" = "Базата данни на чата е изтрита"; +/* No comment provided by engineer. */ +"Chat database exported" = "Базата данни е експортирана"; + /* No comment provided by engineer. */ "Chat database imported" = "Базата данни на е импортирана"; @@ -921,18 +1044,51 @@ set passcode view */ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново."; +/* No comment provided by engineer. */ +"Chat list" = "Списък с чатове"; + /* No comment provided by engineer. */ "Chat migrated!" = "Чатът е мигриран!"; /* No comment provided by engineer. */ "Chat preferences" = "Чат настройки"; +/* alert message */ +"Chat preferences were changed." = "Настройките на чата бяха променени."; + /* No comment provided by engineer. */ "Chat profile" = "Потребителски профил"; +/* No comment provided by engineer. */ +"Chat theme" = "Тема на чата"; + +/* No comment provided by engineer. */ +"Chat will be deleted for all members - this cannot be undone!" = "Чатът ще бъде изтрит за всички членове - това не може да бъде отменено!"; + +/* No comment provided by engineer. */ +"Chat will be deleted for you - this cannot be undone!" = "Чатът ще бъде изтрит за вас - това не може да бъде отменено!"; + +/* chat toolbar */ +"Chat with admins" = "Чат с администраторите"; + +/* No comment provided by engineer. */ +"Chat with member" = "Чат с член"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Разговаряйте с членовете, преди да се присъединят."; + /* No comment provided by engineer. */ "Chats" = "Чатове"; +/* No comment provided by engineer. */ +"Chats with members" = "Чатове с членовете"; + +/* No comment provided by engineer. */ +"Check messages every 20 min." = "Проверявай за съобщенията на всеки 20 минути."; + +/* No comment provided by engineer. */ +"Check messages when allowed." = "Проверявай за съобщенията, когато е разрешено."; + /* alert title */ "Check server address and try again." = "Проверете адреса на сървъра и опитайте отново."; @@ -948,6 +1104,15 @@ set passcode view */ /* No comment provided by engineer. */ "Choose from library" = "Избери от библиотеката"; +/* No comment provided by engineer. */ +"Chunks deleted" = "Изтрити парчета"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Изтеглени парчета"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Качени парчета"; + /* swipe action */ "Clear" = "Изчисти"; @@ -957,15 +1122,30 @@ set passcode view */ /* No comment provided by engineer. */ "Clear conversation?" = "Изчисти разговора?"; +/* No comment provided by engineer. */ +"Clear group?" = "Изчисти група?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Изчисти или изтрий група?"; + /* No comment provided by engineer. */ "Clear private notes?" = "Изчистване на лични бележки?"; /* No comment provided by engineer. */ "Clear verification" = "Изчисти проверката"; +/* No comment provided by engineer. */ +"Color chats with the new themes." = "Цветни чатове с нови теми."; + +/* No comment provided by engineer. */ +"Color mode" = "Цветен режим"; + /* No comment provided by engineer. */ "colored" = "цветен"; +/* report reason */ +"Community guidelines violation" = "Нарушение на правилата на общността"; + /* server test step */ "Compare file" = "Сравни файл"; @@ -975,15 +1155,48 @@ set passcode view */ /* No comment provided by engineer. */ "complete" = "завършен"; +/* No comment provided by engineer. */ +"Completed" = "Завършен"; + +/* No comment provided by engineer. */ +"Conditions accepted on: %@." = "Условия, приети на: %@."; + +/* No comment provided by engineer. */ +"Conditions are accepted for the operator(s): **%@**." = "Условията са приети за оператора(ите): **%@**."; + +/* No comment provided by engineer. */ +"Conditions are already accepted for these operator(s): **%@**." = "Условията вече са приети за тези оператори: **%@**."; + +/* No comment provided by engineer. */ +"Conditions of use" = "Условия за ползване"; + +/* No comment provided by engineer. */ +"Conditions will be accepted for the operator(s): **%@**." = "Условията ще бъдат приети за операторите: **%@**."; + +/* No comment provided by engineer. */ +"Conditions will be accepted on: %@." = "Условията ще бъдат приети на: %@."; + +/* No comment provided by engineer. */ +"Conditions will be automatically accepted for enabled operators on: %@." = "Условията ще бъдат автоматично приети за активираните оператори на: %@."; + /* No comment provided by engineer. */ "Configure ICE servers" = "Конфигурирай ICE сървъри"; +/* No comment provided by engineer. */ +"Configure server operators" = "Конфигуриране на сървърни оператори"; + /* No comment provided by engineer. */ "Confirm" = "Потвърди"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Потвърди изтриването на контакта?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Потвърди актуализаациите на базата данни"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Потвърди файлове от неизвестни сървъри."; + /* No comment provided by engineer. */ "Confirm network settings" = "Потвърди мрежовите настройки"; @@ -1002,18 +1215,27 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm upload" = "Потвърди качването"; +/* token status text */ +"Confirmed" = "Потвърдено"; + /* server test step */ "Connect" = "Свързване"; /* No comment provided by engineer. */ "Connect automatically" = "Автоматично свъзрване"; +/* No comment provided by engineer. */ +"Connect faster! 🚀" = "Свържете се по-бързо! 🚀"; + /* No comment provided by engineer. */ "Connect to desktop" = "Свързване с настолно устройство"; /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "свържете се с разработчиците на SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Свържете се с приятелите си по-бързо."; + /* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Свърване със себе си?\nТова е вашят еднократен линк за връзка!"; @@ -1035,15 +1257,24 @@ set passcode view */ /* No comment provided by engineer. */ "connected" = "свързан"; +/* No comment provided by engineer. */ +"Connected" = "Свързан"; + /* No comment provided by engineer. */ "Connected desktop" = "Свързано настолно устройство"; +/* No comment provided by engineer. */ +"Connected servers" = "Свързани сървъри"; + /* No comment provided by engineer. */ "Connected to desktop" = "Свързан с настолно устройство"; /* No comment provided by engineer. */ "connecting" = "свързване"; +/* No comment provided by engineer. */ +"Connecting" = "Свързване"; + /* No comment provided by engineer. */ "connecting (accepted)" = "свързване (прието)"; @@ -1065,6 +1296,9 @@ set passcode view */ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Свързване със сървър…(грешка: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Тече свързване с контакт, моля изчакайте или проверете по-късно!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Свързване с настолно устройство"; @@ -1074,6 +1308,12 @@ set passcode view */ /* No comment provided by engineer. */ "Connection" = "Връзка"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Състояние на връзката и сървърите."; + +/* No comment provided by engineer. */ +"Connection blocked" = "Връзката е блокирана"; + /* alert title */ "Connection error" = "Грешка при свързване"; @@ -1083,9 +1323,21 @@ set passcode view */ /* chat list item title (it should not be shown */ "connection established" = "установена е връзка"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "Връзката е блокирана от оператора на сървъра:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "Връзката не е готова."; + +/* No comment provided by engineer. */ +"Connection notifications" = "Известия за връзката"; + /* No comment provided by engineer. */ "Connection request sent!" = "Заявката за връзка е изпратена!"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "Връзката изисква предоговаряне на криптирането."; + /* No comment provided by engineer. */ "Connection terminated" = "Връзката е прекратена"; @@ -1958,6 +2210,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Намирайте чатове по-бързо"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; + /* No comment provided by engineer. */ "Fix" = "Поправи"; @@ -2980,9 +3235,6 @@ new chat action */ /* No comment provided by engineer. */ "Polish interface" = "Полски интерфейс"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Запазете последната чернова на съобщението с прикачени файлове."; @@ -4453,3 +4705,4 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "Вашият адрес в SimpleX"; + diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index a401c4bcf9..f8597fa4a5 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- více stabilní doručování zpráv.\n- o trochu lepší skupiny.\n- a více!"; +/* No comment provided by engineer. */ +"- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- volitelně informuje smazané kontakty.\n- profilová jména s mezerami.\n- a více!"; + /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 5 minutové hlasové zprávy.\n- vlastní čas mizení.\n- historie úprav."; @@ -253,6 +256,9 @@ /* No comment provided by engineer. */ "~strike~" = "\\~stávka~"; +/* time to disappear */ +"0 sec" = "0 sek"; + /* No comment provided by engineer. */ "0s" = "0s"; @@ -274,6 +280,12 @@ time interval */ time interval */ "1 week" = "1 týden"; +/* delete after time */ +"1 year" = "1 rok"; + +/* No comment provided by engineer. */ +"1-time link can be used *with one contact only* - share in person or via any messenger." = "Jednorázový odkaz lze použít *pouze s jedním kontaktem* - sdílejte osobně nebo prostřednictvím libovolného komunikátoru."; + /* No comment provided by engineer. */ "5 minutes" = "5 minut"; @@ -307,6 +319,9 @@ time interval */ /* No comment provided by engineer. */ "Abort changing address?" = "Přerušit změnu adresy?"; +/* No comment provided by engineer. */ +"About operators" = "O operátorech"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "O SimpleX chat"; @@ -319,9 +334,21 @@ alert action swipe action */ "Accept" = "Přijmout"; +/* alert action */ +"Accept as member" = "Přijmout za člena"; + +/* alert action */ +"Accept as observer" = "Přijmout jako pozorovatele"; + +/* No comment provided by engineer. */ +"Accept conditions" = "Přijmout podmínky"; + /* No comment provided by engineer. */ "Accept connection request?" = "Přijmout kontakt?"; +/* alert title */ +"Accept contact request" = "Přijmout žádost o kontakt"; + /* notification body */ "Accept contact request from %@?" = "Přijmout žádost o kontakt od %@?"; @@ -329,12 +356,27 @@ swipe action */ swipe action */ "Accept incognito" = "Přijmout inkognito"; +/* alert title */ +"Accept member" = "Přijmout člena"; + /* call status */ "accepted call" = "přijatý hovor"; +/* No comment provided by engineer. */ +"Accepted conditions" = "Přijaté podmínky"; + +/* token status text */ +"Active" = "Aktivní"; + +/* No comment provided by engineer. */ +"Active connections" = "Aktivní spojení"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům."; +/* No comment provided by engineer. */ +"Add friends" = "Přidat přátele"; + /* No comment provided by engineer. */ "Add profile" = "Přidat profil"; @@ -344,48 +386,96 @@ swipe action */ /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Přidejte servery skenováním QR kódů."; +/* No comment provided by engineer. */ +"Add team members" = "Přidat členy týmu"; + /* No comment provided by engineer. */ "Add to another device" = "Přidat do jiného zařízení"; +/* No comment provided by engineer. */ +"Add to list" = "Přidat do seznamu"; + /* No comment provided by engineer. */ "Add welcome message" = "Přidat uvítací zprávu"; +/* No comment provided by engineer. */ +"Add your team members to the conversations." = "Přidat členy týmu do konverzace."; + +/* No comment provided by engineer. */ +"Added message servers" = "Přidané servery zpráv"; + +/* No comment provided by engineer. */ +"Additional accent" = "Další zbarvení"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Další zbarvení 2"; + /* No comment provided by engineer. */ "Address" = "Adresa"; /* No comment provided by engineer. */ "Address change will be aborted. Old receiving address will be used." = "Změna adresy bude přerušena. Budou použity staré přijímací adresy."; +/* No comment provided by engineer. */ +"Address or 1-time link?" = "Adresa nebo jednorázový odkaz?"; + +/* No comment provided by engineer. */ +"Address settings" = "Nastavení adresy"; + /* member role */ "admin" = "správce"; +/* No comment provided by engineer. */ +"Admins can block a member for all." = "Správci mohou blokovat člena pro všechny."; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "Správci mohou vytvářet odkazy pro připojení ke skupinám."; /* No comment provided by engineer. */ "Advanced network settings" = "Pokročilá nastavení sítě"; +/* No comment provided by engineer. */ +"Advanced settings" = "Pokročilá nastavení"; + /* chat item text */ "agreeing encryption for %@…" = "povoluji šifrování pro %@…"; /* chat item text */ "agreeing encryption…" = "povoluji šifrování…"; +/* No comment provided by engineer. */ +"All" = "Vše"; + /* No comment provided by engineer. */ "All app data is deleted." = "Všechna data aplikace jsou smazána."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Všechny chaty a zprávy budou smazány – tuto akci nelze vrátit zpět!"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Všechny chaty budou odstraněny ze seznamu %@ a seznam bude odstraněn."; + /* No comment provided by engineer. */ "All data is erased when it is entered." = "Všechna data se při zadání vymažou."; +/* No comment provided by engineer. */ +"All data is kept private on your device." = "Všechna data jsou uchována ve vašem zařízení."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Všichni členové skupiny zůstanou připojeni."; /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Všechny zprávy budou smazány – tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás."; +/* No comment provided by engineer. */ +"All new messages from %@ will be hidden!" = "Všechny nové zprávy od %@ budou skryté!"; + +/* profile dropdown */ +"All profiles" = "Všechny profily"; + +/* No comment provided by engineer. */ +"All servers" = "Všechny servery"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Všechny vaše kontakty zůstanou připojeny."; @@ -398,6 +488,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Povolte hovory, pouze pokud je váš kontakt povolí."; +/* No comment provided by engineer. */ +"Allow calls?" = "Povolit volání?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Povolte mizící zprávy, pouze pokud vám to váš kontakt dovolí."; @@ -416,12 +509,21 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Povolit odesílání mizících zpráv."; +/* No comment provided by engineer. */ +"Allow sharing" = "Povolit sdílení"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Povolit nevratné smazání odeslaných zpráv. (24 hodin)"; +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Povolit nahlášení zpráv moderátorům."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Povolit odesílání souborů a médii."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Povolit odesílání SimpleX odkazů."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Povolit odesílání hlasových zpráv."; @@ -443,21 +545,36 @@ swipe action */ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Umožněte svým kontaktům odesílat mizící zprávy."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Povolit vašim kontaktům odesílání souborů a médii."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Povolte svým kontaktům odesílání hlasových zpráv."; /* No comment provided by engineer. */ "Already connected?" = "Již připojeno?"; +/* new chat sheet title */ +"Already connecting!" = "Již připojováno!"; + +/* new chat sheet title */ +"Already joining the group!" = "Již se ke skupině připojujete!"; + /* pref value */ "always" = "vždy"; +/* No comment provided by engineer. */ +"Always use private routing." = "Vždy používat soukromé směrování."; + /* No comment provided by engineer. */ "Always use relay" = "Spojení přes relé"; /* No comment provided by engineer. */ "An empty chat profile with the provided name is created, and the app opens as usual." = "Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle."; +/* report reason */ +"Another reason" = "Jiný důvod"; + /* No comment provided by engineer. */ "Answer call" = "Přijmout hovor"; @@ -467,6 +584,9 @@ swipe action */ /* No comment provided by engineer. */ "App build: %@" = "Sestavení aplikace: %@"; +/* No comment provided by engineer. */ +"App data migration" = "Přenos dat aplikace"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Aplikace šifruje nové místní soubory (s výjimkou videí)."; @@ -488,6 +608,21 @@ swipe action */ /* No comment provided by engineer. */ "Appearance" = "Vzhled"; +/* No comment provided by engineer. */ +"Apply" = "Použít"; + +/* No comment provided by engineer. */ +"Archive" = "Archiv"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Archivovat všechny hlášení?"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archivovat a nahrát"; + +/* No comment provided by engineer. */ +"Archived contacts" = "Archivované kontakty"; + /* No comment provided by engineer. */ "Attach" = "Připojit"; @@ -530,6 +665,9 @@ swipe action */ /* No comment provided by engineer. */ "Back" = "Zpět"; +/* No comment provided by engineer. */ +"Background" = "Pozadí"; + /* integrity error chat item */ "bad message hash" = "špatný hash zprávy"; @@ -542,9 +680,45 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Špatné ID zprávy"; +/* No comment provided by engineer. */ +"Better calls" = "Lepší volání"; + +/* No comment provided by engineer. */ +"Better groups" = "Lepší skupiny"; + +/* No comment provided by engineer. */ +"Better groups performance" = "Lepší výkon skupin"; + +/* No comment provided by engineer. */ +"Better message dates." = "Lepší datumy zpráv."; + /* No comment provided by engineer. */ "Better messages" = "Lepší zprávy"; +/* No comment provided by engineer. */ +"Better networking" = "Lepší sítě"; + +/* No comment provided by engineer. */ +"Better notifications" = "Lepší upozornění"; + +/* No comment provided by engineer. */ +"Better privacy and security" = "Lepší soukromí a zabezpečení"; + +/* No comment provided by engineer. */ +"Better security ✅" = "Lepší zabezpečení ✅"; + +/* No comment provided by engineer. */ +"Block member" = "Blokovat člena"; + +/* No comment provided by engineer. */ +"Block member?" = "Blokovat člena?"; + +/* No comment provided by engineer. */ +"Blocked by admin" = "Blokován správcem"; + +/* No comment provided by engineer. */ +"Blur media" = "Rozmazat média"; + /* No comment provided by engineer. */ "bold" = "tučně"; @@ -560,12 +734,18 @@ swipe action */ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Vy i váš kontakt můžete posílat mizící zprávy."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Vy i vaše kontakty můžete posílat soubory a média."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Hlasové zprávy můžete posílat vy i váš kontakt."; /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulharský, finský, thajský a ukrajinský - díky uživatelům a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +/* No comment provided by engineer. */ +"Business address" = "Obchodní adresa"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -584,29 +764,65 @@ swipe action */ /* No comment provided by engineer. */ "Calls" = "Hovory"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Volání zakázáno!"; + +/* No comment provided by engineer. */ +"Camera not available" = "Kamera není k dispozici"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Kontaktu nelze volat"; + +/* No comment provided by engineer. */ +"Can't call member" = "Členu nelze volat"; + +/* alert title */ +"Can't change profile" = "Nelze změnit profil"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Nelze pozvat kontakt!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Nelze pozvat kontakty!"; +/* No comment provided by engineer. */ +"Can't message member" = "Členu nelze poslat zprávu"; + /* alert action alert button new chat action */ "Cancel" = "Zrušit"; +/* No comment provided by engineer. */ +"Cancel migration" = "Zrušit přesun"; + /* feature offered item */ "cancelled %@" = "zrušeno %@"; /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Nelze získat přístup ke klíčence pro uložení hesla databáze"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Zprávu nelze přeposlat"; + /* alert title */ "Cannot receive file" = "Nelze přijmout soubor"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Kapacita překročena - příjemce neobdrží dříve poslané zprávy."; + +/* No comment provided by engineer. */ +"Cellular" = "Mobilní"; + /* No comment provided by engineer. */ "Change" = "Změnit"; +/* alert title */ +"Change automatic message deletion?" = "Změnit automatické mazání zpráv?"; + +/* authentication reason */ +"Change chat profiles" = "Změnit chat profily"; + /* No comment provided by engineer. */ "Change database passphrase?" = "Změnit přístupovou frázi databáze?"; @@ -650,6 +866,9 @@ set passcode view */ /* chat item text */ "changing address…" = "změna adresy…"; +/* No comment provided by engineer. */ +"Chat colors" = "Barvy chatu"; + /* No comment provided by engineer. */ "Chat console" = "Konzola pro chat"; @@ -659,6 +878,9 @@ set passcode view */ /* No comment provided by engineer. */ "Chat database deleted" = "Databáze chatu odstraněna"; +/* No comment provided by engineer. */ +"Chat database exported" = "Chat databáze exportována"; + /* No comment provided by engineer. */ "Chat database imported" = "Importovaná databáze chatu"; @@ -668,6 +890,9 @@ set passcode view */ /* No comment provided by engineer. */ "Chat is stopped" = "Chat je zastaven"; +/* No comment provided by engineer. */ +"Chat migrated!" = "Chat přesunut!"; + /* No comment provided by engineer. */ "Chat preferences" = "Předvolby chatu"; @@ -731,12 +956,21 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm password" = "Potvrdit heslo"; +/* No comment provided by engineer. */ +"Confirm upload" = "Potvrdit nahrání"; + /* server test step */ "Connect" = "Připojit"; +/* No comment provided by engineer. */ +"Connect automatically" = "Připojit automaticky"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "připojit se k vývojářům SimpleX Chat."; +/* new chat sheet title */ +"Connect to yourself?\nThis is your own one-time link!" = "Připojit se k sobě?\nToto je váš vlastní jednorázový odkaz!"; + /* new chat sheet title */ "Connect via link" = "Připojte se prostřednictvím odkazu"; @@ -1518,6 +1752,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Najděte chaty rychleji"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Je možné, že otisk certifikátu v adrese serveru je nesprávný"; + /* No comment provided by engineer. */ "Fix" = "Opravit"; @@ -2336,9 +2573,6 @@ new chat action */ /* No comment provided by engineer. */ "Polish interface" = "Polské rozhraní"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Je možné, že otisk certifikátu v adrese serveru je nesprávný"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachování posledního návrhu zprávy s přílohami."; @@ -3506,3 +3740,4 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "Vaše SimpleX adresa"; + diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 1d034f0eb1..3008d29608 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -2266,10 +2266,10 @@ chat item action */ "Error decrypting file" = "Fehler beim Entschlüsseln der Datei"; /* alert title */ -"Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank"; +"Error deleting chat" = "Fehler beim Löschen des Chats mit dem Mitglied"; /* alert title */ -"Error deleting chat" = "Fehler beim Löschen des Chats mit dem Mitglied"; +"Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank"; /* alert title */ "Error deleting chat!" = "Fehler beim Löschen des Chats!"; @@ -2573,6 +2573,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Chats schneller finden"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig"; + /* No comment provided by engineer. */ "Fix" = "Reparieren"; @@ -3257,6 +3260,9 @@ snd error text */ /* No comment provided by engineer. */ "Member" = "Mitglied"; +/* past/unknown group member */ +"Member %@" = "Mitglied %@"; + /* profile update event chat item */ "member %@ changed to %@" = "Der Mitgliedsname von %1$@ wurde auf %2$@ geändert"; @@ -4072,9 +4078,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Port"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren."; @@ -6350,3 +6353,4 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Ihre SimpleX-Adresse"; + diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index ecd69f675a..6c8232fb63 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -1674,7 +1674,7 @@ swipe action */ "Delete chat profile?" = "¿Eliminar perfil?"; /* alert title */ -"Delete chat with member?" = "¿Eliminar chat con el miembro?"; +"Delete chat with member?" = "¿Eliminar chat con miembro?"; /* No comment provided by engineer. */ "Delete chat?" = "¿Eliminar chat?"; @@ -2041,7 +2041,7 @@ chat item action */ "Enable in direct chats (BETA)!" = "¡Activar en chats directos (BETA)!"; /* No comment provided by engineer. */ -"Enable instant notifications?" = "¿Activar notificación instantánea?"; +"Enable instant notifications?" = "¿Activar notificaciones instantáneas?"; /* No comment provided by engineer. */ "Enable lock" = "Activar bloqueo"; @@ -2266,10 +2266,10 @@ chat item action */ "Error decrypting file" = "Error al descifrar el archivo"; /* alert title */ -"Error deleting chat database" = "Error al eliminar base de datos"; +"Error deleting chat" = "Error al eliminar el chat con el miembro"; /* alert title */ -"Error deleting chat" = "Error al eliminar el chat con el miembro"; +"Error deleting chat database" = "Error al eliminar base de datos"; /* alert title */ "Error deleting chat!" = "¡Error al eliminar chat!"; @@ -2573,6 +2573,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Encuentra chats mas rápido"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Posiblemente la huella del certificado en la dirección del servidor es incorrecta"; + /* No comment provided by engineer. */ "Fix" = "Reparar"; @@ -3257,6 +3260,9 @@ snd error text */ /* No comment provided by engineer. */ "Member" = "Miembro"; +/* past/unknown group member */ +"Member %@" = "Miembro %@"; + /* profile update event chat item */ "member %@ changed to %@" = "el miembro %1$@ ha cambiado a %2$@"; @@ -4072,9 +4078,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Puerto"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Posiblemente la huella del certificado en la dirección del servidor es incorrecta"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva el último borrador del mensaje con los datos adjuntos."; @@ -5257,7 +5260,7 @@ report reason */ "Tail" = "Cola"; /* No comment provided by engineer. */ -"Take picture" = "Tomar foto"; +"Take picture" = "Hacer foto"; /* No comment provided by engineer. */ "Tap button " = "Pulsa el botón "; @@ -6350,3 +6353,4 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Mi dirección SimpleX"; + diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 5bda2a0438..76e4c1be0d 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -1428,6 +1428,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Löydä keskustelut nopeammin"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; + /* No comment provided by engineer. */ "Fix" = "Korjaa"; @@ -2240,9 +2243,6 @@ new chat action */ /* No comment provided by engineer. */ "Polish interface" = "Puolalainen käyttöliittymä"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Säilytä viimeinen viestiluonnos liitteineen."; @@ -3401,3 +3401,4 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "SimpleX-osoitteesi"; + diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 90ac30f87d..2a210cbfa8 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -2465,6 +2465,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Recherche de message plus rapide"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; + /* No comment provided by engineer. */ "Fix" = "Réparer"; @@ -3751,9 +3754,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Port"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserver le brouillon du dernier message, avec les pièces jointes."; @@ -5743,3 +5743,4 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "Votre adresse SimpleX"; + diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 2e2ce8bb68..bb4d4b1eca 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -651,7 +651,7 @@ swipe action */ "Anybody can host servers." = "Bárki üzemeltethet kiszolgálókat."; /* No comment provided by engineer. */ -"App build: %@" = "Az alkalmazás összeállítási száma: %@"; +"App build: %@" = "Alkalmazás összeállítási száma: %@"; /* No comment provided by engineer. */ "App data migration" = "Alkalmazásadatok átköltöztetése"; @@ -675,10 +675,10 @@ swipe action */ "App session" = "Alkalmazás munkamenete"; /* No comment provided by engineer. */ -"App version" = "Az alkalmazás verziója"; +"App version" = "Alkalmazás verziója"; /* No comment provided by engineer. */ -"App version: v%@" = "Az alkalmazás verziója: v%@"; +"App version: v%@" = "Alkalmazás verziója: v%@"; /* No comment provided by engineer. */ "Appearance" = "Megjelenés"; @@ -2266,10 +2266,10 @@ chat item action */ "Error decrypting file" = "Hiba történt a fájl visszafejtésekor"; /* alert title */ -"Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor"; +"Error deleting chat" = "Hiba a taggal való csevegés törlésekor"; /* alert title */ -"Error deleting chat" = "Hiba a taggal való csevegés törlésekor"; +"Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor"; /* alert title */ "Error deleting chat!" = "Hiba történt a csevegés törlésekor!"; @@ -2573,6 +2573,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Csevegési üzenetek gyorsabb megtalálása"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen"; + /* No comment provided by engineer. */ "Fix" = "Javítás"; @@ -3257,6 +3260,9 @@ snd error text */ /* No comment provided by engineer. */ "Member" = "Tag"; +/* past/unknown group member */ +"Member %@" = "%@ ismeretlen vagy már nem tag"; + /* profile update event chat item */ "member %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; @@ -4072,9 +4078,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Port"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt."; @@ -5040,7 +5043,7 @@ chat item action */ "Show percentage" = "Százalék megjelenítése"; /* No comment provided by engineer. */ -"Show preview" = "Értesítések előnézetének megjelenítése"; +"Show preview" = "Értesítésekben megjelenő információk"; /* No comment provided by engineer. */ "Show QR code" = "QR-kód megjelenítése"; @@ -6350,3 +6353,4 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Profil SimpleX-címe"; + diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index bb19130ae4..2eba58ab47 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -2266,10 +2266,10 @@ chat item action */ "Error decrypting file" = "Errore decifrando il file"; /* alert title */ -"Error deleting chat database" = "Errore nell'eliminazione del database della chat"; +"Error deleting chat" = "Errore di eliminazione della chat con il membro"; /* alert title */ -"Error deleting chat" = "Errore di eliminazione della chat con il membro"; +"Error deleting chat database" = "Errore nell'eliminazione del database della chat"; /* alert title */ "Error deleting chat!" = "Errore nell'eliminazione della chat!"; @@ -2573,6 +2573,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Trova le chat più velocemente"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata"; + /* No comment provided by engineer. */ "Fix" = "Correggi"; @@ -3257,6 +3260,9 @@ snd error text */ /* No comment provided by engineer. */ "Member" = "Membro"; +/* past/unknown group member */ +"Member %@" = "Membro %@"; + /* profile update event chat item */ "member %@ changed to %@" = "il membro %1$@ è diventato %2$@"; @@ -4072,9 +4078,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Porta"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Probabilmente l'impronta del certificato nell'indirizzo del server è sbagliata"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserva la bozza dell'ultimo messaggio, con gli allegati."; @@ -6350,3 +6353,4 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Il tuo indirizzo SimpleX"; + diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 33e8a650bc..de2f23ccce 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1662,6 +1662,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "チャットを素早く検索"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "サーバアドレスの証明証IDが正しくないかもしれません"; + /* No comment provided by engineer. */ "Fix" = "修正"; @@ -2486,9 +2489,6 @@ new chat action */ /* No comment provided by engineer. */ "Polish interface" = "ポーランド語UI"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "サーバアドレスの証明証IDが正しくないかもしれません"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "添付を含めて、下書きを保存する。"; @@ -3629,3 +3629,4 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "あなたのSimpleXアドレス"; + diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 457e9200b4..1d5fef3fe3 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -2203,10 +2203,10 @@ chat item action */ "Error decrypting file" = "Fout bij het ontsleutelen van bestand"; /* alert title */ -"Error deleting chat database" = "Fout bij het verwijderen van de chat database"; +"Error deleting chat" = "Fout bij het verwijderen van chat met lid"; /* alert title */ -"Error deleting chat" = "Fout bij het verwijderen van chat met lid"; +"Error deleting chat database" = "Fout bij het verwijderen van de chat database"; /* alert title */ "Error deleting chat!" = "Fout bij verwijderen gesprek!"; @@ -2498,6 +2498,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Vind chats sneller"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Mogelijk is de certificaat vingerafdruk in het server adres onjuist"; + /* No comment provided by engineer. */ "Fix" = "Herstel"; @@ -3937,9 +3940,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Poort"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Mogelijk is de certificaat vingerafdruk in het server adres onjuist"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Bewaar het laatste berichtconcept, met bijlagen."; @@ -6080,3 +6080,4 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Uw SimpleX adres"; + diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 44530d9217..cf08c2b40a 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -2273,6 +2273,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Szybciej znajduj czaty"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy"; + /* No comment provided by engineer. */ "Fix" = "Napraw"; @@ -3463,9 +3466,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Port"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Możliwe, że odcisk palca certyfikatu w adresie serwera jest nieprawidłowy"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Zachowaj ostatnią wersję roboczą wiadomości wraz z załącznikami."; @@ -5332,3 +5332,4 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "Twój adres SimpleX"; + diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index fcec9cf789..b2bdf39086 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -2266,10 +2266,10 @@ chat item action */ "Error decrypting file" = "Ошибка расшифровки файла"; /* alert title */ -"Error deleting chat database" = "Ошибка при удалении данных чата"; +"Error deleting chat" = "Ошибка при удалении чата с членом группы"; /* alert title */ -"Error deleting chat" = "Ошибка при удалении чата с членом группы"; +"Error deleting chat database" = "Ошибка при удалении данных чата"; /* alert title */ "Error deleting chat!" = "Ошибка при удалении чата!"; @@ -2438,6 +2438,9 @@ file error text snd error text */ "Error: %@" = "Ошибка: %@"; +/* server test error */ +"Error: %@." = "Ошибка: %@."; + /* No comment provided by engineer. */ "Error: no database file" = "Ошибка: данные чата не найдены"; @@ -2573,6 +2576,18 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Быстро найти чаты"; +/* No comment provided by engineer. */ +"Fingerprint in destination server address does not match certificate: %@." = "Хэш в адресе сервера назначения не соответствует сертификату: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in forwarding server address does not match certificate: %@." = "Хэш в адресе пересылающего сервера не соответствует сертификату: %@."; + +/* No comment provided by engineer. */ +"Fingerprint in server address does not match certificate: %@." = "Хэш в адресе сервера не соответствует сертификату: %@."; + +/* server test error */ +"Fingerprint in server address does not match certificate." = "Возможно, хэш сертификата в адресе сервера неверный"; + /* No comment provided by engineer. */ "Fix" = "Починить"; @@ -3257,6 +3272,9 @@ snd error text */ /* No comment provided by engineer. */ "Member" = "Член группы"; +/* past/unknown group member */ +"Member %@" = "Член группы %@"; + /* profile update event chat item */ "member %@ changed to %@" = "член %1$@ изменился на %2$@"; @@ -4072,9 +4090,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Порт"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Возможно, хэш сертификата в адресе сервера неверный"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Сохранить последний черновик, вместе с вложениями."; @@ -5111,6 +5126,9 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Аудит SimpleX протоколов от Trail of Bits."; +/* simplex link type */ +"SimpleX relay link" = "Ссылка SimpleX relay"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Упрощенный режим Инкогнито"; @@ -6347,3 +6365,4 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваш адрес SimpleX"; + diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 888a1bfa25..923c1960c7 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -1380,6 +1380,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "ค้นหาแชทได้เร็วขึ้น"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; + /* No comment provided by engineer. */ "Fix" = "แก้ไข"; @@ -2180,9 +2183,6 @@ new chat action */ /* No comment provided by engineer. */ "Polish interface" = "อินเตอร์เฟซภาษาโปแลนด์"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ"; @@ -3305,3 +3305,4 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "ที่อยู่ SimpleX ของคุณ"; + diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 554324b623..f6d38b0632 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -557,6 +557,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow downgrade" = "Sürüm düşürmeye izin ver"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Dosyalara ve medyaya yalnızca iletişiminiz izin verdiğinde izin verin."; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde)"; @@ -608,6 +611,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Kişilerinizin kaybolan mesajlar göndermesine izin verin."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Kişilerinizin dosya ve medya göndermesine izin verin."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Kişilerinizin sesli mesajlar göndermesine izin verin."; @@ -861,6 +867,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "bold" = "kalın"; +/* No comment provided by engineer. */ +"Bot" = "Bot"; + /* No comment provided by engineer. */ "Both you and your contact can add message reactions." = "Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir."; @@ -873,6 +882,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Sen ve konuştuğun kişi kaybolan mesajlar gönderebilir."; +/* No comment provided by engineer. */ +"Both you and your contact can send files and media." = "Sen de kişilerin de medya ve dosya gönderebilir."; + /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Sen ve konuştuğun kişi sesli mesaj gönderebilir."; @@ -1790,6 +1802,9 @@ swipe action */ /* No comment provided by engineer. */ "Delivery receipts!" = "Mesaj gönderildi bilgisi!"; +/* No comment provided by engineer. */ +"Deprecated options" = "Kullanımdan kaldırılan seçenekler"; + /* No comment provided by engineer. */ "Description" = "Açıklama"; @@ -2251,10 +2266,10 @@ chat item action */ "Error decrypting file" = "Dosya şifresi çözülürken hata oluştu"; /* alert title */ -"Error deleting chat database" = "Sohbet veritabanı silinirken sorun oluştu"; +"Error deleting chat" = "Üye ile sohbet silme hatası"; /* alert title */ -"Error deleting chat" = "Üye ile sohbet silme hatası"; +"Error deleting chat database" = "Sohbet veritabanı silinirken sorun oluştu"; /* alert title */ "Error deleting chat!" = "Sohbet silinirken hata oluştu!"; @@ -2370,6 +2385,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Mesaj gönderilirken hata oluştu"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Otomatik kabul ayarında hata"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Görüldü ayarlanırken hata oluştu!"; @@ -2528,6 +2546,9 @@ snd error text */ /* chat feature */ "Files and media" = "Dosyalar ve medya"; +/* No comment provided by engineer. */ +"Files and media are prohibited in this chat." = "Bu sohbette dosyalar ve medya yasaktır."; + /* No comment provided by engineer. */ "Files and media are prohibited." = "Dosyalar ve medya bu grupta yasaklandı."; @@ -2552,6 +2573,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Sohbetleri daha hızlı bul"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; + /* No comment provided by engineer. */ "Fix" = "Düzelt"; @@ -3107,6 +3131,9 @@ snd error text */ /* alert title */ "Keep unused invitation?" = "Kullanılmamış davet tutulsun mu?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "Sohbetlerinizi temiz tutun"; + /* No comment provided by engineer. */ "Keep your connections" = "Bağlantılarınızı koruyun"; @@ -3233,6 +3260,9 @@ snd error text */ /* No comment provided by engineer. */ "Member" = "Kişi"; +/* past/unknown group member */ +"Member %@" = "Üye%@"; + /* profile update event chat item */ "member %@ changed to %@" = "kişi %1$@ , %2$@ olarak değişti"; @@ -3248,6 +3278,9 @@ snd error text */ /* item status text */ "Member inactive" = "Üye inaktif"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Üye silinmiş - istek kabul edilemez"; + /* chat feature */ "Member reports" = "Üye raporları"; @@ -3790,6 +3823,9 @@ new chat action */ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "Sadece sen kaybolan mesajlar gönderebilirsin."; +/* No comment provided by engineer. */ +"Only you can send files and media." = "Sadece sen dosya ve medya gönderebilirsin."; + /* No comment provided by engineer. */ "Only you can send voice messages." = "Sadece sen sesli mesajlar gönderebilirsin."; @@ -3805,6 +3841,9 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send disappearing messages." = "Sadece karşıdaki kişi kaybolan mesajlar gönderebilir."; +/* No comment provided by engineer. */ +"Only your contact can send files and media." = "Sadece kişilerin dosya ve medya gönderebilir."; + /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Sadece karşıdaki kişi sesli mesajlar gönderebilir."; @@ -3820,9 +3859,15 @@ new chat action */ /* authentication reason */ "Open chat console" = "Sohbet konsolunu aç"; +/* alert action */ +"Open clean link" = "Temiz linki aç"; + /* No comment provided by engineer. */ "Open conditions" = "Açık koşullar"; +/* alert action */ +"Open full link" = "Tam linki aç"; + /* new chat action */ "Open group" = "Grubu aç"; @@ -3850,6 +3895,9 @@ new chat action */ /* No comment provided by engineer. */ "Open to join" = "Katılmak için aç"; +/* No comment provided by engineer. */ +"Open to use bot" = "Botu kullanmak için aç"; + /* No comment provided by engineer. */ "Opening app…" = "Uygulama açılıyor…"; @@ -4030,9 +4078,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Port"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Muhtemelen, sunucu adresindeki parmakizi sertifikası doğru değil"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Son mesaj taslağını ekleriyle birlikte koru."; @@ -4329,6 +4374,9 @@ swipe action */ /* No comment provided by engineer. */ "Remove image" = "Resmi kaldır"; +/* No comment provided by engineer. */ +"Remove link tracking" = "Bağlantı izlemeyi kaldır"; + /* No comment provided by engineer. */ "Remove member" = "Kişiyi sil"; @@ -4419,6 +4467,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "katılma isteği reddedildi"; +/* rcv group event chat item */ +"requested connection" = "istenilen bağlantı"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "%@ grubundan bağlantı isteği"; + /* chat list item title */ "requested to connect" = "bağlanma isteği gönderildi"; @@ -4894,6 +4948,9 @@ chat item action */ /* No comment provided by engineer. */ "Set passphrase to export" = "Dışa aktarmak için parola ayarla"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Profil biyografisi ve hoşgeldiniz mesajı düzenle."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "Yeni üyeler için gösterilen bir mesaj ayarla!"; @@ -4934,6 +4991,12 @@ chat item action */ /* No comment provided by engineer. */ "Share link" = "Bağlantıyı paylaş"; +/* alert button */ +"Share old address" = "Eski adresi paylaş"; + +/* alert button */ +"Share old link" = "Eski bağlantıyı paylaş"; + /* No comment provided by engineer. */ "Share profile" = "Profil paylaş"; @@ -4949,12 +5012,18 @@ chat item action */ /* No comment provided by engineer. */ "Share with contacts" = "Kişilerle paylaş"; +/* No comment provided by engineer. */ +"Share your address" = "Adresini paylaş"; + /* No comment provided by engineer. */ "Short description" = "Kısa açıklama"; /* No comment provided by engineer. */ "Short link" = "Kısa bağlantı"; +/* No comment provided by engineer. */ +"Short SimpleX address" = "Kısa SimpleX adresi"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Gizli yönlendirme yoluyla gönderilen mesajlarda → işaretini göster."; @@ -5045,6 +5114,9 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "SimpleX protokolleri Trail of Bits tarafından incelenmiştir."; +/* simplex link type */ +"SimpleX relay link" = "SimpleX aktarıcı bağlantısı"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Basitleştirilmiş gizli mod"; @@ -5199,6 +5271,9 @@ report reason */ /* No comment provided by engineer. */ "Tap Connect to send request" = "Bağlan'a dokunarak isteği gönderin"; +/* No comment provided by engineer. */ +"Tap Connect to use bot" = "Botu kullanmak için Bağlan tuşuna bas"; + /* No comment provided by engineer. */ "Tap Create SimpleX address in the menu to create it later." = "Daha sonra oluşturmak için menüden BasitX adresi oluştur'a dokunun."; @@ -5271,6 +5346,9 @@ report reason */ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Kullanıcılar için teşekkürler - Weblate aracılığıyla katkıda bulun!"; +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "Adres kısa olacak ve profiliniz bu adres üzerinden paylaşılacaktır."; + /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın."; @@ -5310,6 +5388,9 @@ report reason */ /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Bir sonraki mesajın kimliği yanlış (bir öncekinden az veya aynı).\nBazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir."; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "Bağlantı kısa olacak ve grup profili bağlantı üzerinden paylaşılacaktır."; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "Mesaj tüm üyeler için silinecektir."; @@ -5406,6 +5487,12 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Bu ayar, geçerli sohbet profiliniz **%@** deki mesajlara uygulanır."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Bu ayar, mevcut profiliniz içindir."; + +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Kaybolma süresi yalnızca yeni kişiler için ayarlanır."; + /* No comment provided by engineer. */ "Title" = "Başlık"; @@ -5454,6 +5541,9 @@ report reason */ /* No comment provided by engineer. */ "To send" = "Göndermek için"; +/* alert message */ +"To send commands you must be connected." = "Komut göndermek için bağlı olmanız gerekir."; + /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Anlık anlık bildirimleri desteklemek için sohbet veritabanının taşınması gerekir."; @@ -5622,9 +5712,27 @@ report reason */ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Ayarların güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır."; +/* alert button */ +"Upgrade" = "Yükseltme"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Adres güncelleme"; + +/* alert message */ +"Upgrade address?" = "Adres güncellensin mi?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "Yükselt ve sohbeti aç"; +/* alert message */ +"Upgrade group link?" = "Grub linki güncellensin mi?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Linki güncelle"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Adresini yükselt"; + /* No comment provided by engineer. */ "Upload errors" = "Yükleme hataları"; @@ -5667,6 +5775,9 @@ report reason */ /* No comment provided by engineer. */ "Use from desktop" = "Bilgisayardan kullan"; +/* No comment provided by engineer. */ +"Use incognito profile" = "Gizli profil kullan"; + /* No comment provided by engineer. */ "Use iOS call interface" = "iOS arama arayüzünden kullan"; @@ -5859,6 +5970,9 @@ report reason */ /* No comment provided by engineer. */ "Welcome message is too long" = "Hoş geldiniz mesajı çok uzun"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Kişilerine hoş geldin👋"; + /* No comment provided by engineer. */ "What's new" = "Neler yeni"; @@ -6239,3 +6353,4 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "SimpleX adresin"; + diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 532d2ab744..2c3d7b083d 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -2248,10 +2248,10 @@ chat item action */ "Error decrypting file" = "Помилка розшифрування файлу"; /* alert title */ -"Error deleting chat database" = "Помилка видалення бази даних чату"; +"Error deleting chat" = "Помилка при видаленні чату з учасником"; /* alert title */ -"Error deleting chat" = "Помилка при видаленні чату з учасником"; +"Error deleting chat database" = "Помилка видалення бази даних чату"; /* alert title */ "Error deleting chat!" = "Помилка видалення чату!"; @@ -2549,6 +2549,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Швидше знаходьте чати"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; + /* No comment provided by engineer. */ "Fix" = "Виправити"; @@ -4030,9 +4033,6 @@ new chat action */ /* No comment provided by engineer. */ "Port" = "Порт"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Зберегти чернетку останнього повідомлення з вкладеннями."; @@ -6287,3 +6287,4 @@ report reason */ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваша адреса SimpleX"; + diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index ac5bad738c..2803b374f7 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -2441,6 +2441,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "更快地查找聊天记录"; +/* server test error */ +"Fingerprint in server address does not match certificate." = "服务器地址中的证书指纹可能不正确"; + /* No comment provided by engineer. */ "Fix" = "修复"; @@ -3826,9 +3829,6 @@ new chat action */ /* No comment provided by engineer. */ "Polish interface" = "波兰语界面"; -/* server test error */ -"Fingerprint in server address does not match certificate." = "服务器地址中的证书指纹可能不正确"; - /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "保留最后的消息草稿及其附件。"; @@ -5668,3 +5668,4 @@ chat item action */ /* No comment provided by engineer. */ "Your SimpleX address" = "您的 SimpleX 地址"; + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index c2096dea57..96c539109d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -2516,4 +2516,5 @@ افتح الرابط الكامل أزل تتبع الروابط رابط مُرحل SimpleX + خطأ في وضع علامة \"مقروءة\" على الدردشة مع العضو diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index 384a885e3b..bf04fd6a1e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -1559,7 +1559,7 @@ Настолното устройство е заето Връзката с настолното устройство бе прекъсната Критична грешка - Бивш член %1$s + Член %1$s неизвестен статус неизвестен Вътрешна грешка @@ -1859,7 +1859,7 @@ Размер на шрифта Статус на съобщението: %s Задай тема по подразбиране - Потвърдете файловете от неизвестни сървъри. + Потвърди файлове от неизвестни сървъри. Изпратени съобщения Грешка при стартиране на WebView. Актуализирайте системата си до новата версия. Моля, свържете се с разработчиците.\nГрешка: %s Други XFTP сървъри @@ -1967,7 +1967,7 @@ Качени файлове Грешки при качване Размер - Парчета изтрити + Изтрити парчета Изтеглени парчета Изтеглени файлове Настройки @@ -2313,7 +2313,7 @@ Блокиране на членовете за всички? Бизнес връзка С използването на SimpleX Chat вие се съгласявате със:\n- изпращане само на легално съдържание в публични групи.\n- уважение към другите потребители – без спам. - Промената на профила е невъзможна + Промяната на профила е невъзможна съобщенията не могат да бъдат изпратени Каталонски, индонезийски, румънски и виетнамски - благодарение на нашите потребители! криптиране от край до край.]]> @@ -2508,4 +2508,21 @@ Членът е изтрит - не може да се приеме заявката заявка за връзка от група %1$s Тази настройка е за текущия профил + Разреши файлове и медия само ако вашият контакт ги разрешава. + Позволи на вашите контактите да изпращат файлове и медия. + Бот + И вие, и вашият контакт можете да изпращате файлове и медия. + Отпадащи опции + Грешка при маркиране на чата като прочетен + Файлове и медията са забранени в този чат. + Само вие можете да изпращате файлове и медия. + Само вашият контакт може да изпраща файлове и медия. + Отвори почистен линк + Отвори пълен линк + Отвори за използване на бот + Забрани изпращането на файлове и медия. + Премахни параметри за проследяване от линковете + SimpleX реле линк + Натисни Свързване, за използване на бота + За да изпращате команди, трябва да сте свързани. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index 71e2e57576..c9b1f79694 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -1999,7 +1999,7 @@ Convidar a xatejar Nou rol de membre No hi ha contactes per afegir - Membre anterior %1$s + Membre %1$s Ometre convidar membres desconegut Convidar membres @@ -2496,4 +2496,5 @@ Obrir enllaç complet Eliminar el seguiment de l\'enllaç Opcions obsoletes + Enllaç de servidor SimpleX diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 76172ccab8..4d6aa47fd3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -548,8 +548,8 @@ Smazat pro mě upraveno neautorizované odeslání - jste pozváni do skupiny - připojit jako %s + Jste pozváni do skupiny + Připojit jako %s připojuji… Začněte nový chat Chat s vývojáři @@ -1172,7 +1172,7 @@ Sdílet adresu s kontakty\? Přestat sdílet adresu\? Vytvořit adresu, aby se s vámi lidé mohli spojit. - Uložit nastavení automatického přijímání + Uložit nastavení SimpleX adresy Přestat sdílet Automaticky přijmout Můžete vytvořit později @@ -1363,7 +1363,7 @@ 6 nových jazyků pro rozhraní Aplikace šifruje nové místní soubory (kromě videí) Arabština, bulharština, finština, hebrejština, thajština a ukrajinština - díky uživatelům a Weblate. - propojeno napřímo + požádáno o připojení Otevřít Šifrování uložených souborů a médií Chyba vytváření kontaktu @@ -1374,7 +1374,7 @@ Vytvořit nový profil v desktopové aplikaci. 💻 Změnit inkognito při připojování. - připojení k adresáři skupin (BETA)!\n- doručenky (až 20 členů).\n- rychlejší a stabilnější. - odeslat přímou zprávu + odeslat pro připojení smazaný kontakt Chyba Vytvořit profil @@ -1573,7 +1573,7 @@ Vyhledávání přijímá pozvánky. Zobrazit pomalé API volání odstraněna kontaktní adresa - Vložte člena %1$s + Člen %1$s Vložte odkaz na připojení! Nejnovější historie a vylepšený directory bot. Soukromé poznámky @@ -2282,7 +2282,7 @@ Report: %s Nahlásit další: uvidí pouze skupinový moderátoři. Můžete nastavit název připojení, pro pamatování, s kým byl odkaz sdílen. - Vaše připojení bylo přesunuto na %s, ale došlo k neočekávané chybě při přesměrování na profil. + Vaše připojení bylo přesunuto na %s, ale nastala chyba při přepínání profilu. Pro každý profil použijte různé přihlašovací údaje Neznámé servery Pro ochranu vaší IP adresy, soukromé směrování používá vaše servery SMP k doručování zpráv. @@ -2396,7 +2396,7 @@ Přijmout Přijmout člena schválen adminy - Přidat krátký odkaz + Upgradovat adresu přijat %1$s Vás přijal schválení @@ -2466,4 +2466,7 @@ Odmítnout žádost o kontakt Odebrat sledování odkazů Odstranit zprávy a blokovat členy. + koncovým šifrováním.]]> + SimpleX relé odkaz + Bez soukromého směrování sezení diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index e788e2fcc4..db5e983983 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -2606,4 +2606,5 @@ Verbinden tippen, um den Bot zu nutzen. Um Befehle senden zu können, müssen Sie verbunden sein. SimpleX Relais-Link + Fehler beim Versuch, den Chat mit einem Mitglied als gelesen zu markieren diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 720be554b9..fa356715db 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -23,7 +23,7 @@ ICONO DE LA APLICACIÓN La optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración. El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos. - Se puede desactivar en Configuración – las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.]]> + Se puede desactivar en la configuración. En ese caso las notificaciones se seguirán mostrando mientras la aplicación esté en funcionamiento.]]> Siempre activo Permitir y después: @@ -438,7 +438,7 @@ Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos. La base de datos no está cifrada. Escribe una contraseña para protegerla. Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas. - Notificación instantánea + Notificaciones instantáneas Configuración avanzada Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes. Cómo afecta a la batería @@ -463,7 +463,7 @@ ¡Invitación caducada! ha salido Mensajes en vivo - Notificación instantánea + Notificaciones instantáneas Servicio El mensaje se marcará para eliminar. El destinatario o destinatarios podrán revelar este mensaje. ¡Mensaje en vivo! @@ -738,7 +738,7 @@ Esta acción es irreversible. Los mensajes enviados y recibidos anteriores a la selección serán eliminados. Podría tardar varios minutos. Esta configuración se aplica a los mensajes del perfil actual ¡Esta cadena no es un enlace de conexión! - SimpleX se ejecuta en segundo plano en lugar de usar notificaciones push.]]> + SimpleX se ejecuta en segundo plano.]]> Configuración Altavoz desactivado Inciar chat nuevo @@ -2425,7 +2425,7 @@ Chat con miembros Chat con administradores Eliminar chat - ¿Eliminar chat con el miembro? + ¿Eliminar chat con miembro? Rechazar ¿Rechazar al miembro? Revisar miembros @@ -2531,4 +2531,5 @@ Limpiar enlaces de seguimiento Abrir enlace completo Enlace de servidor SimpleX + Error al marcar el chat con miembro como leído diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index 579bdf74db..a3c6a65e32 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -52,7 +52,7 @@ A csevegési profillal (alapértelmezett), vagy a kapcsolattal (BÉTA). Egy új, véletlenszerű profil lesz megosztva. A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. - Az alkalmazás összeállítási száma: %s + Alkalmazás összeállítási száma: %s Hang- és videóhívások Speciális hálózati beállítások A hangüzenetek küldése engedélyezve van a partnerei számára. @@ -63,7 +63,7 @@ Kapcsolódás folyamatban! Nem lehet fogadni a fájlt Hitelesítés elérhetetlen - Az alkalmazás verziója + Alkalmazás verziója Üdvözlőüzenet hozzáadása titkosítás elfogadása %s számára… \nElérhető az v5.1-es kiadásban @@ -161,7 +161,7 @@ Mindkét fél tud hívásokat kezdeményezni. Sikertelen hitelesítés %s összes új üzenete el lesz rejtve! - Az alkalmazás verziója: v%s + Alkalmazás verziója: v%s A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. Kiszolgáló hozzáadása Hang bekapcsolva @@ -882,7 +882,7 @@ Megnyitás Protokoll időtúllépése titok - Értesítések előnézetének megjelenítése + Értesítésekben megjelenő információk várakozás a visszaigazolásra… Fájl megállítása a csoporthivatkozáson keresztül @@ -1543,7 +1543,7 @@ Fejlesztői beállítások A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s %s hordozható eszköz elfoglalt]]> - %1$s tag + %1$s ismeretlen vagy már nem tag ismeretlen állapot %1$s a következőre módosította a nevét: %2$s eltávolította a kapcsolattartási címet @@ -2393,7 +2393,7 @@ csatlakozási kérés elutasítva Ön elhagyta a csoportot a tag régi verziót használ - Hiba a taggal való csevegés törlésekor + Hiba a csevegés törlésekor Ön nem tud üzeneteket küldeni! a partner nem áll készen nincs szinkronizálva @@ -2499,4 +2499,7 @@ Teljes hivatkozás megnyitása Nyomonkövetési paraméterek eltávolítása a hivatkozásokból SimpleX továbbítókiszolgáló-hivatkozás + Hiba a csevegés olvasottként való megjelölésekor + A célkiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. + A továbbítókiszolgáló címében szereplő ujjlenyomat nem egyezik a tanúsítvánnyal: %1$s. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 917133829c..55b108cfb9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -692,7 +692,7 @@ Kode QR tidak valid Kesalahan saat mengganti profil Ketuk untuk tempel tautan - Koneksi Anda dipindahkan ke %s tetapi terjadi kesalahan tak terduga saat mengarahkan Anda ke profil. + Koneksi Anda dipindahkan ke %s tetapi gagal saat mengarahkan Anda ke profil. Kirim kami email Kirim pertanyaan dan ide Kunci SimpleX @@ -2375,7 +2375,7 @@ grup telah dihapus Silakan tunggu pengurus grup untuk meninjau permintaanmu untuk bergabung ke grup ditinjau oleh pengurus - Tambah tautan singkat + Tingkatkan tautan menerimamu diterima %1$s Atur penerimaan anggota @@ -2478,4 +2478,29 @@ Deskripsi singkat: Alamat singkat SimpleX Tautan relay SimpleX + Sambut kontak Anda 👋 + Bio Anda: + Kontak bisnis Anda + Kontak Anda + Grup Anda + Profil Anda + Ketuk Hubungkan untuk chat + Tekan Hubungkan untuk kirim permintaan + Tekan Hubungkan untuk gunakan bot + Tekan Gabung grup + Batas waktu koneksi TCP bg + Alamat akan singkat, dan profil Anda akan dibagikan melalui alamat tersebut. + Tautan akan singkat, dan profil grup akan dibagikan melalui tautan tersebut. + Pengirim TIDAK akan diberi tahu. + Pengaturan ini untuk profil Anda saat ini + Waktu menghilang hanya diatur untuk kontak baru. + Untuk mengirim perintah, Anda harus terhubung. + Untuk menggunakan profil lain setelah mencoba hubungkan, hapus chat dan gunakan tautan lagi. + Perbarui alamat Anda + Tingkatkan + Tingkatkan alamat? + Tingkatkan tautan grup + Tingkatkan tautan grup? + Gunakan profil samaran + Pesan sambutan diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 65423f1bbd..35b0bab541 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -2535,4 +2535,5 @@ Apri link completo Rimuovi il tracciamento del link Link del relay SimpleX + Errore nel segnare la chat con il membro come letta diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index 3ba57571c7..7232cc56d3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -52,7 +52,7 @@ Toate conversațiile și mesajele vor fi șterse – această acțiune nu poate fi anulată! Toate mesajele vor fi șterse - această acțiune nu poate fi anulată! Permite reacții la mesaje doar dacă și contactul tău le permite. - Permite ștergerea ireversibilă a mesajelor trimise. (24 ore) + Permite ștergerea definitivă a mesajelor trimise. (24 de ore) Permite trimiterea de fișiere și media. Toate datele sunt șterse la introducerea lor. Toți membrii grupului vor rămâne conectați. @@ -63,12 +63,12 @@ Permite apeluri doar dacă le permite contactul tău. Permiteți mesaje vocale? Permite mesaje vocale doar dacă contactul tău le permite. - Permite ștergerea mesajelor ireversibile doar dacă și contactul tău îți permite ție. (24 ore) + Permite ștergerea definitivă a mesajelor doar dacă și contactul tău o permite. (24 de ore) Permite reacții la mesaje. Permite trimiterea de mesaje directe membrilor. Permite trimiterea de mesaje efemere. Permite trimiterea de mesaje vocale. - Permite mesaje efemere doar dacă le permite contactul tău. + Permite mesaje autodistructive doar dacă și contactul tău le permite. Permite contactelor tale să trimită mesaje care dispar. Aspect Versiunea aplicației @@ -88,16 +88,16 @@ Camera și microfon Atenție: arhiva va fi ștearsă.]]> Anulează previzualizarea fișierului - Un profil de chat gol cu numele furnizat este creat, iar aplicația se deschide ca de obicei. + Un profil de conversație gol cu numele furnizat este creat, iar aplicația se deschide ca de obicei. Se conectează deja! Aplicația poate primi notificări doar când rulează, niciun serviciu în fundal nu va fi lansat. Se alătură deja grupului! Mereu pornit Un nou profil aleatoriu va fi distribuit. - Android Keystore este utilizat pentru a stoca în siguranță fraza de acces – acest lucru permite funcționarea serviciului de notificare. + Android Keystore este folosit pentru a stoca în siguranță parola. Acest lucru permite funcționarea serviciului de notificări. și %d alte evenimente Răspunde la apel - Android Keystore va fi utilizat pentru a stoca în siguranță fraza de acces după ce reporniți aplicația sau schimbați fraza de acces – acest lucru va permite primirea notificărilor. + Android Keystore va fi folosit pentru a stoca în siguranță parola după ce repornești aplicația sau schimbi parola — acest lucru va permite primirea de notificări. APLICAȚIE Creează grup Apeluri audio și video @@ -143,7 +143,7 @@ Creează-ți profilul Apeluri audio și video Audio pornit - Frază de acces actuală… + Parola actuală… Creează link de grup Creează link creator @@ -171,13 +171,13 @@ Elimină membru Eliminați membrul? Resetează la valorile implicite - Resetați culoarea + Resetezi culoarea Resetați culorile Elimină imaginea Repetă încărcarea apel respins - Eliminați fraza de acces din Keystore? - Eliminați fraza de acces din setări? + Elimini parola din Keystore? + Elimini parola din setări? Repetă cererea de conectare? Necesar Reîncearcă @@ -190,20 +190,20 @@ Renegociezi criptarea? Resetare Respinge - Salvați fraza de acces în setări - Salvați și actualizați profilul grupului + Salvează parola în setări + Salvează și actualizează profilul grupului Repetă cererea de alăturare? Reporniți conversația salvat salvat de la %s Salvează Salvat - Salvat de la + Salvat din Renegociază - Salvați serverele + Salvează serverele Serverele ICE WebRTC salvate vor fi eliminate. - Salvați parola profilului - Salvați fraza de acces și deschideți chatul + Salvează parola profilului + Salvează parola și deschide conversația %s și %s Renegociază criptarea Salvează și notifică contactul @@ -216,8 +216,8 @@ Salvezi setările? Salvezi preferințele? Repornire - Restaurați copia de rezervă a bazei de date - Restaurați copia de rezervă a bazei de date? + Restaurează backupul bazei de date + Restaurezi backupul bazei de date? Eroare de restaurare a bazei de date %1$s eliminat %s și %s conectați @@ -225,15 +225,15 @@ Salvează Arată Respinge - Salvați serverele? + Salvezi serverele? Apel respins Mesaj salvat - Reporniți aplicația pentru a crea un nou profil de chat. - Salvați fraza de acces în Keystore - Salvați profilul grupului + Repornește aplicația pentru a crea un profil de conversație nou. + Salvează parola în Keystore + Salvează profilul grupului Repetă Trimite previzualizări de linkuri - Setați frază de acces + Setează parola Partajează adresa Trimis la Secundar @@ -244,26 +244,26 @@ Versiunea serverului este incompatibilă cu setările de rețea. Trimitere prin trimiterea de fișiere nu este încă acceptată - Scanați codul de securitate din aplicația contactului dvs. + Scanează codul de securitate din aplicația persoanei tale de contact. Selectează contactele Autodistrugere Răspuns trimis Partajează media… Partajează mesaj… - Afișează lista de chat în fereastră nouă + Afișează lista de conversații într-o fereastră nouă Afișează consola în fereastră nouă - Configurați fraza de acces a bazei de date - Setați parola bazei de date + Configurează parola bazei de date + Setează parola bazei de date setați adresă de contact nouă %s (actual) Codul sesiunii Expeditorul a anulat transferul de fișiere. - Serverul necesită autorizare pentru a crea cozi, verificați parola + Serverul necesită autorizare pentru a crea cozi. Verifică parola Partajează trimitere eșuată Caută sau lipește linkul SimpleX Setează numele contactului - Salvați mesajul de bun venit? + Salvezi mesajul de bun venit? Evaluarea securității Scanează codul QR de pe desktop Caută @@ -276,14 +276,14 @@ Mesaj trimis Scanează cod QR Trimite - Trimiteți întrebări și idei + Trimite întrebări și idei Afișează opțiunile pentru dezvoltatori sec Mesajele trimise vor fi șterse după ora setată. - Serverul necesită autorizare pentru încărcare, verificați parola + Serverul necesită autorizare pentru a încărca fișiere. Verifică parola Afișează contactul și mesajul Partajează fișier… - Nume contact… + Setează numele contactului… Trimite mesaj Trimiteți un mesaj care dispare (scanează sau lipește din clipboard) @@ -316,7 +316,7 @@ codul de securitate a fost schimbat Afișează ultimele mesaje Trimiteți un mesaj direct - Setați tema implicită + Setează tema implicită SimpleX SimpleX nu poate rula în fundal. Vei primi notificări doar atunci când aplicația rulează. Serviciul SimpleX Chat @@ -324,7 +324,7 @@ Oprire Linkuri SimpleX Linkurile SimpleX sunt interzise. - Securitatea chatului SimpleX a fost auditată de Trail of Bits. + Securitatea SimpleX Chat a fost auditată de Trail of Bits. Mesaje SimpleX Chat Apeluri SimpleX Chat Adresă SimpleX @@ -351,13 +351,13 @@ Niște servere au eșuat testul: Difuzor oprit Difuzor pornit - Copie de rezervă a datelor aplicației + Backup al datelor aplicației Tema aplicației Accent suplimentar 2 Începe conversația Toate modurile de culoare Folosește mereu releu - Aplică pentru + Aplică la Începe o nouă conversație Acordă o stea pe GitHub criptare standard end-to-end @@ -386,14 +386,14 @@ Creat la Migrează de pe alt dispozitiv pe noul dispozitiv și scanați codul QR.]]> Te conectezi prin adresa de contact? - Doriți să vă conectați printr-un link de unică folosință? + Vrei să te conectezi printr-un link unic? Conectare incognito Contactul deja există Schimbă codul de acces Puteți activa SimpleX Lock din Setări. Conversații Alege un fișier - Contactul dvs. trebuie să fie online pentru ca conexiunea să se finalizeze.\nPuteți anula această conexiune și elimina contactul (și încerca mai târziu cu un link nou). + Persoana ta de contact trebuie să fie online pentru ca procesul de conectare să se finalizeze.\nPoți anula această conexiune și șterge contactul (apoi poți încerca mai târziu cu un link nou). eroare apel Apelurile tale Schimbă @@ -404,8 +404,8 @@ Preferințe contact Te conectezi cu %1$s? Anulează previzualizarea imaginii - Discută cu dezvoltatorii - Trebuie să introduceți fraza de acces de fiecare dată când porniți aplicația – aceasta nu este stocată pe dispozitiv. + Conversează cu dezvoltatorii + Trebuie să introduci parola de fiecare dată când pornești aplicația - aceasta nu este stocată pe dispozitiv. blocat Preferințe conversație Mod întunecat @@ -422,7 +422,7 @@ Conectare prin link Puteți vedea din nou linkul de invitație în detaliile conexiunii. Profilurile tale de conversație - Creează profil de chat + Creează un profil de conversație apel încheiat %1$s Creează Bluetooth @@ -433,9 +433,9 @@ Contacte CONVERSAȚII BAZĂ DE DATE CONVERSAȚIE - Baza de date a conversației ștearsă + Baza de date a conversației a fost ștearsă Conversația rulează - Baza dvs. de date a chatului + Baza de date a conversațiilor tale Baza de date a conversațiilor tale nu este criptată - setează o parolă pentru a o proteja. Parola pentru criptarea bazei de date va fi actualizată și stocată în setări. ați schimbat rolul %s în %s @@ -479,17 +479,17 @@ Copiat Camera Ați invitat un contact - Profilul tău de chat va fi trimis\ncontactului tău + Profilul tău de conversație va fi trimis contactului tău. Contribuie Profil conversație aldin Puteți folosi marcarea pentru a formata mesajele: se apelează… Schimbă modul de autodistrugere - Baza de date a conversației importată + Baza de date a conversației a fost importată Trebuie să utilizați cea mai recentă versiune a bazei de date de chat DOAR pe un singur dispozitiv, altfel este posibil să nu mai primiți mesaje de la unele contacte. Nu se poate accesa Keystore pentru a salva parola bazei de date - Conversație migrată! + Conversația a fost migrată! Continuă apel în curs Apel în curs @@ -498,7 +498,7 @@ Nume contact Creează adresă Bază de date criptată! - Schimbați fraza de acces a bazei de date? + Schimbi parola bazei de date? Conversația este oprită Puteți porni discuția din Setările aplicației / Baza de date sau repornind aplicația. ați ieșit @@ -531,7 +531,7 @@ Șterge Confirmați fișiere de la servere necunoscute. am schimbat adresa pentru tine - Confirmați parola nouă… + Confirmă noua parolă… Conectare conexiune %1$d conexiune stabilită @@ -543,7 +543,7 @@ se conectează… Schimbați modul de blocare Versiunea aplicației: %s - pentru fiecare profil de conversație pe care le aveți în aplicație]]> + pentru fiecare profil de conversație pe care îl ai în aplicație.]]> Permiteți downgrade-ul pentru fiecare contact și membru al grupului.\nRețineți: dacă aveți multe conexiuni, consumul bateriei și al traficului de date poate crește considerabil, iar unele conexiuni pot eșua.]]> Conexiune terminată @@ -561,7 +561,7 @@ se conectează Schimbați rolul de grup? Modul color - După profilul de chat (implicit) sau după conexiune (BETA). + După profilul de conversație (implicit) sau după conexiune (BETA). Compară codurile de securitate cu contactele tale. Conexiune oprită Conexiune oprită @@ -579,7 +579,7 @@ se conectează… Eroare de conexiune (AUTENTIFICARE) Optimizarea bateriei este activă, dezactivând serviciul de fundal și cererile periodice pentru mesaje noi. Le puteți reactiva din setări. - conectat + Conectat Creează grup:: pentru a crea un nou grup.]]> Ștergeți conversația? Șterge @@ -595,7 +595,7 @@ rolul tău a fost schimbat în %s conectat se conectează - conectat + Conectat Ștergeți Buton de închidere Ștergeți verificarea @@ -607,7 +607,7 @@ complet De reținut: folosirea aceleiași baze de date în două aparate, va intrerupe descifrarea mesajelor din conexiunile tale, ca măsură de protecție.]]> Confirmați încărcarea - Confirmați că țineți minte parola de la baza de date pentru a o migra. + Confirmă că-ți amintești parola bazei de date pentru a o migra. Conexiune Ștergi profilul de conversație? Șterge pentru mine @@ -638,7 +638,7 @@ Descriere Șterge fișierul Șterge coada - Ștergeți contactul? + Ștergi contactul? Șterge Șterge Ștergi fișierele și conținutul media? @@ -655,20 +655,20 @@ Șterge Ștergeți mesajul? Livrare - Ștergeți conexiunea în așteptare? + Ștergi conexiunea în așteptare? Ștergeți serverul Șterge baza de date contact șters Ștergi grupul? Șterge baza de date de pe acest dispozitiv - Șterge profil de conversație - Șterge fișierele pentru toate profilurile de chat + Șterge profilul de conversație + Șterge fișierele pentru toate profilurile de conversație Oricine poate găzdui servere. Contacte arhivate Beta Apeluri interzise! Nu se poate apela membrul grupului - Bază de date conversație exportată + Baza de date a conversației a fost exportată Verifică pentru actualizări contactul %1$s s-a schimbat în %2$s Contactul este șters. @@ -688,7 +688,7 @@ BAZĂ DE DATE CONVERSAȚIE Conectează-te cu prietenii mai ușor. încercări - Completat + Finalizat ID-urile bazei de date și opțiunea de izolare a transportului. ID bază de date ID bază de date: %d @@ -700,7 +700,7 @@ Contactele pot marca mesajele pentru ștergere; tu le vei putea vizualiza. Downgrade al bazei de date Migrarea bazei de date este în curs.\nAceasta poate dura câteva minute. - Parola bazei de date și export + Parola și exportul bazei de date Toate profilurile Profil actual Confirmi ștergerea contactului? @@ -735,7 +735,7 @@ Apeluri îmbunătățite Apel audio primit Încheie apelul - marcajele şterse + marcat ca șters %d mesaje marcate șterse moderat de %s %1$d alte erori de fișier. @@ -769,7 +769,7 @@ eroare la afișarea conținutului criptare end-to-end, cu confidențialitate perfectă în avans, nerepudiere și recuperare în caz de compromitere.]]> criptare end-to-end rezistentă la computere cuantice, cu confidențialitate perfectă în avans, nerepudiere și recuperare în caz de compromitere.]]> - Acest chat este protejat prin criptare end-to-end. + Această conversație este protejată prin criptare integrală. Link complet incognito printr-un link de unică folosință a + b @@ -799,24 +799,24 @@ moderat conversație nevalidă date nevalide - Acest chat este protejat prin criptare end-to-end rezistentă la detecții cuantice. + Această conversație este protejată prin criptare end-to-end rezistentă la atacuri cuantice. Note private Eroare la citirea parolei bazei de date Versiunea bazei de date este incompatibilă Invitația la grup nu mai este validă, a fost eliminată de expeditor. Se alătură grupului - Părăsiți grupul + Părăsește grupul Eroare la ștergerea linkului de grup Eroare la trimiterea invitației dezactivat Membrii pot trimite mesaje vocale. - Filtrați conversațiile necitite și favorite. + Filtrează conversațiile necitite și favorite. Migrare în curs Managementul rețelei Finalizează migrarea pe un alt dispozitiv. Incompatibil! - Cum să utilizați serverele dvs. - Importați baza de date de chat? + Cum să utilizezi serverele tale + Importă baza de date a conversației? Ștergeți raportul Redirecționați mesajele… Șterge doar conversația @@ -829,7 +829,7 @@ PENTRU CONSOLĂ Moderat la Remediați conexiunea - Multiple profile de chat + Profiluri de conversație multiple Ascuns Introducere parolă Redirecționare server: %1$s\nEroare:%2$s @@ -866,7 +866,7 @@ Alătură-te conversațiilor de grup Livrarea mesajelor îmbunătățită Interfață lituaniană - Faceți conversațiile să arate diferit! + Fă-ți conversațiile să arate diferit! Menționați membrii 👋 Ajutați administratorii să modereze grupurile lor. Doar un singur dispozitiv poate funcționa în același timp @@ -874,9 +874,9 @@ Se descarcă arhiva Finalizați migrarea Eroare la verificarea parolei: - Eroare la importarea bazei de date de conversații + Eroare la importarea bazei de date a conversației Mesaje - Eroare la exportarea bazei de date de conversații + Eroare la exportarea bazei de date a conversației Nicio informație, încercați să reîncărcați Activează jurnalele Autentificarea dispozitivului este dezactivată. Se dezactivează SimpleX Lock. @@ -890,18 +890,18 @@ Nu există servere de mesaje. Nu există servere pentru rutarea mesajelor private. Nu există servere pentru a primi mesaje. - Pentru profilul de chat %s: + Pentru profilul de conversație %s: Nu există servere pentru a primi fișiere. Eroare la conectarea la serverul de redirecționare %1$s. Vă rugăm să încercați mai târziu. Versiunea serverului de destinație %1$s este incompatibilă cu serverul de redirecționare %2$s. Notificările instantanee sunt dezactivate! Dezactivare notificări Migrați de pe un alt dispozitiv - Eroare la ștergerea bazei de date a chatului + Eroare la ștergerea bazei de date a conversației Actualizarea bazei de date este necesară renegocierea criptării membru - Ștergeți conversația? + Ștergi conversația? eroare Link nevalid Fișierul va fi primit după ce persoana de contact va finaliza încărcarea. @@ -923,7 +923,7 @@ nu oprit` %ds - Remediați criptarea după restaurarea copiilor de rezervă. + Repară criptarea după restaurarea backupurilor. Ștergeți sau moderați până la 200 de mesaje. Primirea mesajelor Eroare la resetarea statisticilor @@ -965,13 +965,13 @@ Pentru rutare privată Activați funcția TCP keep-alive Fără sunet când este inactiv! - Teme noi de chat + Teme noi pentru conversații Descentralizarea rețelei altul Doar 10 imagini pot fi trimise simultan Mesajul este prea mare! - Imaginea va fi primită când contactul dvs. va fi online, vă rugăm să așteptați sau să verificați mai târziu! - Șterge mesajele din conversație de pe dispozitiv. + Imaginea va fi primită atunci când persoana ta de contact este online. Te rugăm să aștepți sau să verifici mai târziu! + Șterge mesajele conversației de pe dispozitivul tău. Dacă alegeți să respingeți, expeditorul NU va fi notificat. Cum se utilizează Marcare în mesaje @@ -1005,12 +1005,12 @@ Sau distribuiți în siguranță acest link pentru fișier Statusul mesajului Niciun telefon conectat - Numai tu poți șterge ireversibil mesajele (contactul tău le poate marca pentru ștergere). (24 de ore) + Doar tu poți șterge definitiv mesajele (contactul tău le poate marca pentru ștergere). (24 de ore) Doar contactul tău poate trimite mesaje vocale. Găsește conversații mai rapid Luminos Nu - Deschizi link-ul web? + Deschizi linkul web? MESAJE ȘI FIȘIERE moderator Rol inițial @@ -1023,7 +1023,7 @@ Cod QR nevalid Cod de securitate incorect! Dispare la: %s - Conectați-vă folosind credențialele dvs. + Conectează-te cu datele tale de autentificare Deschide conversația Eroare la keychain Imediat @@ -1066,9 +1066,9 @@ Nicio conversație selectată Încărcarea fișierului Eroare la salvarea fișierului - Ștergeți fără notificare + Șterge fără notificare Deconectat - Remediați conexiunea? + Repari conexiunea? Mesaj care dispare Mai mult Pentru fiecare server vor fi utilizate acreditări noi SOCKS. @@ -1082,9 +1082,9 @@ Mod de blocare Cod de acces nou Caracteristici experimentale - Fraza de acces a bazei de date este necesară pentru a deschide chatul. - Introduceți fraza de acces… - Downgrade și deschide chatul + Este necesară parola bazei de date pentru a deschide conversația. + Introdu parola… + Retrogradează și deschide conversația Vor fi afișate mesaje de la acești membri! nici unul Eroare la eliminarea membrului @@ -1094,7 +1094,7 @@ Fă profilul privat! Deschideți contiții Nu mai afișa - Permite să ai mai multe conexiuni anonime într-un singur profil de conversație, fără a partaja date între ele. + Permite să ai mai multe conexiuni anonime fără a partaja date între ele într-un singur profil de conversație. Luminos Mesajele care dispar sunt interzise în această conversație. Numai tu poți trimite mesaje care dispar. @@ -1150,7 +1150,7 @@ Nume nou afișat: Instrumente pentru dezvoltatori FIȘIERE - Deschide linkuri din lista de chat + Deschide linkurile din lista de conversații Forma mesajului Importați baza de date Arhivă nouă a bazei de date @@ -1182,7 +1182,7 @@ Textul condițiilor actuale nu a putut fi încărcat, puteți consulta condițiile prin intermediul acestui link: Eroare la actualizarea serverului Serverul operator - Ștergeți profilul de chat pentru + Șterge profilul de conversație pentru Introdu parola în câmpul de căutare Incognito Modul incognito îți protejează confidențialitatea prin utilizarea unui profil aleatoriu nou pentru fiecare contact. @@ -1191,8 +1191,8 @@ Bună ziua! Mesaje directe Mesaje care dispar - Doar contactul dvs. poate șterge ireversibil mesajele (le puteți marca pentru ștergere). (24 de ore) - Ștergerea ireversibilă a mesajelor este interzisă în această conversație. + Doar persoana ta de contact poate șterge definitiv mesajele (tu le poți marca pentru ștergere). (24 de ore) + Ștergerea definitivă a mesajelor este interzisă în această conversație. Numai tu poți trimite mesaje vocale. Reacțiile la mesaje sunt interzise în această conversație. Numai tu poți efectua apeluri. @@ -1201,12 +1201,12 @@ %d ore Mesajele directe între membri sunt interzise. Mesajele directe între membri sunt interzise în această conversație. - Membrii pot trimite link-uri SimpleX. + Membrii pot trimite linkuri SimpleX. %d saptamana oferit %s oferit %s: %2s Șterge conversația - Ștergeți conversația cu membrul? + Ștergi conversația cu membrul? Membru se va alătura grupului, acceptați membru? Ascunde ecranul aplicației din aplicațiile recente. Nume, avatare și izolare transport diferite. @@ -1216,7 +1216,7 @@ Conectare mai rapidă și mesaje mai de încredere. Interfață în japoneză și portugheză Interfață de utilizator maghiară și turcă - Redirecționați și salvați mesajele + Redirecționează și salvează mesajele Activează Flux în setările Rețea și servere pentru o mai bună confidențialitate a metadatelor. pentru o mai bună confidențialitate a metadatelor. Redirecționați până la 20 de mesaje simultan. @@ -1239,7 +1239,7 @@ Desktopul are codul de invitație greșit Descărcarea a eșuat Importul a eșuat - Introduceți fraza de acces + Introdu parola Eroare la descărcarea arhivei Migrați către un alt dispozitiv duplicate @@ -1262,7 +1262,7 @@ Invitația la grup a expirat Eroare la crearea listei de conversație Eroare la încărcarea listelor de conversații - Eroare la actualizarea listei de chat + Eroare la actualizarea listei de conversații Notificări instant Fără parolă pentru aplicație Favorite @@ -1271,7 +1271,7 @@ niciun detaliu Deschide cu %s Link de invitație unic - Ștergeți lista? + Ștergi lista? Marchează ca verificat Activează pentru toți grup șters @@ -1290,7 +1290,7 @@ Sursa mesajelor rămâne privată. Mesaje în direct Asigurați-vă că configurația proxy este corectă. - Cel mai probabil acest contact a șters conexiunea cu dvs. + Cel mai probabil, această persoană a șters conexiunea cu tine. Avertizare de livrare a mesajelor Membrul va fi eliminat din chat - acest lucru nu poate fi anulat! Mesaje primite @@ -1306,7 +1306,7 @@ Gazdele Onion vor fi folosite atunci când sunt disponibile. Numai tu poți adăuga reacții la mesaje. Încă nu există conexiune directă, mesajul a fost redirecționat de administrator. - - Deschide chatul la primul mesaj necitit.\n- Sări la mesajele citate. + - Deschide conversația la primul mesaj necitit.\n- Mergi la mesajele citate. Oprit Deschide setările serverului criptare ok pentru %s @@ -1336,14 +1336,14 @@ Stabilește o conexiune privată Deschide SimpleX Chat pentru a accepta apelul Activați blocarea - Se poate întâmpla atunci când tu sau conexiunea ta ați folosit backupul vechi al bazei de date. + Acest lucru se poate întâmpla atunci când tu sau contactul tău ați folosit un backup vechi al bazei de date. Cod de acces Mod incognito Conexiune de rețea Statusul rețelei Asigurați-vă că fișierul are sintaxa YAML corectă. Exportați tema pentru a avea un exemplu de structură de fișiere tematice. Grupuri incognito - Noua experiență de chat 🎉 + Experiență nouă de conversație 🎉 Noi opțiuni media Primiți notificări când sunteți menționați. Migrare completă @@ -1357,7 +1357,7 @@ Confidențialitate și securitate îmbunătățite Moderarea grupului Fișierul a fost șters sau linkul este invalid - Pentru a continua, chatul trebuie oprit. + Pentru a continua, conversația trebuie oprită. Extinde Parola bazei de date este diferită de cea salvată în Keystore. observator @@ -1385,7 +1385,7 @@ Dezactivează (păstrează suprascrierile) Dacă introduceți parola de autodistrugere în timp ce deschideți aplicația: Mediu - Părăsiți conversația + Părăsește conversația Fără conversații cu membrii Confirmare de primire a mesajelor! minute @@ -1398,7 +1398,7 @@ %d fișier(e) cu dimensiunea totală de %s Fișiere și media invitat %1$s - Membrii pot șterge ireversibil mesajele trimise. (24 de ore) + Membrii pot șterge definitiv mesajele trimise. (24 de ore) Membrii pot trimite mesaje ce dispar. Mesajele directe între membri sunt interzise în acest grup. Mesajele care dispar sunt interzise. @@ -1406,7 +1406,7 @@ Desktop găsit Se descarcă detaliile linkului Eroare internă - Sau importați fișierul de arhivă + Sau importă un fișier de arhivă Sau lipește linkul arhivei Eroare la inițializarea WebView. Actualizați sistemul la noua versiune. Vă rugăm să contactați dezvoltatorii.\nEroare: %s Grupul va fi șters pentru toți membrii - această acțiune nu poate fi anulată! @@ -1418,7 +1418,7 @@ Descărcați %s (%s) Instalează actualizarea Nu crea adresă - De exemplu, dacă persoana de contact primește mesaje prin intermediul unui server SimpleX Chat, aplicația le va livra prin intermediul unui server Flux. + De exemplu, dacă un contact al tău primește mesaje printr-un server SimpleX Chat, aplicația ta le va trimite printr-un server Flux. Deschide Setările Safari / Site-uri web / Microfon, apoi selectează Permite pentru localhost. Apel ratat Renegocierea criptării a eșuat. @@ -1429,7 +1429,7 @@ Deschide linkul Baza de date va fi criptată, iar parola va fi stocată în setări. Eroare: %s - Părăsiți grupul? + Părăsești grupul? Grupul nu a fost găsit! Eroare la crearea contactului membrului Doar proprietarii conversației pot schimba preferințele. @@ -1445,7 +1445,7 @@ oprit activat pentru contact activat pentru tine - Profiluri de chat ascunse + Profiluri de conversație ascunse Acum administratorii pot:\n- șterge mesajele membrilor.\n- dezactiva membrii (rol de observator) Reacții la mesaje Rapid și fără așteptare până când expeditorul este online! @@ -1503,25 +1503,25 @@ Oprit Deschideți locația fișierului Ieșire fără salvare - Parola profilului ascunsă + Parolă profil ascuns italic Cum funcționează SimpleX Fără identificatori de utilizator. - Se poate întâmpla când:\n1. Mesajele au expirat în clientul care trimite după 2 zile sau pe server după 30 de zile.\n2. Decriptarea mesajelor a eșuat, deoarece tu sau contactul dvs. ați folosit o copie de rezervă a bazei de date vechi.\n3. Conexiunea a fost compromisă. - Eroare la exportul bazei de date a chatului + Acest lucru se poate întâmpla atunci când:\n1. Mesajele au expirat în aplicația de trimitere după 2 zile sau pe server după 30 de zile.\n2. Decriptarea mesajului a eșuat, deoarece tu sau contactul tău ați folosit un backup vechi al bazei de date.\n3. Conexiunea a fost compromisă. + Eroare la exportarea bazei de date a conversației Eroare la oprirea conversației niciodată Niciun fișier primit sau trimis - Frază de acces nouă… + Parolă nouă… Criptezi baza de date? - Introduceți fraza de acces corectă. + Introdu parola corectă. Vrei să te alături grupului? Migrații: %s Alătură-te - Părăsiți - Părăsiți conversația? + Părăsește + Părăsești conversația? a părăsit - invitat prin linkul dvs. de grup + invitat prin linkul tău de grup Un membru nou vrea să se alăture grupului. criptare agreată renegocierea criptării este permisă @@ -1530,7 +1530,7 @@ Moderat la: %s Numele complet al grupului: Preferințele grupului - Ștergerea ireversibilă a mesajelor este interzisă. + Ștergerea definitivă a mesajelor este interzisă. Membrii pot adăuga reacții la mesaje. Membrii pot trimite fișiere și media. Reacțiile la mesaje sunt interzise. @@ -1545,7 +1545,7 @@ Admiterea membrilor oprit Linkuri de grup - Ștergere ireversibilă a mesajului + Ștergere definitivă a mesajelor Măriți dimensiunea fontului. Deconectează Deconectat din motivul: %s @@ -1570,7 +1570,7 @@ Introduceți numele grupului: Umple Descărcați noi versiuni de pe GitHub. - Dacă ați primit un link de invitație SimpleX Chat, îl puteți deschide în browser-ul dvs.: + Dacă ai primit un link de invitație SimpleX Chat, îl poți deschide în browser-ul tău: Link nevalid! Nu Invitați membrii @@ -1604,7 +1604,7 @@ Confidențialitatea redefinită Vă rugăm să verificați conexiunea la rețea cu %1$s și să încercați din nou. Rutare mesaje private 🚀 - Bara de instrumente de chat accesibilă + Bara de instrumente pentru conversație, accesibilă Redă din lista de conversații. Vă rugăm să încercați mai târziu. Citeşte mai mult @@ -1626,31 +1626,31 @@ portul %d Periodic Servere presetate - Vă rugăm să o rețineți sau să o păstrați în siguranță - nu există nicio modalitate de a recupera o parolă pierdută! + Reține parola sau păstreaz-o în siguranță – nu există nicio modalitate de a o recupera dacă o pierzi! Confidențialitate și securitate Protejează ecranul aplicației Imagini de profil - Vă rugăm să stocați în siguranță fraza de acces, NU veți putea accesa chatul dacă o pierdeți. + Te rugăm să stochezi parola în siguranță, altfel NU vei putea accesa conversația dacă o pierzi. Interziceți trimiterea de mesaje vocale. Apeluri cu funcție picture-in-picture Interfață persană - Proxiat + Prin proxy Conexiuni de profil și server răspuns primit… Politica de confidențialitate și condițiile de utilizare. RUTAREA MESAJELOR PRIVATE - Vă rugăm să păstrați parola în siguranță, NU o veți putea schimba dacă o pierdeți. - Fraza de acces nu a fost găsită în Keystore, vă rugăm să o introduceți manual. Acest lucru se poate fi întâmplat dacă ați restaurat datele aplicației utilizând un instrument de backup. Dacă nu este cazul, vă rugăm să contactați dezvoltatorii. - Parola din Keystore nu poate fi citită. Acest lucru s-ar fi putut întâmpla după o actualizare de sistem incompatibilă cu aplicația. Dacă nu este cazul, vă rugăm să contactați dezvoltatorii. - Fost membru %1$s + Te rugăm să stochezi parola în siguranță, altfel NU o vei putea schimba dacă o pierzi. + Parola nu a fost găsită în Keystore. Te rugăm să o introduci manual. Acest lucru s-ar putea întâmpla dacă ai restaurat datele aplicației folosind un instrument de backup. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii. + Parola stocată în Keystore nu poate fi citită. Acest lucru se poate întâmpla după o actualizare a sistemului incompatibilă cu aplicația. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii. + Membru %1$s în așteptare recenzie în așteptare Primit la: %s Expirare protocol Număr de PING-uri - Parolă de profil + Parola profilului Interziceți reacțiile la mesaje. - Interzice ștergerea ireversibilă a mesajelor. + Interzice ștergerea definitivă a mesajelor. Interziceți trimiterea de mesaje directe către membri. Interziceți trimiterea de mesaje care dispar. Interziceți raportarea mesajelor către moderatori. @@ -1658,12 +1658,12 @@ Păstrează ultima schiță a mesajului, cu atașamente. Nume de fișiere private Criptare rezistentă cuantic - Confidențialitate pentru clienții dvs. + Confidențialitate pentru clienții tăi. Nume de fișiere media private. Aleatoriu Vă rugăm să raportați dezvoltatorilor:\n%s\n\nSe recomandă repornirea aplicației. Vă rugăm să confirmați că setările de rețea sunt corecte pentru acest dispozitiv. - Fraza de acces este necesară + Este necesară o parolă Parolă Servere presetate Lipiți adresa desktopului @@ -1680,13 +1680,13 @@ Protejați adresa IP Mesaj primit Note private - Bara de instrumente de chat accesibilă + Bara de instrumente a conversației Temă de profil Lipește linkul pe care l-ai primit Parola nu a fost modificată! Parolă setată! - Fraza de acces din Keystore nu poate fi citită, vă rugăm să o introduceți manual. Acest lucru se poate fi întâmplat după o actualizare de sistem incompatibilă cu aplicația. Dacă nu este cazul, vă rugăm să contactați dezvoltatorii. - Bare de instrumente accesibile + Parola stocată în Keystore nu poate fi citită. Te rugăm să o introduci manual. Acest lucru se poate întâmpla după o actualizare a sistemului incompatibilă cu aplicația. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii. + Bări de instrumente ale aplicației, accesibile Vă rugăm să așteptați ca moderatorii grupului să vă examineze cererea de alăturare. Interziceți trimiterea de mesaje care dispar. Interziceți trimiterea de fișiere și fișiere media. @@ -1694,7 +1694,7 @@ Servere proxy Apel în așteptare Vă rugăm să raportați dezvoltatorilor. - Vă rugăm să introduceți parola anterioară după restaurarea copiei de rezervă a bazei de date. Această acțiune nu poate fi anulată. + Te rugăm să introduci parola anterioară după restaurarea backupului bazei de date. Această acțiune nu poate fi anulată. criptare e2e rezistentă cuantic Chitanțele sunt dezactivate Primit la @@ -1709,15 +1709,15 @@ Server presetat Evaluează aplicația Port - Conversațiile private, grupurile și contactele dvs. nu sunt accesibile operatorilor serverului. - Parola aleatorie este stocată în setări ca text simplu.\nO puteți modifica ulterior. - Vă rugăm să introduceți fraza de acces curentă corectă. + Conversațiile private, grupurile și contactele tale nu sunt accesibile operatorilor serverului. + O parolă aleatorie este stocată în setări, ca text simplu.\nO poți schimba ulterior. + Te rugăm să introduci parola actuală\ncorectă. Previzualizare - Verificați dacă dispozitivul mobil și desktopul sunt conectate la aceeași rețea locală și dacă firewall-ul desktopului permite conexiunea.\nVă rugăm să comunicați orice alte probleme dezvoltatorilor. + Te rugăm să verifici dacă dispozitivul mobil și cel desktop sunt conectate la aceeași rețea locală și dacă firewall-ul de pe desktop permite conexiunea.\nTe rugăm să raportezi orice alte probleme dezvoltatorilor. Servere conectate anterior În așteptare confirmare primită… - Protejează-ți profilurile de chat cu o parolă! + Protejează-ți profilurile de conversații cu o parolă! Te rog să verifici dacă ai folosit linkul corect sau cere contactului tău să îți trimită unul nou. Eroare de rutare privată respins @@ -1729,7 +1729,7 @@ Server adăugat la operatorul %s. Setați mesajul afișat noilor membri! Setați-l în locul autentificării sistemului. - Setați parola pentru export + Setează o parolă pentru export Raportați conținutul: doar moderatorii grupului îl vor vedea. Adresa serverului Primirea fișierului va fi oprită. @@ -1742,11 +1742,11 @@ Salvează și reconectează-te Raportați altceva: doar moderatorii grupului îl vor vedea. caută - Setați numele chatului… - Serverul de retransmisie este utilizat doar dacă este necesar. O altă parte poate observa adresa dvs. IP. - Resetați toate statisticile? + Setează numele conversației… + Serverul de retransmisie este utilizat doar dacă este necesar. O altă parte poate observa adresa ta IP. + Resetezi toate statisticile? Resetați toate sugestiile - Trimiteți mesaje direct atunci când adresa IP este protejată și serverul dvs. sau serverul de destinație nu acceptă rutare privată. + Trimite mesaje direct atunci când adresa IP este protejată, iar serverul tău sau cel de destinație nu acceptă rutare privată. Parola de autodistrugere activată! Setați parola Examinați condițiile @@ -1782,7 +1782,7 @@ Istoric recent și bot de directoare îmbunătățit. Setează expirarea mesajelor în conversații. Trimiterea confirmărilor de livrare va fi activată pentru toate contactele. - Trimiterea confirmărilor de livrare va fi activată pentru toate contactele din toate profilurile de chat vizibile. + Trimiterea confirmărilor de livrare va fi activată pentru toate contactele din toate profilurile de conversații vizibile. Securizat Selectat %d Trimiterea confirmărilor este activată pentru %d contacte @@ -1796,12 +1796,12 @@ Trimiterea confirmărilor este activată pentru %d grupuri Parolă de autodistrugere Trimiteți rapoarte private - Trimiteți mesaje direct atunci când serverul dvs. sau serverul de destinație nu acceptă rutare privată. + Trimite mesaje direct atunci când serverul tău sau cel de destinație nu acceptă rutarea privată. eliminat din grup cererea de înscriere a fost respinsă Selectează profilul de conversație Salvează setările adresei SimpleX - Salvați lista + Salvează lista TRIMITE CONFIRMĂRI DE LIVRARE LA Parola de autodistrugere a fost schimbată! Înregistrare actualizată la @@ -1823,7 +1823,7 @@ Înregistrați mesajul vocal Scanează / Lipește link Server - Reporniți aplicația pentru a utiliza baza de date de chat importată. + Repornește aplicația pentru a folosi baza de date de conversații importată. Eliminați membri? Primirea prin intermediul Raportarea mesajelor este interzisă în acest grup. @@ -1836,17 +1836,17 @@ Adresa serverului este incompatibilă cu setările de rețea: %1$s. Coadă securizată Este posibil ca expeditorul să fi șters cererea de conectare. - Versiunea serverului este incompatibilă cu aplicația dvs.: %1$s. + Versiunea serverului este incompatibilă cu aplicația ta: %1$s. Deblocare Încărcarea a eșuat Aplicația se poate închide după 1 minut în fundal. Cu sunet - Această setare se aplică mesajelor din profilul tău de chat actual + Această setare se aplică mesajelor din profilul de conversație actual Folosiți servere SimpleX Chat? Proxy SOCKS Nume de utilizator SimpleX Chat și Flux au ajuns la un acord pentru a include în aplicație servere operate de Flux. - Aplicația preia mesaje noi periodic — folosește câteva procente din baterie pe zi. Aplicația nu utilizează notificări push — datele de pe dispozitivul dvs. nu sunt trimise către servere. + Aplicația preia mesajele noi periodic și consumă câteva procente din baterie pe zi. Aplicația nu folosește notificări push, așadar datele de pe dispozitivul tău nu sunt trimise către servere. Blocare SimpleX Cheie greșită sau adresă necunoscută a fragmentului de fișier - cel mai probabil fișierul este șters. Prea multe videoclipuri! @@ -1857,7 +1857,7 @@ Fișiere încărcate Folosește profilul curent Folosește noul profil incognito - Vizualizarea s-a blocat + Vizualizarea a eșuat Funcție lentă Blocare SimpleX Deblocați pentru toți @@ -1869,7 +1869,7 @@ Mulțumim utilizatorilor – contribuiți prin Weblate! Cu un consum redus de energie al bateriei. Cu un consum redus de energie al bateriei. - Începând de la %s.\nToate datele sunt păstrate private pe dispozitivul dvs. + Începând cu %s.\nToate datele sunt păstrate confidențiale pe dispozitivul tău. Abonat Erori de încărcare Apel video @@ -1893,7 +1893,7 @@ Mesaj vocal (%1$s) Trimite Comută lista de conversații: - Pentru a primi notificări, vă rugăm să introduceți fraza de acces a bazei de date + Pentru a primi notificări, te rugăm să introduci parola bazei de date Se așteaptă videoclipul Mesaje nelivrate Testul a eșuat la pasul %s. @@ -1903,7 +1903,7 @@ Atinge pentru a începe o conversație nouă Folosește acreditări proxy diferite pentru fiecare conexiune. Adresă SimpleX sau link unic? - Pentru a vă proteja confidențialitatea, SimpleX folosește ID-uri separate pentru fiecare dintre contactele dvs. + Pentru a-ți proteja confidențialitatea, SimpleX folosește ID-uri separate pentru fiecare persoană de contact. Ai fost invitat în grup Pentru a primi Utilizare pentru fișiere @@ -1911,7 +1911,7 @@ Temă Utilizați aplicația cu o singură mână. Videoclipul va fi primit după ce persoana de contact termină de încărcat. - Videoclipul va fi recepționat când contactul dvs. va fi online, vă rugăm să așteptați sau să verificați mai târziu! + Videoclipul va fi primit atunci când persoana ta de contact este online. Te rugăm să aștepți sau să verifici mai târziu! video Utilizarea serverelor SimpleX Chat. Folosiți proxy SOCKS? @@ -1923,7 +1923,7 @@ Începând de la %s. Se așteaptă conectarea mobilului: Server XFTP - Această acțiune nu poate fi anulată - profilul, contactele, mesajele și fișierele dvs. vor fi pierdute iremediabil. + Această acțiune nu poate fi anulată - profilul, contactele, mesajele și fișierele tale vor fi pierdute definitiv. Sistem Deblocați membrul? Cu sunet @@ -1947,13 +1947,13 @@ Ați acceptat conexiunea Partajează adresa public Partajează acest link de invitație unic - Pentru a verifica criptarea end-to-end cu contactul dvs., comparați (sau scanați) codul de pe dispozitivele dvs. + Pentru a verifica criptarea end-to-end, compară (sau scanează) codul de pe dispozitivele voastre cu persoana de contact. Utilizare pentru conexiuni noi Utilizați proxy SOCKS Oprești partajarea adresei? Când aplicația rulează Aplicația vă protejează confidențialitatea utilizând operatori diferiți în fiecare conversație. - Vizualizați condițiile + Vizualizează condițiile Afișează doar contactul Deblocare Porniți @@ -1963,8 +1963,8 @@ Stop Opriți primirea fișierului? Se așteaptă fișierul - Această acțiune nu poate fi anulată - mesajele trimise și primite în acest chat anterior celei selectate vor fi șterse. - Vizualizați codul de securitate + Această acțiune nu poate fi anulată - mesajele trimise și primite în această conversație, mai vechi decât data selectată, vor fi șterse. + Vizualizează codul de securitate Atinge pentru a lipi linkul Textul pe care l-ai lipit nu este un link SimpleX. Stabil @@ -1989,13 +1989,13 @@ Videoclipuri și fișiere de până la 1 GB Comută modul incognito la conectare. Formă imagine de profil - Comutați profilul de chat pentru invitațiile unice. + Schimbă profilul de conversații pentru invitații unice. Puteți activa mai târziu prin Setări săptămâni Acesta este linkul tău unic, valabil o singură dată! Servere necunoscute! Fără Tor sau VPN, adresa ta IP va fi vizibilă pentru aceste retransmiteri XFTP:\n%1$s. - Actualizați parola bazei de date + Actualizează parola bazei de date Opriți trimiterea fișierului? tăiere Viitorul mesageriei @@ -2006,7 +2006,7 @@ Da Coada Utilizare de pe desktop - Opriți chatul pentru a exporta, importa sau șterge baza de date a chatului. Nu veți putea primi și trimite mesaje în timp ce chatul este oprit. + Oprește conversațiile pentru a exporta, importa sau șterge baza de date a conversațiilor. Nu vei putea primi și trimite mesaje cât timp conversațiile sunt oprite. Unele fișiere nu au fost exportate Parola este stocată în setări ca text simplu. deblocat %s @@ -2070,20 +2070,20 @@ WiFi Oprire Mesaje ignorate - Parola va fi stocată în setări ca text simplu după ce o modificați sau reporniți aplicația. + Parola va fi stocată în setări ca text simplu după ce o schimbi sau repornești aplicația. Conexiune TCP Erori de abonare Atingeți pentru a vă conecta incognito Imaginea nu poate fi decodificată. Vă rugăm să încercați o altă imagine sau să contactați dezvoltatorii. - Serverele pentru fișierele noi din profilul tău de chat actual + Serverele pentru fișierele noi ale profilului tău de conversații actual Acest grup nu mai există. Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact (eroare: %1$s). Actualizarea setărilor va reconecta clientul la toate serverele. Acest șir de caractere nu este un link! S-a atins timpul de expirare la conectarea la desktop Pentru a te proteja împotriva înlocuirii linkului tău, poți compara codurile de securitate ale contactelor. - Pentru a vă proteja informațiile, activați SimpleX Lock.\nVi se va solicita să finalizați autentificarea înainte ca această funcție să fie activată. - Dacă contactul dvs. a șters conexiunea sau acest link a fost deja utilizat, ar putea fi o eroare – vă rugăm să o raportați.\nPentru a vă conecta, vă rugăm să rugați contactul dvs. să creeze un alt link de conexiune și să verificați dacă aveți o conexiune la rețea stabilă. + Pentru a-ți proteja informațiile, activează Blocare SimpleX.\nVa trebui să finalizezi autentificarea înainte de a activa această funcție. + Dacă persoana ta de contact nu a șters conexiunea sau dacă acest link a mai fost folosit, este posibil să fie o eroare. Te rugăm să o raportezi.\nPentru a te conecta, roagă persoana ta de contact să creeze un nou link și asigură-te că ai o conexiune stabilă la internet. Când partajezi un profil incognito cu cineva, acest profil va fi folosit pentru grupurile la care te invită. Mesaj vocal La conectarea apelurilor audio și video. @@ -2108,14 +2108,14 @@ Folosește rutare privată cu servere necunoscute. Folosește rutare privată cu servere necunoscute atunci când adresa IP nu este protejată. Folosiți portul TCP %1$s atunci când nu este specificat niciun port. - Nu stocăm niciunul dintre contactele sau mesajele dvs. (odată livrate) pe servere. + Nu stocăm niciunul dintre contactele sau mesajele tale (odată livrate) pe servere. prin releu Soft Puternic A doua ticăitură pe care am ratat-o! ✅ Mulțumim utilizatorilor – contribuiți prin Weblate! Afișează procentul - Serverele pentru conexiuni noi ale profilului dvs. de chat actual + Serverele pentru noile conexiuni ale profilului tău de conversație actual Afișează apeluri API lente Pentru a fi notificat despre noile versiuni, activați verificarea periodică pentru versiunile Stabile sau Beta. Folosește o parolă aleatorie @@ -2158,7 +2158,7 @@ Blocarea SimpleX nu este activă! Autentificare sistem Baza de date nu funcționează corect. Atingeți pentru a afla mai multe. - Raportul va fi arhivat pentru dvs. + Raportul va fi arhivat. Atingeți Creează o adresă SimpleX în meniu pentru a o crea mai târziu. Acest text este disponibil în setări Ești invitat în grup @@ -2166,25 +2166,25 @@ Se așteaptă imaginea Se așteaptă videoclipul Eroare temporară de fișier - Eliminare favorit + Elimină din favorite Mențiuni necitite Conexiunea pe care ați acceptat-o va fi anulată! Contactul cu care ați partajat acest link NU se va putea conecta! vrea să se conecteze cu tine! Partajează adresa SimpleX pe rețelele de socializare. - Pentru a se conecta, contactul dvs. poate scana codul QR sau poate folosi linkul din aplicație. - Puteți seta numele conexiunii, pentru a vă aminti cu cine a fost partajat linkul. + Pentru a te conecta, persoana ta de contact poate scana codul QR sau poate folosi linkul din aplicație. + Poți seta numele conexiunii, ca să-ți amintești cu cine ai partajat linkul. Adresa SimpleX și linkurile unice pot fi partajate în siguranță prin orice aplicație de mesagerie. Codul scanat nu este un cod QR de tip link SimpleX. Server de testare Servere de testare - Folosește acreditări aleatorii + Folosește credențiale aleatorii Actualizare disponibilă: %s Folosește chatul Hash-ul mesajului anterior este diferit. Această acțiune nu poate fi anulată - mesajele trimise și primite anterior celei selectate vor fi șterse. Poate dura câteva minute. - Frază de acces greșită! - Încercarea de a schimba fraza de acces a bazei de date nu a fost finalizată. + Parolă greșită! + Încercarea de a schimba parola bazei de date nu a fost finalizată. Ai fost invitat(ă) în grup. Alătură-te pentru a te conecta cu membrii grupului. profil de grup actualizat Condiții actualizate @@ -2194,8 +2194,8 @@ Acest link a fost utilizat cu un alt dispozitiv mobil, vă rugăm să creați un link nou pe desktop. Verifică conexiunile Poți să încerci încă o dată. - Nu ești conectat la aceste servere. Rutarea privată este utilizată pentru a livra mesaje către ele. - Se oprește chatul + Nu ești conectat la aceste servere. Pentru livrarea mesajelor către ele se folosește rutarea privată. + Se opresc conversațiile Total ești observator Videoclipul nu poate fi decodificat. Vă rugăm să încercați un alt videoclip sau să contactați dezvoltatorii. @@ -2218,9 +2218,9 @@ %s.]]> %s]]> Arhivare raport - Chatul va fi șters pentru tine - această acțiune nu poate fi anulată! + Conversația va fi ștearsă doar pentru tine - această acțiune nu poate fi anulată! Utilizarea bateriei aplicației / Nerestricționat în setările aplicației.]]> - Ghidul utilizatorului.]]> + Ghidul utilizatorului.]]> V-ați alăturat acestui grup. Se conectează la membrul invitat al grupului. Profilul tău va fi trimis persoanei de contact de la care ai primit acest link. Te vei conecta cu toți membrii grupului. @@ -2241,11 +2241,11 @@ Puteți vizualiza în continuare conversația cu %1$s în lista de conversații. Condiții acceptate %s.]]> - %1$d eroare(erori) de fișier:\n%2$s + %1$d erori de fișier:\n%2$s Conexiune blocată Conexiunea este blocată de operatorul serverului:\n%1$s. criptate end-to-end, cu securitate post-cuantică în mesajele directe.]]> - Bare de instrumente + Bările de instrumente ale aplicației Securitate îmbunătățită ✅ Toate conversațiile vor fi eliminate din lista %s, iar lista va fi ștearsă Conexiunea ta a fost mutată la %s, dar a apărut o eroare la schimbarea profilului. @@ -2266,7 +2266,7 @@ Verifică mesajele la fiecare 10 minute %s, acceptați condițiile de utilizare.]]> Confirmă-ți datele de autentificare - Schimbă profilurile de chat + Schimbă profilurile de conversație Contacte Conexiunea necesită renegocierea criptării. Adresa ta SimpleX @@ -2279,14 +2279,14 @@ Servere media și de fișiere adăugate Estompare - conectare la serviciul de directoare (BETA)!\n- confirmări de livrare (până la 20 de membri).\n- mai rapid și mai stabil. - Arhivează contactele pentru a discuta mai târziu. + Arhivează contactele pentru a conversa mai târziu. %1$s.]]> Ai partajat o cale de fișier nevalidă. Raportează problema dezvoltatorilor aplicației. Deschide în aplicația mobilă, apoi atinge Conectare în aplicație.]]> Actualizează adresa Configurați operatorii serverului Condițiile vor fi acceptate pentru operatorii activați după 30 de zile. - Faceți clic pe butonul de informații de lângă câmpul de adresă pentru a permite utilizarea microfonului. + Apasă pe butonul de informații de lângă bara de adrese pentru a permite accesul la microfon. Colţ Puteți salva arhiva exportată. Nu veți mai primi mesaje de la acest chat. Istoricul conversației va fi păstrat. @@ -2294,7 +2294,7 @@ Încercați să invitați persoana de contact cu care ați partajat un profil incognito în grupul în care utilizați profilul principal Poți partaja un link sau un cod QR - oricine se va putea alătura grupului. Nu vei pierde membrii grupului dacă îl ștergi ulterior. Blocați membrii pentru toți? - Chat + Conversație %s.]]> Condiții acceptate pe: %s. Condițiile vor fi acceptate automat pentru operatorii activați pe: %s. @@ -2304,7 +2304,7 @@ Condiții de utilizare Permite raportarea mesajelor către moderatori. Conversează cu membrii - Discută cu administratorii + Convorbire cu administratori Acceptă Acceptă membru Date de mesaj îmbunătățite. @@ -2312,14 +2312,14 @@ Confidențialitate și securitate îmbunătățite %s este inactiv]]> %s lipsește]]> - %1$s!]]> + %1$s!]]> Confirmat Erori de confirmare Vi se va solicita să vă autentificați când porniți sau reluați aplicația după 30 de secunde în fundal. - conecta la dezvoltatorii SimpleX Chat pentru a adresa orice întrebări și pentru a primi actualizări.]]> + să te conectezi la dezvoltatorii SimpleX Chat pentru a le adresa orice întrebări și pentru a primi actualizări.]]> Adaugă prieteni Adaugă membri echipei - Chatul există deja! + Conversația există deja! Deschide în aplicația mobilă.]]> Continuă %s a fost deconectat]]> @@ -2336,7 +2336,7 @@ Adaugă la listă Modificați lista invitație acceptată - Puteți să vă împărtășiți adresa ca link sau cod QR - oricine se poate conecta la dvs. + Îți poți partaja adresa sub formă de link sau cod QR - oricine se poate conecta cu tine. Acceptă Confidențialitatea ta Acceptă condițiile @@ -2350,9 +2350,9 @@ Dispozitive Xiaomi: vă rugăm să activați Pornirea automată în setările de sistem pentru ca notificările să funcționeze.]]> Arhivă Pictogramă de context - Puteți vizualiza rapoartele dvs. în Chat cu administratorii. - Schimbați ordinea - Vei fi conectat(ă) atunci când dispozitivul contactului tău este online, te rugăm să aștepți sau să verifici mai târziu! + Poți vizualiza rapoartele tale în conversația cu administratorii. + Schimbă ordinea + Te vei conecta atunci când dispozitivul contactului tău va fi online, te rugăm să aștepți sau să verifici mai târziu! scana codul QR în timpul apelului video sau contactul tău poate partaja un link de invitație.]]> Profilul tău %1$s va fi distribuit. Nu îți vei pierde contactele dacă ulterior îți ștergi adresa. @@ -2361,7 +2361,7 @@ Toate serverele Profilul tău este stocat pe dispozitiv și este partajat doar cu contactele tale. Serverele SimpleX nu pot vedea profilul tău. Baza ta de date actuală de chat va fi ȘTERSĂ și ÎNLOCUITĂ cu cea importată.\nAceastă acțiune nu poate fi anulată - profilul, contactele, mesajele și fișierele tale vor fi pierdute iremediabil. - Discută cu administratorii + Convorbire cu administratori Poți partaja această adresă cu contactele tale pentru a le permite să se conecteze cu %s. Conversează cu membrul Cererea de conectare va fi trimisă acestui membru al grupului. @@ -2371,7 +2371,7 @@ (nou)]]> Serverul tău Trebuie să îi permiți contactului tău să te sune pentru a-l putea suna. - Adresa serverului dvs. + Adresa serverului tău Serverele tale Vei fi conectat la grup atunci când dispozitivul gazdei grupului va fi online, te rugăm să aștepți sau să verifici mai târziu! Tu decizi cine se poate conecta. @@ -2387,7 +2387,7 @@ Acceptă ca membru toți Afaceri - Prin utilizarea SimpleX Chat sunteți de acord să:\n- trimiteți doar conținut legal în grupurile publice.\n- respectați ceilalți utilizatori – fără spam. + Prin utilizarea SimpleX Chat ești de acord să:\n- trimiți doar conținut legal în grupurile publice.\n- respecți ceilalți utilizatori – fără spam. doar cu un singur contact - partajează-l personal sau prin orice altă aplicație de mesagerie.]]> Nu trebuie să utilizați aceeași bază de date pe două dispozitive. arătați codul QR în apelul video sau distribuiți linkul.]]> @@ -2395,7 +2395,7 @@ SimpleX rulează în fundal în loc să utilizeze notificări push.]]> Utilizarea bateriei aplicației / Nerestricționat în setările aplicației.]]> Bucăți descărcate - Chatul va fi șters pentru toți membrii - această acțiune nu poate fi anulată! + Conversația va fi ștearsă pentru toți membrii - această acțiune nu poate fi anulată! Conexiunea nu este gata. Conținutul încalcă condițiile de utilizare Folosești un profil incognito pentru acest grup - pentru a preveni partajarea profilului principal, invitarea contactelor nu este permisă. @@ -2410,7 +2410,7 @@ Biografie prea mare Nu se poate modifica profilul Conversează cu administratorii - Conversează cu membrii înainte să se alăture + Conversează cu membrii înainte ca aceștia să se alăture. Conectează-te Conectează-te mai rapid! 🚀 contactul ar trebui să accepte… @@ -2422,7 +2422,7 @@ 4 noi limbi de interfață Catalană, Indoneziană, Română și Vietnameză - mulțumită utilizatorilor noștri! criptare end-to-end.]]> - Deschide conversație nouă + Deschide o conversație nouă Creează un grup nou Deschide pentru a accepta Deschide pentru conectare @@ -2481,7 +2481,7 @@ Grupul tău Doar tu poți trimite fișiere și conținut media. Doar contactele tale pot trimite fișiere și conținut media. - Deschide link securizat + Deschide linkul curat Respinge cererea de contact Timp expirare conexiune TCP în fundal Linkul va fi scurt, iar profilul grupului va fi partajat prin acesta. @@ -2495,4 +2495,7 @@ Atinge Alătură-te grupului Actualizează linkul grupului Profilul tău + Link de releu SimpleX + Grup + Eroare la marcarea conversației cu membrul ca fiind citită diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 329e75c681..6bb357fea8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -76,8 +76,8 @@ Ошибка удаления ожидаемого соединения Ошибка при изменении адреса Ошибка теста на шаге %s. - Сервер требует авторизации для создания очередей, проверьте пароль - Возможно, хэш сертификата в адресе сервера неверный + Сервер требует авторизации для создания очередей, проверьте пароль. + Хэш в адресе сервера не соответствует сертификату. Соединение Создание очереди Защита очереди @@ -2479,7 +2479,7 @@ ожидает одобрения Отклонить Отклонить члена группы? - Ошибка при удалении чата с членом группы + Ошибка при удалении чата Полная ссылка Ошибка при вступлении члена группы Ссылка не поддерживается @@ -2610,4 +2610,9 @@ Открыть очищенную ссылку Открыть полную ссылку Удалять параметры отслеживания + Ошибка прочтения чата + Хэш в адресе пересылающего сервера не соответствует сертификату: %1$s. + Хэш в адресе сервера не соответствует сертификату: %1$s. + Ссылка SimpleX relay + Хэш в адресе сервера назначения не соответствует сертификату: %1$s. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 43e67bb51b..ec8af9053e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -2503,4 +2503,10 @@ Açıklama çok büyük Kaybolma süresi yalnızca yeni kişiler için ayarlanır. Gizli profil kullan + Botu kullanmak için açın + Botu kullanmak için Bağlan tuşuna basın. + Bot + Komutlar gönderebilmek için bağlanmanış olmanız gereklidir. + Üye silinmiş - isteği kabul edemeyecek + Grup linkini güncelle diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index a72d95c3b1..d6686f358d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -2519,4 +2519,5 @@ 打开完整链接 删除链接跟踪 SimpleX 中继链接 + 将和成员的聊天标记为已读时出错 From 2f20199012660336d640345f0af7de4314765343 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 8 Sep 2025 16:04:07 +0100 Subject: [PATCH 035/137] website: translations Currently translated at 95.7% (246 of 257 strings) Translation: SimpleX Chat/SimpleX Chat website Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/pl/ Co-authored-by: Jester Hodl --- website/langs/pl.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/website/langs/pl.json b/website/langs/pl.json index d45207a709..e3c8fa6059 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -46,7 +46,7 @@ "developers": "Deweloperzy", "simplex-explained-tab-1-text": "1. Jakie są doświadczenia użytkowników", "simplex-explained-tab-1-p-1": "Możesz tworzyć kontakty i grupy oraz prowadzić dwukierunkowe rozmowy, jak w każdym innym komunikatorze.", - "simplex-explained-tab-2-p-2": "Serwery przekazują wiadomości tylko w jedną stronę, nie mając pełnego obrazu konwersacji i połączeń użytkownika.", + "simplex-explained-tab-2-p-2": "Serwery przekazują wiadomości tylko w jedną stronę, nie mając pełnego obrazu konwersacji ani połączeń użytkownika.", "simplex-explained-tab-3-p-2": "Użytkownicy mogą jeszcze bardziej zwiększyć prywatność metadanych, używając Tor do uzyskania dostępu do serwerów, co zapobiega korelacji na podstawie adresu IP.", "hero-p-1": "Inne aplikacje posiadają identyfikatory użytkowników (ID): Signal, Matrix, Session, Briar, Jami, Cwtch itp.
SimpleX nie, nie posiada nawet losowych numerów.
To radykalnie poprawia Twoją prywatność.", "feature-2-title": "Obrazy, wideo i pliki
zaszyfrowane przez E2E", @@ -63,7 +63,7 @@ "simplex-private-card-3-point-2": "Odcisk palca serwera i wiązanie kanałów zapobiegają atakom MITM i replay.", "simplex-private-card-3-point-3": "Wznowienie połączenia jest wyłączone, aby zapobiec atakom na sesje.", "simplex-private-card-4-point-1": "Aby chronić swój adres IP, możesz uzyskać dostęp do serwerów przez Tor lub inną sieć nakładki transportowej.", - "simplex-private-card-6-point-1": "Wiele platform komunikacyjnych jest podatnych na ataki MITM ze strony serwerów lub dostawców sieci.", + "simplex-private-card-6-point-1": "Wiele sieci komunikacyjnych jest podatnych na ataki MITM ze strony serwerów lub dostawców sieci.", "simplex-private-card-6-point-2": "Aby temu zapobiec aplikacje SimpleX przekazują klucze jednorazowe out-of-band, gdy udostępniasz adres jako link lub kod QR.", "simplex-private-card-7-point-1": "Aby zagwarantować integralność, wiadomości są kolejno numerowane i zawierają hash poprzedniej wiadomości.", "simplex-private-card-7-point-2": "Jeśli jakaś wiadomość zostanie dodana, usunięta lub zmieniona, odbiorca zostanie o tym powiadomiony.", @@ -112,7 +112,7 @@ "simplex-unique-4-title": "Jesteś właścicielem sieci SimpleX", "simplex-unique-4-overlay-1-title": "W pełni zdecentralizowana — użytkownicy są właścicielami sieci SimpleX", "hero-overlay-card-1-p-1": "Wielu użytkowników pytało: jeśli SimpleX nie ma identyfikatorów użytkowników, skąd może wiedzieć, gdzie dostarczyć wiadomości?", - "hero-overlay-card-1-p-2": "Aby dostarczyć wiadomości, zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, SimpleX używa tymczasowych anonimowych identyfikatorów kolejek wiadomości, oddzielnych dla każdego z twoich połączeń — nie ma żadnych długoterminowych identyfikatorów.", + "hero-overlay-card-1-p-2": "Aby dostarczyć wiadomości, zamiast identyfikatorów użytkownika używanych przez wszystkie inne sieci, SimpleX używa tymczasowych anonimowych identyfikatorów kolejek wiadomości, oddzielnych dla każdego z twoich połączeń — nie ma żadnych długoterminowych identyfikatorów.", "simplex-network-overlay-card-1-li-1": "Sieci P2P opierają się na pewnym wariancie DHT do przekazywania wiadomości. DHT muszą równoważyć gwarancję dostarczenia i opóźnienie. SimpleX ma zarówno lepszą gwarancję dostarczenia, jak i mniejsze opóźnienia niż P2P, ponieważ wiadomość może być nadmiarowo przepuszczona przez kilka serwerów równolegle, z wykorzystaniem serwerów wybranych przez odbiorcę. W sieciach P2P wiadomość jest przepuszczana przez , O(log N) węzłów sekwencyjnie, z wykorzystaniem węzłów wybranych przez algorytm.", "privacy-matters-overlay-card-3-p-3": "Zwykli ludzie są aresztowani za to, co udostępniają w sieci, nawet za pośrednictwem swoich \"anonimowych\" kont, nawet w krajach demokratycznych.", "hero-overlay-card-1-p-6": "Więcej w SimpleX whitepaper.", @@ -121,18 +121,18 @@ "hero-overlay-card-2-p-4": "SimpleX chroni przed tymi atakami, nie mając w swojej konstrukcji żadnych identyfikatorów użytkowników. A jeśli używasz trybu Incognito, będziesz miał inną nazwę wyświetlania dla każdego kontaktu, unikając wszelkich wspólnych danych między nimi.", "simplex-network-overlay-card-1-p-1": "P2P protokoły i aplikacje do przesyłania wiadomości mają różne problemy, które czynią je mniej niezawodnymi niż SimpleX, bardziej skomplikowanymi w analizie i podatnymi na kilka rodzajów ataków.", "simplex-network-overlay-card-1-li-2": "Projekt SimpleX, w przeciwieństwie do większości sieci P2P, nie posiada żadnych globalnych identyfikatorów użytkowników, nawet tymczasowych, a jedynie używa tymczasowych identyfikatorów parami, zapewniając lepszą anonimowość i ochronę metadanych.", - "simplex-network-overlay-card-1-li-4": "Implementacje P2P mogą być blokowane przez niektórych dostawców Internetu (np. BitTorrent). SimpleX jest agnostykiem transportowym - może działać przez standardowe protokoły internetowe, np. WebSockets.", + "simplex-network-overlay-card-1-li-4": "Implementacje P2P mogą być blokowane przez niektórych dostawców Internetu (np. BitTorrent). SimpleX jest agnostykiem transportowym — może działać przez standardowe protokoły internetowe, np. WebSockets.", "privacy-matters-overlay-card-1-p-3": "Niektóre firmy finansowe i ubezpieczeniowe wykorzystują wykresy społeczne do określania stóp procentowych i składek. Często powoduje to, że osoby o niższych dochodach płacą więcej — jest to znane jako 'premia za ubóstwo'.", - "privacy-matters-overlay-card-1-p-4": "Platforma SimpleX chroni prywatność Twoich połączeń lepiej niż jakakolwiek alternatywa, w pełni zapobiegając udostępnieniu Twojego wykresu społecznego jakimkolwiek firmom lub organizacjom. Nawet gdy ludzie korzystają z serwerów udostępnianych przez SimpleX Chat, nie znamy liczby użytkowników ani ich połączeń.", + "privacy-matters-overlay-card-1-p-4": "Sieć SimpleX chroni prywatność Twoich połączeń lepiej niż jakakolwiek alternatywa, w pełni zapobiegając udostępnieniu Twojego wykresu społecznego jakimkolwiek firmom lub organizacjom. Nawet gdy ludzie korzystają z serwerów prekonfigurowanych przez SimpleX Chat, aplikacje i operatorzy serwerów nie znają liczby użytkowników ani ich połączeń.", "privacy-matters-overlay-card-1-p-2": "Sprzedawcy internetowi wiedzą, że osoby o niższych dochodach częściej dokonują pilnych zakupów, więc mogą naliczać wyższe ceny lub usuwać rabaty.", - "privacy-matters-overlay-card-2-p-2": "Aby być obiektywnym i podejmować niezależne decyzje musisz mieć kontrolę nad swoją przestrzenią informacyjną. Jest to możliwe tylko wtedy, gdy korzystasz z prywatnej platformy komunikacyjnej, która nie ma dostępu do Twojego wykresu społecznego.", - "privacy-matters-overlay-card-2-p-3": "SimpleX to pierwsza platforma, która z założenia nie posiada żadnych identyfikatorów użytkowników, w ten sposób chroniąc Twój wykres połączeń lepiej niż jakakolwiek znana alternatywa.", + "privacy-matters-overlay-card-2-p-2": "Aby być obiektywnym i podejmować niezależne decyzje musisz mieć kontrolę nad swoją przestrzenią informacyjną. Jest to możliwe tylko wtedy, gdy korzystasz z prywatnej sieci komunikacyjnej, która nie ma dostępu do Twojego wykresu społecznego.", + "privacy-matters-overlay-card-2-p-3": "SimpleX to pierwsza sieć, która z założenia nie posiada żadnych identyfikatorów użytkowników, w ten sposób chroniąc Twój wykres połączeń lepiej niż jakakolwiek znana alternatywa.", "simplex-network-overlay-card-1-li-5": "Wszystkie znane sieci P2P mogą być podatne na atak Sybil, ponieważ każdy węzeł jest możliwy do odkrycia, a sieć działa jako całość. Znane środki łagodzące wymagają albo scentralizowanego komponentu, albo kosztownego proof of work. SimpleX nie ma możliwości odkrywania serwerów, jest pofragmentowana i działa jako wiele odizolowanych podsieci, co uniemożliwia ataki na całą sieć.", "privacy-matters-overlay-card-2-p-1": "Jeszcze nie tak dawno obserwowaliśmy, jak główne wybory były manipulowane przez szacowną firmę konsultingową, która wykorzystywała nasze wykresy społeczne do zniekształcania naszego spojrzenia na świat rzeczywisty i manipulowania naszymi głosami.", "privacy-matters-overlay-card-3-p-1": "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.", "privacy-matters-overlay-card-3-p-2": "Jedną z najbardziej wstrząsających historii jest doświadczenie Mohamedou Ould Salahi 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.", "privacy-matters-overlay-card-3-p-4": "Nie wystarczy używać komunikatora szyfrowanego end-to-end, wszyscy powinniśmy używać komunikatorów, które chronią prywatność naszych sieci osobistych — z kim jesteśmy połączeni.", - "simplex-unique-overlay-card-1-p-1": "W przeciwieństwie do innych platform komunikacyjnych, SimpleX ma brak identyfikatorów przypisanych do użytkowników. Nie polega na numerach telefonów, adresach opartych na domenie (jak e-mail lub XMPP), nazwach użytkowników, kluczach publicznych czy nawet losowych liczbach, aby zidentyfikować swoich użytkowników — nie wiemy, ile osób korzysta z naszych serwerów SimpleX.", + "simplex-unique-overlay-card-1-p-1": "W przeciwieństwie do innych sieci komunikacyjnych, SimpleX ma brak identyfikatorów przypisanych do użytkowników. Nie polega na numerach telefonów, adresach opartych na domenie (jak e-mail lub XMPP), nazwach użytkowników, kluczach publicznych czy nawet losowych liczbach, aby zidentyfikować swoich użytkowników — operatorzy serwerów SimpleX nie wiedzą, ile osób korzysta z ich serwerów.", "hero-overlay-card-2-p-3": "Nawet w przypadku najbardziej prywatnych aplikacji, które korzystają z trzeciej wersji usług Tor, jeśli rozmawiasz z dwoma różnymi kontaktami za pośrednictwem tego samego profilu, mogą one udowodnić, że są połączone z tą samą osobą.", "simplex-network-overlay-card-1-li-3": "P2P nie rozwiązuje ataku MITM problemu, a większość istniejących implementacji nie używa wiadomości out-of-band do początkowej wymiany klucza. SimpleX używa wiadomości out-of-band lub, w niektórych przypadkach, wcześniej istniejących bezpiecznych i zaufanych połączeń do początkowej wymiany klucza.", "hero-overlay-card-1-p-5": "Tylko urządzenia klienckie przechowują profile użytkowników, kontakty i grupy; wiadomości są przesyłane z 2-warstwowym szyfrowaniem end-to-end.", @@ -141,9 +141,9 @@ "privacy-matters-overlay-card-1-p-1": "Wiele dużych firm wykorzystuje informacje o tym, z kim jesteś połączony, aby oszacować Twoje dochody, sprzedać Ci produkty, których tak naprawdę nie potrzebujesz, oraz ustalić ceny.", "simplex-unique-overlay-card-2-p-2": "Nawet w przypadku opcjonalnego adresu użytkownika, podczas gdy może on być używany do wysyłania spamowych zapytań o kontakt, możesz go zmienić lub całkowicie usunąć bez utraty jakichkolwiek połączeń.", "simplex-unique-overlay-card-1-p-2": "Do dostarczania wiadomości SimpleX używa parami anonimowych adresów jednokierunkowych kolejek z wiadomościami, oddzielnych dla wiadomości odbieranych i wysyłanych, zwykle przez różne serwery.", - "simplex-unique-overlay-card-1-p-3": "Taka konstrukcja chroni prywatność tego, z kim się komunikujesz, ukrywając ją przed serwerami platformy SimpleX i przed wszelkimi obserwatorami. Aby ukryć swój adres IP przed serwerami, możesz połączyć się z serwerami SimpleX za pośrednictwem sieci Tor.", + "simplex-unique-overlay-card-1-p-3": "Taka konstrukcja chroni prywatność tego, z kim się komunikujesz, ukrywając ją przed serwerami sieci SimpleX i przed wszelkimi obserwatorami. Aby ukryć swój adres IP przed serwerami, możesz połączyć się z serwerami SimpleX za pośrednictwem sieci Tor.", "simplex-unique-overlay-card-3-p-4": "Nie ma żadnych identyfikatorów ani szyfrogramów wspólnych między wysyłanym i odbieranym ruchem serwera — jeśli ktokolwiek to obserwuje, nie może łatwo określić, kto komunikuje się z kim, nawet jeśli bezpieczeństwo protokołu TLS zostało zagrożone.", - "simplex-unique-overlay-card-2-p-1": "Ponieważ nie masz identyfikatora na platformie SimpleX, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku.", + "simplex-unique-overlay-card-2-p-1": "Ponieważ nie masz identyfikatora w sieci SimpleX, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat przechowuje wszystkie dane użytkowników tylko na urządzeniach klienckich przy użyciu przenośnego formatu zaszyfrowanej bazy danych, który można wyeksportować i przenieść na dowolne obsługiwane urządzenie.", "simplex-unique-overlay-card-3-p-3": "W przeciwieństwie do serwerów sieci federacyjnych (e-mail, XMPP czy Matrix), serwery SimpleX nie przechowują kont użytkowników, a jedynie przekazują wiadomości, chroniąc prywatność obu stron.", "simplex-unique-overlay-card-4-p-1": "Możesz używać SimpleX z własnymi serwerami i nadal komunikować się z osobami, które korzystają z dostarczonych przez nas prekonfigurowanych serwerów.", From eafbc80e79bf330bbc19b68ba1e679ddad30023c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 8 Sep 2025 17:25:31 +0100 Subject: [PATCH 036/137] 6.4.5: ios 298, android 319, desktop 120 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++++++++++----------- apps/multiplatform/gradle.properties | 8 ++--- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 98f27ac2ed..07e68e9284 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -2003,7 +2003,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2028,7 +2028,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2053,7 +2053,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2078,7 +2078,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2095,11 +2095,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2115,11 +2115,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2140,7 +2140,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2155,7 +2155,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2177,7 +2177,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2192,7 +2192,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2214,7 +2214,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2240,7 +2240,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2265,7 +2265,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2291,7 +2291,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2316,7 +2316,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2331,7 +2331,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2350,7 +2350,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 297; + CURRENT_PROJECT_VERSION = 298; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2365,7 +2365,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.4.4; + MARKETING_VERSION = 6.4.5; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 02acc2abb4..b4f41b8221 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.4.4 -android.version_code=317 +android.version_name=6.4.5 +android.version_code=319 android.bundle=false -desktop.version_name=6.4.4 -desktop.version_code=119 +desktop.version_name=6.4.5 +desktop.version_code=120 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 From b97a40f3565adf5762bf8d7bef8e85a536f003a7 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Tue, 9 Sep 2025 07:56:02 +0000 Subject: [PATCH 037/137] flatpak: update metainfo (#6257) --- .../flatpak/chat.simplex.simplex.metainfo.xml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index 630b178aaf..e44251361a 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,27 @@ + + https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html + +

New in v6.4.5:

+
    +
  • fixes for chats with admins.
  • +
  • better information about network errors.
  • +
+

New in v6.4-6.4.4:

+
    +
  • new UX to connect.
  • +
  • review new group members.
  • +
  • chat with group admins.
  • +
  • new UI languages: Catalan, Indonesian, Romanian and Vietnamese.
  • +
  • Linux app builds for aarch64 CPUs
  • +
  • UI support for bot commands.
  • +
  • support markdown hyperlinks, such as [click here](https://example.com).
  • +
  • option to remove tracking parameters from the links.
  • +
+
+
https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html From f2061a7c88dd505618bb6c1d034a44a31727a4f5 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:16:12 +0000 Subject: [PATCH 038/137] ci: update libffi to 3.5.2 in Mac build (#6258) --- scripts/desktop/build-lib-mac.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 934d9b8d7d..a3a824f81b 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -41,12 +41,12 @@ mkdir deps 2> /dev/null || true #cp $GHC_LIBS_DIR/libffi.dylib ./deps ( BUILD=$PWD - cp /tmp/libffi-3.4.4/*-apple-darwin*/.libs/libffi.dylib $BUILD/deps || \ + cp /tmp/libffi-3.5.2/*-apple-darwin*/.libs/libffi.dylib $BUILD/deps || \ ( \ cd /tmp && \ - curl --tlsv1.2 "https://gitlab.haskell.org/ghc/libffi-tarballs/-/raw/libffi-3.4.4/libffi-3.4.4.tar.gz?inline=false" -o libffi.tar.gz && \ + curl --tlsv1.2 "https://gitlab.haskell.org/ghc/libffi-tarballs/-/raw/3914c27381526ce586ea0ac0359a332fd82987af/libffi-3.5.2.tar.gz?inline=false" -o libffi.tar.gz && \ tar -xzvf libffi.tar.gz && \ - cd "libffi-3.4.4" && \ + cd "libffi-3.5.2" && \ ./configure && \ make && \ cp *-apple-darwin*/.libs/libffi.dylib $BUILD/deps \ From 382241fe3efab8d8d2cad2d8aaed61aed3f4fe67 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:22:34 +0000 Subject: [PATCH 039/137] core: rework synchronous group forwarding in receive loop into asynchronous delivery tasks (#6178) --- bots/api/TYPES.md | 21 + cabal.project | 2 +- docs/rfcs/2025-08-11-channels-forwarding.md | 237 ++++++++ .../types/typescript/src/types.ts | 36 ++ scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 4 + src/Simplex/Chat.hs | 4 + src/Simplex/Chat/Controller.hs | 5 +- src/Simplex/Chat/Delivery.hs | 188 ++++++ src/Simplex/Chat/Library/Commands.hs | 18 +- src/Simplex/Chat/Library/Internal.hs | 54 +- src/Simplex/Chat/Library/Subscriber.hs | 541 ++++++++++++------ src/Simplex/Chat/Messages.hs | 23 +- src/Simplex/Chat/Messages/Batch.hs | 43 ++ src/Simplex/Chat/Protocol.hs | 36 +- src/Simplex/Chat/Store/Delivery.hs | 362 ++++++++++++ src/Simplex/Chat/Store/Groups.hs | 4 +- src/Simplex/Chat/Store/Messages.hs | 23 +- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20250813_delivery_tasks.hs | 128 +++++ .../Store/Postgres/Migrations/chat_schema.sql | 160 +++++- src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20250813_delivery_tasks.hs | 172 ++++++ .../SQLite/Migrations/chat_query_plans.txt | 156 ++++- .../Store/SQLite/Migrations/chat_schema.sql | 83 ++- src/Simplex/Chat/Store/Shared.hs | 15 +- src/Simplex/Chat/Types.hs | 1 + src/Simplex/Chat/View.hs | 2 + tests/ChatTests/Groups.hs | 102 ++-- tests/ChatTests/Profiles.hs | 49 +- 30 files changed, 2151 insertions(+), 328 deletions(-) create mode 100644 docs/rfcs/2025-08-11-channels-forwarding.md create mode 100644 src/Simplex/Chat/Delivery.hs create mode 100644 src/Simplex/Chat/Store/Delivery.hs create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20250813_delivery_tasks.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 8353359484..f20c7c4913 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -2131,6 +2131,7 @@ MemberSupport: **Record type**: - groupId: int64 +- useRelays: bool - localDisplayName: string - groupProfile: [GroupProfile](#groupprofile) - localAlias: string @@ -3581,6 +3582,26 @@ InvalidQuote: InvalidMention: - type: "invalidMention" +InvalidDeliveryTask: +- type: "invalidDeliveryTask" +- taskId: int64 + +DeliveryTaskNotFound: +- type: "deliveryTaskNotFound" +- taskId: int64 + +InvalidDeliveryJob: +- type: "invalidDeliveryJob" +- jobId: int64 + +DeliveryJobNotFound: +- type: "deliveryJobNotFound" +- jobId: int64 + +WorkItemError: +- type: "workItemError" +- errContext: string + --- diff --git a/cabal.project b/cabal.project index 8a861a1909..dbf8b3c08f 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 7e98b3103f4eb9a6a9a99604afb6f3a32ffc013d + tag: a4f049d8da1021ee76fbd1b25635d16aef24154a source-repository-package type: git diff --git a/docs/rfcs/2025-08-11-channels-forwarding.md b/docs/rfcs/2025-08-11-channels-forwarding.md new file mode 100644 index 0000000000..ba0711c57a --- /dev/null +++ b/docs/rfcs/2025-08-11-channels-forwarding.md @@ -0,0 +1,237 @@ +# Channels forwarding + +This expands on the previous [channels rfc](./2025-07-30-channels.md), specifically on message forwarding by chat relays. + +## Problem + +Current implementation of groups uses forwarding mechanism for improved message delivery between connecting members. From perspective of reusing it for channels it has following limitations: +- Messages are forwarded only for the duration of members establishing connection, until they report x.grp.mem.con to forwarding admin. +- For a given pair of members only a single admin forwards messages - inviting admin (host) forwards messages between its invitees and members introduced to them. It means: + - This admin can be a single point of failure and/or can cause arbitrary delays in message delivery based on its availability when forwarding messages between these member pairs. + - Member pairs fully trust their single forwarding admin in forwarded content. + +Not limitations of protocol, but other weak points of current implementation: +- Forward operations are synchronous to message reception (by forwarding admin), so they're not resumable on failure. +- Forward operations are carried out as "group sends" to all required members (invitees/introduced members in respect to sending member), number of which is expected to grow very large in channels and can potentially have a big load on the database. + +## Solution + +Chat relays will serve as forwarding agents instead of inviting admins, with following modifications to protocol and implementation. + +### Forwarding not limited to establishing connection + +- In channels members will not connect directly to other members and channel owners. +- Chat relays will not take into consideration status of introductions. +- Chat relays will forward messages between owners and all members (from members to owners, for example, for reactions, comments). + +### All chat relays forward all messages + +- Instead of inviting admins all chat relays will forward all messages. +- Members will deduplicate messages (already implemented). +- Members should highlight differences in deduplicated messages. This would solve trust issue. + - Question: If messages are to be signed by owners, what differences can chat relays introduce? Does this point in initial doc imply owner sending different versions of message to chat relays? Losses in delivery? Answer: Not all messages will be signed. + +#### Highlighting deduplicated messages differences + +Currently we simply ignore duplicate messages - see createNewRcvMessage. + +Some options on how to highlight difference in messages from chat relays: +1. Show only the fact that there is difference. + - Persist duplicate error (SEDuplicateGroupMessage) as flag on chat item to then display warning sign in UI. +2. Save message hashes as some additional entity linked to message or chat item for each chat relay. + - If any new hash differs, set flag on chat item for warning sign in UI. + - Add ability to load additional info (with other chat item info via APIGetChatItemInfo) showing which relays sent different hashes (e.g. 1 differs from 2 others - show 1 as warning, all differ - show all as warning). +3. As previous, but save content. Possibly save content additionally to hashes, for faster comparison. + - Saving content means converting duplicate messages to chat item content, or only saving text, but latter will fail to show difference in files. +4. Create new versions as "different version of another message" chat items. + - Timestamp would have to be made the same as on original chat item, as it's not guaranteed to be the same - each chat relay will forward a broker ts based on its own metadata of receiving message from sender. + - It could be done via special replies, to re-use reply machinery. However, this won't work for messages being replies themselves then. + - Alternatively, it could say "different message from X relay". + +Service events will not have content. We still can/should compare them and indicate difference, e.g. by creating a special chat item (similar to integrity violation). + +How to compute hashes. Problem is with file descriptions, as it is legitimate case that they will be different. Hash could be computed as hash of message, excluding file description from FileInvitation. File digest and names should be the same, the problem of different names needs to be fixed. + +Files can also cause problem with showing difference in content if file preview is the same, but file hash is different. So showing difference in content does not exclude necessity of showing difference in hashes as in option 2. + +As a note, this whole section discusses an edge case of chat relays maliciously changing messages, and countermeasures to prevent them doing so undetectably. May be not worth implementing overly complex solution as MVP (options 3, 4). + +For MVP from UI standpoint this should suffice: a marker on item, that would show in info which relays delivered it with a sign which relay delivered different hash. Possibly also special event item next to marked item (with same timestamp) - for service events that don't create a chat item, or in case we don't account it for some chat items. + +### Forwarding jobs + +- Received messages will be marked for forwarding and picked up by a forwarding worker to form forwarding jobs. +- Forwarding job can be split into smaller batches by members to make smaller group sends one at a time. + +**How forwarding works now:** + +Currently we build ad-hoc forwarding instruction for each message based on its scope (GroupForwardScope). The output of processing a batch of messages is `Map GroupForwardScope (NonEmpty (ChatMessage 'Json))`. Then for each scope message batches are sent separately to different member lists (to clarify, "sent" here means scheduled for sending in agent). All this is done synchronously in receive loop. + +**How forwarding will work:** + +Chat relay should be able to handle channels consisting of hundreds of thousands of members. Synchronous forwarding will be replaced by asynchronous forwarding jobs to reduce load in the receive loop, and make forwarding resumable on failure. Also loading all members in memory will not scale, so these jobs have to be split into smaller batches. + +Question: Should synchronous processing be changed to forwarding jobs for all types of scopes, or only for Main (GFSMain - for regular messages) and All (GFSAll - for all current members and for all pending members scopes), and not for Support scope (GFSMemberSupport)? + - Pros/cons of persisting jobs for all types of scopes: + - Possibly more uniform processing. However, for support scope group member id is required. Either encode scope as json or add field for scope group member id for persistence (see schema below). + - Requires forwarding worker to have more logic ("if"), or different worker types. + - Pros/cons of persisting jobs only for Main and All scopes: + - Forwarding worker and persistence are simpler, but logic lives in 2 places (synchronous as now + worker). + - As a note, support scopes have small number of members to forward to, so optimizing forward for them is not a necessity. + +Answer: All forwarding logic will become asynchronous. This will reduce load in the receive loop for inviting admins in regular groups as well. + +Forwarding jobs for different groups can be concurrent, inside group should be sequential to follow order of messages. One approach could be to create a dedicated forwarding worker for each group. Different scope types can also use dedicated workers (possibly: 1 worker for all + main scopes, 1 - for all support scopes), so heavy full group forwarding wouldn't slow down forwarding in much smaller support scopes. + +Forwarding workers will re-use worker abstraction from agent. + +Forwarding workers should use low priority db pool. + +We roughly need following data for a forwarding job: +- group id, +- forwarding scope (forward_scope), +- sending member - to include memberId in XGrpMsgForward, +- broker timestamp of received messages batch to include in XGrpMsgForward, +- "message from channel" flag from sending owner - see "Hiding sending owner id" section below (group_as_sender), +- list of messages (events). + +**How to build message batches for a forwarding job:** + +Currently for each message a full MsgBody is saved on `messages` table record, which is just a ByteString. Also if received messages were batched, full batch body is saved for each message record (it's a questionable design). For example, here is message body of batched deletion of 2 chat items, which is saved on 2 message records: + +``` +msg_body = [{"v":"1-16","msgId":"ZUd3bzVDTXFHWmc3Z29YSQ==","event":"x.msg.del","params":{"msgId":"QVVuN2RkaXg0aVJJdTZXSA=="}},{"v":"1-16","msgId":"amM5dzV2RkRjNmNYSDRLQQ==","event":"x.msg.del","params":{"msgId":"NzFlV3pNRStpQTRrQjRYUg=="}}] +``` + +On the other hand for forwarding operation we require ChatMessage 'Json (for XGrpMsgForward), which could be saved on message records instead. + +As a side note, it seems we can stop saving full batch body as msg_body for received messages, as it's not used for any purpose. This field is only used for retrieving and sending pending group messages (sent, not received). + +Alternatively, we could read full msg_body, and repeatedly parse list of ChatMessage 'Json. However, this conflicts with possibility that a single batch body will have messages of different scopes, which current processing logic allows, although there's no legitimate case as of now. This means we'd have make jobs forward to different lists of members, or have some supervisor to launch different jobs, which further complicates the processing. Also, this limits forwarding to the same batch sizes (in terms of number of messages) as those batches that received messages came in, and instead we could possibly fit more messages into batches if available. Overall, persisting ChatMessage' Json on each message record, and then retrieving available messages for the job, seems preferable. + +**How to split forwarding jobs into smaller delivery batches:** + +For simplicity, this will be done only for channels, and not for inviting admins in regular groups, at least initially. Inviting admins have more complex logic, involving multiple queries and filters to retrieve less (only necessary) members, as we're trying to limit load on them, as they can be mobile clients. They will be moved to asynchronous processing too, though. + +Forwarding job will read required members in loop using a cursor (e.g., use group_member_id as a cursor) to split into further delivery jobs (group sends) by members. As a reminder, group send in terms of chat logic is not a network operation, but a request to agent. We can consider this part a black box for now, and consider if groups sends should be made concurrently later. For now though, for simplicity we can consider that group sends are done in a synchronous loop with moving cursor for member retrieval. Cursor position can be remembered on the job record after each group send, for recovery. + +It's unclear if there's a need for a separate lower level abstraction for delivery jobs, that could be reused for feeds and other purposes, as they would require different logic of building delivery lists, and scheduling a delivery for agent already serves a similar purpose. For now, it seems this additional abstraction is unnecessary. + +**How a forwarding job will work overall:** + +0. There is some process on start that launches necessary workers for each group/scope client serves as a forwarding agent (for those that have messages that need forwarding). Also message receive loop "kicks" necessary workers. Below we start with a worker for some group/scope trying to retrieve next work item, that is being next message(s) to forward. +1. Worker retrieves next message from `messages` table that was marked for forwarding, that matches the worker's group/scope. + - We may need a separate field on message record to track "forward pending" state, to know what to forward. Setting forward_scope as a task with addition of forward_complete flag to mark forward completion may be enough (see schema below). +2. Worker checks, if this message is already attached to some forwarding job, and the state of the job. + 1. Worker gets job record. + - If message is not attached to any job yet, worker creates a new job record, sets job id on the message record (normal execution path). + - Otherwise worker gets existing job attached to the message (recovery path). + 2. Worker determines forwarding batch encoding for the job. + - If encoding is not saved on the job, it means it wasn't determined before (normal execution): + 1. Worker retrieves more messages that match its group/scope. + - Could be in loop or in bulk. + - We won't know how much we'd need. Some heuristic could be read 100, then mark those that are used with jobId, or load more if needed. + - Also message list has to preserve reception order. + 2. Worker prepares encoding (wraps messages in XGrpMsgForward events with metadata) and saves it on the job record. + - It's important to put encoding into job and mark all relevant messages in one transaction. + - Otherwise worker uses saved encoding (recovery). +3. With moving cursor on group member id for member retrieval, forward encoded batch. In loop: + 1. Retrieve members up to some limit and filtering according to a cursor. In normal execution path on first iteration job would not have a previously saved cursor yet. + - For Main scope all current members will be retrieved. + - For Support scopes - moderators and above + scope member. For support scope job can be done without a cursor, instead simply loading all its members and forwarding. + - For All scope - all current and pending members (In channels there is no need for member review. So possibly we don't have to account for All scope). + - Main difference from forwarding in regular groups here is that for inviting admins we retrieve only introduced and invited members for the message's sending member, that are not yet connected. For channels there will be no such filtering. + 2. Group send encoded batch to retrieved members. + 3. Update cursor on the job. +4. Marks all messages attached to the job as forwarded (forward_complete), deletes the job record. + - Possibly do in cleanup manager and delete after say a week. + +Schema draft: + +```sql +CREATE TABLE forwarding_jobs ( + forwarding_job_id INTEGER PRIMARY KEY, + msg_batch_encoding TEXT, + cursor_group_member_id INTEGER +) + +ALTER TABLE messages ADD COLUMN chat_message_json TEXT; +ALTER TABLE messages ADD COLUMN forward_scope TEXT; +ALTER TABLE messages ADD COLUMN group_as_sender INTEGER NOT NULL DEFAULT 0; -- for "message from channel" flag from owner +ALTER TABLE messages ADD COLUMN forward_complete INTEGER NOT NULL DEFAULT 0; +ALTER TABLE messages ADD COLUMN forwarding_job_id INTEGER REFERENCES forwarding_jobs ON DELETE SET NULL; + +-- indexes for fkey, search based on group/scope and order; +``` + +## Other considerations + +### Hiding sending owner id + +Initial doc mentions "messages sent from channel name" in minimal testable scope. This would be a feature allowing owners to send messages to channel without it being clear for members which owner sent it. For this, we'd also like to hide sending owner's member id from forwarding operation. + +This means MemberId in XGrpMsgForward should become optional. + +```haskell +XGrpMsgForward :: Maybe MemberId -> ChatMessage 'Json -> UTCTime -> ChatMsgEvent 'Json +``` + +Receiving members would then save this message as received from group. We already have necessary machinery for chat items persistence - see ShowGroupAsSender. However, message processing logic would have to be reworked to allow for absence of group member. Also, sending owner would have to indicate it to chat relays in XMsgNew (TBC - other events?). + +```haskell +XMsgNew :: ShowGroupAsSender -> MsgContainer -> ChatMsgEvent 'Json +``` + +ShowGroupAsSender can be in MsgContainer, not separate. + +Chat relays should not hide sending owner from other owners: sending owner should be visible to them, but message shown as from channel. This means forwarding job has to be split into separate sends with different sets of XGrpMsgForward events (with and without sending owner's member id). + +### Connection deleting events + +Forwarding regular messages, reactions, updates and many other events is straightforward. However, naive processing of some events currently breaks forwarding logic, specifically member and group deletion. + +As it is now, forwarding operation follows event processing. So, processing of XGrpMemDel, XGrpDel first deletes member connection (or connections with all members in case of group deletion), then attempts to forward these events, which in reality never works as at this point connection is already deleted. + +Currently we ignore this problem (there are TODOs), as all members according to protocol connect with each other, and forwarding serves only as message delivery improvement/backup, and not a main route, and so members in at least supposedly most cases receive these events from the original sender. + +With chat relays, however, no messages are sent directly from owners to members, so if logic is kept as is these events would never be received. So, their processing should be special cased to delay until after forwarding operation. + +## Further considerations, ideas (Update) + +- Sending member profiles. + - Profiles of owners (and admins, moderators) should be sent to all members on joining. + - Profiles of other members should be sent on interaction with them. + - For MVP: we can show counts on reactions and avoid solving it, but we should have a design to solve this problem, as it would be necessary for comments and later for large groups. + - Solution draft - partition members based on join timestamp and sender last interaction time: + - Track last interaction timestamp on each member. + - When forwarding from this member partition members into two parts: include profile for those who joined after interaction, don't include for those who joined before interaction. + - This suggests that job would be divided into two parts. + - Protocol to request member profile as a fallback. + - This becomes more complex in case batch has multiple members - for n required profiles to send, recipients need to be partitioned into n + 1 parts. + - Better solution - schedule profile deliveries separately from batch on first post-join delivery per sender. + - Track last_profile_delivery_ts (for sender), join_ts (for recipient) on member records. + - On the sender's first overall interaction (last_profile_delivery_ts is null), first create a special task to deliver profile to all (in scope All). + - On following sends on batching, for senders whose last_profile_delivery_ts < any member's join_ts (i.e., some member missed the initial broadcast), create a profile delivery task for those specific senders. + - Message task should have a flag whether profile should be delivered to anyone (set to false on first profile delivery). Checking last_profile_delivery_ts is null seems to be sufficient. +- Don't partition for owners based on "message from channel" flag to simplify delivery - no need for two separate jobs/cursors. +- When chat relay receives group deletion event, or event removing it [chat relay itself] from the group: + - Chat relay should kill all forwarding workers for the group -> delete all jobs -> create one new job to forward group deletion. + - It could be a special type of job. +- Sending each reaction (and in future comment) won't scale well. + - Instead periodically send reaction and comment counts. + - Send reactions and comments themselves on request. + - This implies that instead of message records, special entity for forwarding tasks should be added, for worker to search for next work items - see more below. +- Connections with priority. + - Client could have 2 connections/queues with relay, and relay - 2 subscribers. + - Separation of responsibilities between connections/queues: + - Normal queue to be used for regular forwarding of messages, reactions, etc. + - High-priority queue to be used for serving client requests and sending important service events (e.g. ownership changes, group deletion). + - Possibly special case of connection redundancy. +- Design to be reworked to use special entity for forwarding tasks instead of relying on messages. + - Points for this: + - Batched reactions/comments counts. + - Special logic on group deletion/relay removal. + - Possibly special logic on sending member profiles, as it's not needed for all types of jobs. + - Sum type of task types. + - Some tasks may simply point to message records. + - Some tasks may be created for further updating their metadata / scheduling, e.g. "send reactions/comments count update", and the information itself may be taken from chat items. diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 3a056293c5..bdc99ee750 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2427,6 +2427,7 @@ export enum GroupFeatureEnabled { export interface GroupInfo { groupId: number // int64 + useRelays: boolean localDisplayName: string groupProfile: GroupProfile localAlias: string @@ -3812,6 +3813,11 @@ export type StoreError = | StoreError.UsageConditionsNotFound | StoreError.InvalidQuote | StoreError.InvalidMention + | StoreError.InvalidDeliveryTask + | StoreError.DeliveryTaskNotFound + | StoreError.InvalidDeliveryJob + | StoreError.DeliveryJobNotFound + | StoreError.WorkItemError export namespace StoreError { export type Tag = @@ -3892,6 +3898,11 @@ export namespace StoreError { | "usageConditionsNotFound" | "invalidQuote" | "invalidMention" + | "invalidDeliveryTask" + | "deliveryTaskNotFound" + | "invalidDeliveryJob" + | "deliveryJobNotFound" + | "workItemError" interface Interface { type: Tag @@ -4269,6 +4280,31 @@ export namespace StoreError { export interface InvalidMention extends Interface { type: "invalidMention" } + + export interface InvalidDeliveryTask extends Interface { + type: "invalidDeliveryTask" + taskId: number // int64 + } + + export interface DeliveryTaskNotFound extends Interface { + type: "deliveryTaskNotFound" + taskId: number // int64 + } + + export interface InvalidDeliveryJob extends Interface { + type: "invalidDeliveryJob" + jobId: number // int64 + } + + export interface DeliveryJobNotFound extends Interface { + type: "deliveryJobNotFound" + jobId: number // int64 + } + + export interface WorkItemError extends Interface { + type: "workItemError" + errContext: string + } } export enum SwitchPhase { diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index a5f2790418..554cbb2286 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."7e98b3103f4eb9a6a9a99604afb6f3a32ffc013d" = "0g6lm65hs2kp2rsk9lqzj42nq51i5xynxrf16axma80cq0jqzxzl"; + "https://github.com/simplex-chat/simplexmq.git"."a4f049d8da1021ee76fbd1b25635d16aef24154a" = "1wvrmb6swpsl24by8d6wz0nfj8bi2pbkigv26pl7c1binc0qichy"; "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"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 88a92e6bb4..fd8a089193 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -40,6 +40,7 @@ library Simplex.Chat.AppSettings Simplex.Chat.Call Simplex.Chat.Controller + Simplex.Chat.Delivery Simplex.Chat.Files Simplex.Chat.Library.Commands Simplex.Chat.Library.Internal @@ -72,6 +73,7 @@ library Simplex.Chat.Store.AppSettings Simplex.Chat.Store.Connections Simplex.Chat.Store.ContactRequest + Simplex.Chat.Store.Delivery Simplex.Chat.Store.Direct Simplex.Chat.Store.Files Simplex.Chat.Store.Groups @@ -115,6 +117,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20250729_member_contact_requests Simplex.Chat.Store.Postgres.Migrations.M20250801_via_group_link_uri Simplex.Chat.Store.Postgres.Migrations.M20250802_chat_peer_type + Simplex.Chat.Store.Postgres.Migrations.M20250813_delivery_tasks else exposed-modules: Simplex.Chat.Archive @@ -255,6 +258,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri Simplex.Chat.Store.SQLite.Migrations.M20250802_chat_peer_type + Simplex.Chat.Store.SQLite.Migrations.M20250813_delivery_tasks other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e14275b75c..e39f8d9a77 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -163,6 +163,8 @@ newChatController remoteCtrlSession <- newTVarIO Nothing filesFolder <- newTVarIO optFilesFolder chatStoreChanged <- newTVarIO False + deliveryTaskWorkers <- TM.emptyIO + deliveryJobWorkers <- TM.emptyIO expireCIThreads <- TM.emptyIO expireCIFlags <- TM.emptyIO cleanupManagerAsync <- newTVarIO Nothing @@ -203,6 +205,8 @@ newChatController remoteCtrlSession, config, filesFolder, + deliveryTaskWorkers, + deliveryJobWorkers, expireCIThreads, expireCIFlags, cleanupManagerAsync, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index d8a6f7a30a..0b2f16ad1d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -54,6 +54,7 @@ import Numeric.Natural import qualified Paths_simplex_chat as SC import Simplex.Chat.AppSettings import Simplex.Chat.Call +import Simplex.Chat.Delivery import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Operators @@ -70,7 +71,7 @@ import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg, Worker) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority) @@ -244,6 +245,8 @@ data ChatController = ChatController remoteCtrlSession :: TVar (Maybe (SessionSeq, RemoteCtrlSession)), -- Supervisor process for hosted controllers config :: ChatConfig, filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps, + deliveryTaskWorkers :: TMap DeliveryWorkerKey Worker, + deliveryJobWorkers :: TMap DeliveryWorkerKey Worker, expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIFlags :: TMap UserId Bool, cleanupManagerAsync :: TVar (Maybe (Async ())), diff --git a/src/Simplex/Chat/Delivery.hs b/src/Simplex/Chat/Delivery.hs new file mode 100644 index 0000000000..d0a77514eb --- /dev/null +++ b/src/Simplex/Chat/Delivery.hs @@ -0,0 +1,188 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Delivery where + +import Data.ByteString.Char8 (ByteString) +import Data.Int (Int64) +import Data.Maybe (fromMaybe) +import Data.Time.Clock (UTCTime) +import Simplex.Chat.Messages (GroupChatScopeInfo (..), MessageId) +import Simplex.Chat.Options.DB (FromField (..), ToField (..)) +import Simplex.Chat.Protocol +import Simplex.Chat.Types +import Simplex.Chat.Types.Shared +import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import Simplex.Messaging.Encoding.String + +type DeliveryWorkerKey = (GroupId, DeliveryWorkerScope) + +data DeliveryWorkerScope + = DWSGroup + | DWSMemberSupport + -- | DWSMemberProfileUpdate + deriving (Eq, Ord, Show) + +instance FromField DeliveryWorkerScope where fromField = fromTextField_ textDecode + +instance ToField DeliveryWorkerScope where toField = toField . textEncode + +instance TextEncoding DeliveryWorkerScope where + textDecode = \case + "group" -> Just DWSGroup + "member_support" -> Just DWSMemberSupport + -- "member_profile_update" -> Just DWSMemberProfileUpdate + _ -> Nothing + textEncode = \case + DWSGroup -> "group" + DWSMemberSupport -> "member_support" + -- DWSMemberProfileUpdate -> "member_profile_update" + +data DeliveryJobScope + = DJSGroup {jobSpec :: DeliveryJobSpec} + | DJSMemberSupport {supportGMId :: GroupMemberId} + -- | DJSMemberProfileUpdate + deriving (Show) + +data DeliveryJobSpec + = DJDeliveryJob {includePending :: Bool} + | DJRelayRemoved + deriving (Show) + +data DeliveryJobSpecTag + = DJSTDeliveryJob + | DJSTRelayRemoved + deriving (Show) + +instance FromField DeliveryJobSpecTag where fromField = fromTextField_ textDecode + +instance ToField DeliveryJobSpecTag where toField = toField . textEncode + +instance TextEncoding DeliveryJobSpecTag where + textDecode = \case + "delivery_job" -> Just DJSTDeliveryJob + "relay_removed" -> Just DJSTRelayRemoved + _ -> Nothing + textEncode = \case + DJSTDeliveryJob -> "delivery_job" + DJSTRelayRemoved -> "relay_removed" + +toWorkerScope :: DeliveryJobScope -> DeliveryWorkerScope +toWorkerScope = \case + DJSGroup _ -> DWSGroup + DJSMemberSupport _ -> DWSMemberSupport + -- DJSMemberProfileUpdate -> DWSMemberProfileUpdate + +isRelayRemoved :: DeliveryJobScope -> Bool +isRelayRemoved = \case + DJSGroup {jobSpec} -> case jobSpec of + DJRelayRemoved -> True + _ -> False + _ -> False + +jobScopeImpliedSpec :: DeliveryJobScope -> DeliveryJobSpec +jobScopeImpliedSpec = \case + DJSGroup {jobSpec} -> jobSpec + DJSMemberSupport {} -> DJDeliveryJob {includePending = False} + +jobSpecImpliedPending :: DeliveryJobSpec -> Bool +jobSpecImpliedPending = \case + DJDeliveryJob {includePending} -> includePending + DJRelayRemoved -> True + +infoToDeliveryScope :: GroupInfo -> Maybe GroupChatScopeInfo -> DeliveryJobScope +infoToDeliveryScope GroupInfo {membership} = \case + Nothing -> DJSGroup {jobSpec = DJDeliveryJob {includePending = False}} + Just GCSIMemberSupport {groupMember_} -> + let supportGMId = groupMemberId' $ fromMaybe membership groupMember_ + in DJSMemberSupport {supportGMId} + +memberEventDeliveryScope :: GroupMember -> Maybe DeliveryJobScope +memberEventDeliveryScope m@GroupMember {memberRole, memberStatus} + | memberStatus == GSMemPendingApproval = Nothing + | memberStatus == GSMemPendingReview = Just $ DJSMemberSupport {supportGMId = groupMemberId' m} + | memberRole >= GRModerator = Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} + | otherwise = Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}} + +data NewMessageDeliveryTask = NewMessageDeliveryTask + { messageId :: MessageId, + jobScope :: DeliveryJobScope, + messageFromChannel :: MessageFromChannel + } + deriving (Show) + +data MessageDeliveryTask = MessageDeliveryTask + { taskId :: Int64, + jobScope :: DeliveryJobScope, + senderGMId :: GroupMemberId, + senderMemberId :: MemberId, + senderMemberName :: ContactName, + brokerTs :: UTCTime, + chatMessage :: ChatMessage 'Json, + messageFromChannel :: MessageFromChannel + } + deriving (Show) + +deliveryTaskId :: MessageDeliveryTask -> Int64 +deliveryTaskId = taskId + +data DeliveryTaskStatus + = DTSNew -- created for delivery task worker to pick up and convert into a delivery job + | DTSProcessed -- processed by delivery task worker, delivery job created, task can be deleted + | DTSError -- permanent error + deriving (Show) + +instance FromField DeliveryTaskStatus where fromField = fromTextField_ textDecode + +instance ToField DeliveryTaskStatus where toField = toField . textEncode + +instance TextEncoding DeliveryTaskStatus where + textDecode = \case + "new" -> Just DTSNew + "processed" -> Just DTSProcessed + "error" -> Just DTSError + _ -> Nothing + textEncode = \case + DTSNew -> "new" + DTSProcessed -> "processed" + DTSError -> "error" + +data MessageDeliveryJob = MessageDeliveryJob + { jobId :: Int64, + jobScope :: DeliveryJobScope, + singleSenderGMId_ :: Maybe GroupMemberId, -- Just for single-sender deliveries, Nothing for multi-sender deliveries + body :: ByteString, + cursorGMId_ :: Maybe GroupMemberId + } + deriving (Show) + +deliveryJobId :: MessageDeliveryJob -> Int64 +deliveryJobId = jobId + +data DeliveryJobStatus + = DJSPending -- created for delivery job worker to pick up + | DJSComplete -- complete by delivery job worker, job can be deleted + | DJSError -- permanent error + deriving (Show) + +instance FromField DeliveryJobStatus where fromField = fromTextField_ textDecode + +instance ToField DeliveryJobStatus where toField = toField . textEncode + +instance TextEncoding DeliveryJobStatus where + textDecode = \case + "pending" -> Just DJSPending + "complete" -> Just DJSComplete + "error" -> Just DJSError + _ -> Nothing + textEncode = \case + DJSPending -> "pending" + DJSComplete -> "complete" + DJSError -> "error" + +-- data MemberProfileUpdateTask = undefined + +-- data MemberProfileUpdateJob = undefined diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index b668a1d793..41d684c114 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -74,6 +74,7 @@ import Simplex.Chat.Store import Simplex.Chat.Store.AppSettings import Simplex.Chat.Store.ContactRequest import Simplex.Chat.Store.Connections +import Simplex.Chat.Store.Delivery import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups @@ -181,6 +182,7 @@ startChatController mainApp enableSndFiles = do then do startXFTP xftpStartWorkers void $ forkIO $ startFilesToReceive users + startDeliveryWorkers startCleanupManager void $ forkIO $ mapM_ startExpireCIs users else when enableSndFiles $ startXFTP xftpStartSndWorkers @@ -190,6 +192,10 @@ startChatController mainApp enableSndFiles = do runExceptT (withAgent $ \a -> startWorkers a tmp) >>= \case Left e -> liftIO $ putStrLn $ "Error starting XFTP workers: " <> show e Right _ -> pure () + startDeliveryWorkers = + runExceptT (startDeliveryTaskWorkers >> startDeliveryJobWorkers) >>= \case + Left e -> liftIO $ putStrLn $ "Error starting delivery workers: " <> show e + Right _ -> pure () startCleanupManager = do cleanupAsync <- asks cleanupManagerAsync readTVarIO cleanupAsync >>= \case @@ -607,7 +613,6 @@ processChatCommand vr nm = \case gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId let mc = MCReport reportText reportReason cm = ComposedMessage {fileSource = Nothing, quotedItemId = Just reportedItemId, msgContent = mc, mentions = M.empty} - -- TODO [knocking] reports sent to support scope may be wrong sendGroupContentMessages user gInfo (Just $ GCSMemberSupport Nothing) False Nothing [composedMessageReq cm] ReportMessage {groupName, contactName_, reportReason, reportedMessage} -> withUser $ \user -> do gId <- withFastStore $ \db -> getGroupIdByName db user groupName @@ -2379,6 +2384,7 @@ processChatCommand vr nm = \case withGroupLock "blockForAll" groupId $ do Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId when (selfSelected gInfo) $ throwCmdError "can't block/unblock self" + -- TODO [channels fwd] consider sending restriction to all members (remove filtering), as we do in delivery jobs let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members when (length blockMems /= length memberIds) $ throwChatError CEGroupMemberNotFound when (length memberIds > 1 && anyAdmin) $ throwCmdError "can't block/unblock multiple members when admins selected" @@ -4314,6 +4320,8 @@ cleanupManager = do forM_ us $ cleanupUser interval stepDelay forM_ us' $ cleanupUser interval stepDelay cleanupMessages `catchAllErrors` eToView + cleanupDeliveryTasks `catchAllErrors` eToView + cleanupDeliveryJobs `catchAllErrors` eToView -- TODO possibly, also cleanup async commands cleanupProbes `catchAllErrors` eToView liftIO $ threadDelay' $ diffToMicroseconds interval @@ -4345,6 +4353,14 @@ cleanupManager = do ts <- liftIO getCurrentTime let cutoffTs = addUTCTime (-(30 * nominalDay)) ts withStore' (`deleteOldMessages` cutoffTs) + cleanupDeliveryTasks = do + ts <- liftIO getCurrentTime + let cutoffTs = addUTCTime (-(7 * nominalDay)) ts + withStore' (`deleteDoneDeliveryTasks` cutoffTs) + cleanupDeliveryJobs = do + ts <- liftIO getCurrentTime + let cutoffTs = addUTCTime (-(7 * nominalDay)) ts + withStore' (`deleteDoneDeliveryJobs` cutoffTs) cleanupProbes = do ts <- liftIO getCurrentTime let cutoffTs = addUTCTime (-(14 * nominalDay)) ts diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index e3885a3c9a..707ec7c109 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1441,21 +1441,26 @@ mkGroupChatScope gInfo@GroupInfo {membership} m | otherwise = pure (gInfo, m, Nothing) -mkGetMessageChatScope :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> Maybe MsgScope -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo) -mkGetMessageChatScope vr user gInfo@GroupInfo {membership} m msgScope_ = +mkGetMessageChatScope :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> MsgContent -> Maybe MsgScope -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo) +mkGetMessageChatScope vr user gInfo@GroupInfo {membership} m mc msgScope_ = mkGroupChatScope gInfo m >>= \case groupScope@(_gInfo', _m', Just _scopeInfo) -> pure groupScope - (_, _, Nothing) -> case msgScope_ of - Nothing -> pure (gInfo, m, Nothing) - Just (MSMember mId) - | sameMemberId mId membership -> do - (gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo - pure (gInfo', m, Just scopeInfo) - | otherwise -> do - referredMember <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo mId - -- TODO [knocking] return patched _referredMember' too? - (_referredMember', scopeInfo) <- mkMemberSupportChatInfo referredMember - pure (gInfo, m, Just scopeInfo) + (_, _, Nothing) + | isReport mc -> do + -- TODO [knocking] return patched _m'? + (_m', scopeInfo) <- mkMemberSupportChatInfo m -- only support scope member can send a report (m is sender) + pure (gInfo, m, Just scopeInfo) + | otherwise -> case msgScope_ of + Nothing -> pure (gInfo, m, Nothing) + Just (MSMember mId) + | sameMemberId mId membership -> do + (gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo + pure (gInfo', m, Just scopeInfo) + | otherwise -> do + referredMember <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo mId + -- TODO [knocking] return patched _referredMember'? + (_referredMember', scopeInfo) <- mkMemberSupportChatInfo referredMember + pure (gInfo, m, Just scopeInfo) mkGroupSupportChatInfo :: GroupInfo -> CM (GroupInfo, GroupChatScopeInfo) mkGroupSupportChatInfo gInfo@GroupInfo {membership} = @@ -1971,6 +1976,7 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded +-- TODO [channels fwd] review for channels - should only directly send to chat relays, for others - MSAForwarded memberSendAction :: GroupInfo -> NonEmpty (ChatMsgEvent e) -> [GroupMember] -> GroupMember -> Maybe MemberSendAction memberSendAction gInfo events members m@GroupMember {memberRole, memberStatus} = case memberConn m of Nothing -> pendingOrForwarded @@ -2010,6 +2016,14 @@ memberSendAction gInfo events members m@GroupMember {memberRole, memberStatus} = XGrpMsgForward {} -> True _ -> False +-- Should match memberSendAction logic +readyMemberConn :: GroupMember -> Maybe (GroupMemberId, Connection) +readyMemberConn GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}} + | (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) && not (connInactive conn) = + Just (groupMemberId, conn) + | otherwise = Nothing +readyMemberConn GroupMember {activeConn = Nothing} = Nothing + sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM () sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do msg <- createSndMessage chatMsgEvent (GroupId groupId) @@ -2041,7 +2055,8 @@ saveDirectRcvMSG :: MsgEncodingI e => Connection -> MsgMeta -> MsgBody -> ChatMe saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do conn' <- updatePeerChatVRange conn chatVRange let agentMsgId = fst $ recipient agentMsgMeta - newMsg = NewRcvMessage {chatMsgEvent, msgBody} + brokerTs = metaBrokerTs agentMsgMeta + newMsg = NewRcvMessage {chatMsgEvent, msgBody, brokerTs} rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing pure (conn', msg) @@ -2050,7 +2065,8 @@ saveGroupRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> Connectio saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do (am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- updateMemberChatVRange authorMember conn chatVRange let agentMsgId = fst $ recipient agentMsgMeta - newMsg = NewRcvMessage {chatMsgEvent, msgBody} + brokerTs = metaBrokerTs agentMsgMeta + newMsg = NewRcvMessage {chatMsgEvent, msgBody, brokerTs} rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} msg <- withStore (\db -> createNewMessageAndRcvMsgDelivery db (GroupId groupId) newMsg sharedMsgId_ rcvMsgDelivery $ Just amGroupMemId) @@ -2064,11 +2080,13 @@ saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta _ -> throwError e pure (am', conn', msg) -saveGroupFwdRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> GroupMember -> MsgBody -> ChatMessage e -> CM RcvMessage -saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember@GroupMember {memberId = refMemberId} msgBody ChatMessage {msgId = sharedMsgId_, chatMsgEvent} = do - let newMsg = NewRcvMessage {chatMsgEvent, msgBody} +saveGroupFwdRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> GroupMember -> MsgBody -> ChatMessage e -> UTCTime -> CM RcvMessage +saveGroupFwdRcvMsg user groupId forwardingMember refAuthorMember@GroupMember {memberId = refMemberId} msgBody ChatMessage {msgId = sharedMsgId_, chatMsgEvent} brokerTs = do + let newMsg = NewRcvMessage {chatMsgEvent, msgBody, brokerTs} fwdMemberId = Just $ groupMemberId' forwardingMember refAuthorId = Just $ groupMemberId' refAuthorMember + -- TODO [channels fwd] recipient should deduplicate its own messages if they are forwarded back + -- TODO - it can happen when chat relay forwards a batch of messages of different senders withStore (\db -> createNewRcvMessage db (GroupId groupId) newMsg sharedMsgId_ refAuthorId fwdMemberId) `catchAllErrors` \e -> case e of ChatErrorStore (SEDuplicateGroupMessage _ _ (Just authorGroupMemberId) Nothing) -> do diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 9ecc09004e..7dd8595f93 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -22,12 +22,14 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader +import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Either (lefts, partitionEithers, rights) +import Data.Foldable (foldr') import Data.Functor (($>)) import Data.Int (Int64) -import Data.List (foldl') -import Data.List.NonEmpty (NonEmpty (..), (<|)) +import Data.List (find, foldl') +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -41,8 +43,10 @@ import qualified Data.UUID.V4 as V4 import Data.Word (Word32) import Simplex.Chat.Call import Simplex.Chat.Controller +import Simplex.Chat.Delivery import Simplex.Chat.Library.Internal import Simplex.Chat.Messages +import Simplex.Chat.Messages.Batch (batchDeliveryTasks1) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.ProfileGenerator (generateRandomProfile) @@ -51,6 +55,7 @@ import Simplex.Chat.Store import Simplex.Chat.Store.Connections import Simplex.Chat.Store.ContactRequest import Simplex.Chat.Store.Direct +import Simplex.Chat.Store.Delivery import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Messages @@ -65,6 +70,8 @@ import Simplex.FileTransfer.Protocol (FilePartyI) import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent +import Simplex.Messaging.Agent.Client (getAgentWorker, waitForWork, withWork_, withWorkItems) +import Simplex.Messaging.Agent.Env.SQLite (Worker (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -228,14 +235,8 @@ processAgentMsgSndFile _corrId aFileId msg = do memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)] memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts') where - mConns' = mapMaybe useMember ms + mConns' = mapMaybe readyMemberConn ms sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts - -- Should match memberSendAction logic - useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}} - | (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) && not (connInactive conn) = - Just (groupMemberId, conn) - | otherwise = Nothing - useMember _ = Nothing _ -> pure () _ -> pure () -- TODO error? SFWARN e -> do @@ -898,12 +899,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- possible improvement is to choose scope based on event (some events specify scope) (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchAllErrors` \_ -> pure () - (fwdScopesMsgs, shouldDelConns) <- foldM (processAChatMsg gInfo' m' tags eInfo) (M.empty, False) aChatMsgs - when (isUserGrpFwdRelay gInfo') $ do - unless (blockedByAdmin m) $ - forM_ (M.assocs fwdScopesMsgs) $ \(groupForwardScope, fwdMsgs) -> - forwardMsgs groupForwardScope (L.reverse fwdMsgs) `catchAllErrors` eToView - when shouldDelConns $ deleteGroupConnections gInfo' True + newDeliveryTasks <- reverse <$> foldM (processAChatMsg gInfo' m' tags eInfo) [] aChatMsgs + shouldDelConns <- + if isUserGrpFwdRelay gInfo' && not (blockedByAdmin m) + then createDeliveryTasks gInfo' m' newDeliveryTasks + else pure False withRcpt <- checkSendRcpt $ rights aChatMsgs pure (withRcpt, shouldDelConns) where @@ -914,72 +914,69 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -> GroupMember -> TVar [Text] -> Text - -> (Map GroupForwardScope (NonEmpty (ChatMessage 'Json)), ShouldDeleteGroupConns) + -> [NewMessageDeliveryTask] -> Either String AChatMessage - -> CM (Map GroupForwardScope (NonEmpty (ChatMessage 'Json)), ShouldDeleteGroupConns) - processAChatMsg gInfo' m' tags eInfo (fwdScopeMap, shouldDelConns) = \case + -> CM [NewMessageDeliveryTask] + processAChatMsg gInfo' m' tags eInfo newDeliveryTasks = \case Right (ACMsg SJson chatMsg) -> do - (cmFwdScope_, cmShouldDelConns) <- - processEvent gInfo' m' tags eInfo chatMsg `catchAllErrors` \e -> eToView e $> (Nothing, False) - let fwdScopeMap' = - case cmFwdScope_ of - Nothing -> fwdScopeMap - Just cmFwdScope -> M.alter (Just . maybe [chatMsg] (chatMsg <|)) cmFwdScope fwdScopeMap - shouldDelConns' = shouldDelConns || cmShouldDelConns - pure (fwdScopeMap', shouldDelConns') + newTask_ <- processEvent gInfo' m' tags eInfo chatMsg `catchAllErrors` \e -> eToView e $> Nothing + pure $ maybe newDeliveryTasks (: newDeliveryTasks) newTask_ Right (ACMsg SBinary chatMsg) -> do void (processEvent gInfo' m' tags eInfo chatMsg) `catchAllErrors` \e -> eToView e - pure (fwdScopeMap, shouldDelConns) + pure newDeliveryTasks Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "group msg=error " <> eInfo <> " " <> tshow e eToView (ChatError . CEException $ "error parsing chat message: " <> e) - pure (fwdScopeMap, shouldDelConns) - processEvent :: forall e. MsgEncodingI e => GroupInfo -> GroupMember -> TVar [Text] -> Text -> ChatMessage e -> CM (Maybe GroupForwardScope, ShouldDeleteGroupConns) + pure newDeliveryTasks + processEvent :: forall e. MsgEncodingI e => GroupInfo -> GroupMember -> TVar [Text] -> Text -> ChatMessage e -> CM (Maybe NewMessageDeliveryTask) processEvent gInfo' m' tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do let tag = toCMEventTag chatMsgEvent atomically $ modifyTVar' tags (tshow tag :) logInfo $ "group msg=" <> tshow tag <> " " <> eInfo let body = chatMsgToBody chatMsg - (m'', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta body chatMsg - -- ! see isForwardedGroupMsg: processing functions should return GroupForwardScope for same events - case event of - XMsgNew mc -> memberCanSend m'' scope $ (,False) <$> newGroupContentMessage gInfo' m'' mc msg brokerTs False + (m'', conn', msg@RcvMessage {msgId, chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta body chatMsg + -- ! see isForwardedGroupMsg: processing functions should return DeliveryJobScope for same events + deliveryJobScope_ <- case event of + XMsgNew mc -> memberCanSend m'' scope $ newGroupContentMessage gInfo' m'' mc msg brokerTs False where ExtMsgContent {scope} = mcExtMsgContent mc -- file description is always allowed, to allow sending files to support scope - XMsgFileDescr sharedMsgId fileDescr -> (,False) <$> groupMessageFileDescription gInfo' m'' sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> memberCanSend m'' msgScope $ (,False) <$> groupMessageUpdate gInfo' m'' sharedMsgId mContent mentions msgScope msg brokerTs ttl live - XMsgDel sharedMsgId memberId scope_ -> (,False) <$> groupMessageDelete gInfo' m'' sharedMsgId memberId scope_ msg brokerTs - XMsgReact sharedMsgId (Just memberId) scope_ reaction add -> (,False) <$> groupMsgReaction gInfo' m'' sharedMsgId memberId scope_ reaction add msg brokerTs + XMsgFileDescr sharedMsgId fileDescr -> groupMessageFileDescription gInfo' m'' sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> memberCanSend m'' msgScope $ groupMessageUpdate gInfo' m'' sharedMsgId mContent mentions msgScope msg brokerTs ttl live + XMsgDel sharedMsgId memberId scope_ -> groupMessageDelete gInfo' m'' sharedMsgId memberId scope_ msg brokerTs + XMsgReact sharedMsgId (Just memberId) scope_ reaction add -> groupMsgReaction gInfo' m'' sharedMsgId memberId scope_ reaction add msg brokerTs -- TODO discontinue XFile - XFile fInv -> (Nothing, False) <$ processGroupFileInvitation' gInfo' m'' fInv msg brokerTs - XFileCancel sharedMsgId -> (,False) <$> xFileCancelGroup gInfo' m'' sharedMsgId - XFileAcptInv sharedMsgId fileConnReq_ fName -> (Nothing, False) <$ xFileAcptInvGroup gInfo' m'' sharedMsgId fileConnReq_ fName - XInfo p -> (,False) <$> xInfoMember gInfo' m'' p brokerTs - XGrpLinkMem p -> (Nothing, False) <$ xGrpLinkMem gInfo' m'' conn' p - XGrpLinkAcpt acceptance role memberId -> (Nothing, False) <$ xGrpLinkAcpt gInfo' m'' acceptance role memberId msg brokerTs - XGrpMemNew memInfo msgScope -> (,False) <$> xGrpMemNew gInfo' m'' memInfo msgScope msg brokerTs - XGrpMemIntro memInfo memRestrictions_ -> (Nothing, False) <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_ - XGrpMemInv memId introInv -> (Nothing, False) <$ xGrpMemInv gInfo' m'' memId introInv - XGrpMemFwd memInfo introInv -> (Nothing, False) <$ xGrpMemFwd gInfo' m'' memInfo introInv - XGrpMemRole memId memRole -> (,False) <$> xGrpMemRole gInfo' m'' memId memRole msg brokerTs - XGrpMemRestrict memId memRestrictions -> (,False) <$> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs - XGrpMemCon memId -> (Nothing, False) <$ xGrpMemCon gInfo' m'' memId + XFile fInv -> Nothing <$ processGroupFileInvitation' gInfo' m'' fInv msg brokerTs + XFileCancel sharedMsgId -> xFileCancelGroup gInfo' m'' sharedMsgId + XFileAcptInv sharedMsgId fileConnReq_ fName -> Nothing <$ xFileAcptInvGroup gInfo' m'' sharedMsgId fileConnReq_ fName + XInfo p -> xInfoMember gInfo' m'' p brokerTs + XGrpLinkMem p -> Nothing <$ xGrpLinkMem gInfo' m'' conn' p + XGrpLinkAcpt acceptance role memberId -> Nothing <$ xGrpLinkAcpt gInfo' m'' acceptance role memberId msg brokerTs + XGrpMemNew memInfo msgScope -> xGrpMemNew gInfo' m'' memInfo msgScope msg brokerTs + XGrpMemIntro memInfo memRestrictions_ -> Nothing <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_ + XGrpMemInv memId introInv -> Nothing <$ xGrpMemInv gInfo' m'' memId introInv + XGrpMemFwd memInfo introInv -> Nothing <$ xGrpMemFwd gInfo' m'' memInfo introInv + XGrpMemRole memId memRole -> xGrpMemRole gInfo' m'' memId memRole msg brokerTs + XGrpMemRestrict memId memRestrictions -> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs + XGrpMemCon memId -> Nothing <$ xGrpMemCon gInfo' m'' memId XGrpMemDel memId withMessages -> case encoding @e of SJson -> xGrpMemDel gInfo' m'' memId withMessages chatMsg msg brokerTs False - SBinary -> pure (Nothing, False) -- impossible - XGrpLeave -> (,False) <$> xGrpLeave gInfo' m'' msg brokerTs - XGrpDel -> (Just GFSAll, True) <$ xGrpDel gInfo' m'' msg brokerTs - XGrpInfo p' -> (,False) <$> xGrpInfo gInfo' m'' p' msg brokerTs - XGrpPrefs ps' -> (,False) <$> xGrpPrefs gInfo' m'' ps' + SBinary -> pure Nothing -- impossible + XGrpLeave -> xGrpLeave gInfo' m'' msg brokerTs + XGrpDel -> Just (DJSGroup {jobSpec = DJRelayRemoved}) <$ xGrpDel gInfo' m'' msg brokerTs + XGrpInfo p' -> xGrpInfo gInfo' m'' p' msg brokerTs + XGrpPrefs ps' -> xGrpPrefs gInfo' m'' ps' -- TODO [knocking] why don't we forward these messages? - XGrpDirectInv connReq mContent_ msgScope -> memberCanSend m'' msgScope $ (Nothing, False) <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs - XGrpMsgForward memberId memberName msg' msgTs -> (Nothing, False) <$ xGrpMsgForward gInfo' m'' memberId memberName msg' msgTs - XInfoProbe probe -> (Nothing, False) <$ xInfoProbe (COMGroupMember m'') probe - XInfoProbeCheck probeHash -> (Nothing, False) <$ xInfoProbeCheck (COMGroupMember m'') probeHash - XInfoProbeOk probe -> (Nothing, False) <$ xInfoProbeOk (COMGroupMember m'') probe - BFileChunk sharedMsgId chunk -> (Nothing, False) <$ bFileChunkGroup gInfo' sharedMsgId chunk msgMeta - _ -> (Nothing, False) <$ messageError ("unsupported message: " <> tshow event) + XGrpDirectInv connReq mContent_ msgScope -> memberCanSend m'' msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs + XGrpMsgForward memberId memberName msg' msgTs -> Nothing <$ xGrpMsgForward gInfo' m'' memberId memberName msg' msgTs brokerTs + XInfoProbe probe -> Nothing <$ xInfoProbe (COMGroupMember m'') probe + XInfoProbeCheck probeHash -> Nothing <$ xInfoProbeCheck (COMGroupMember m'') probeHash + XInfoProbeOk probe -> Nothing <$ xInfoProbeOk (COMGroupMember m'') probe + BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup gInfo' sharedMsgId chunk msgMeta + _ -> Nothing <$ messageError ("unsupported message: " <> tshow event) + forM deliveryJobScope_ $ \jobScope -> + -- TODO [channels fwd] XMsgNew to return messageFromChannel + pure $ NewMessageDeliveryTask {messageId = msgId, jobScope, messageFromChannel = False} checkSendRcpt :: [AChatMessage] -> CM Bool checkSendRcpt aMsgs = do currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo @@ -991,58 +988,34 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = hasDeliveryReceipt (toCMEventTag chatMsgEvent) - -- TODO forwardMsgs member retrieval can be further optimized: - -- - move remaining filters to SQL (memberCurrentOrPending, memberCurrent) - -- - create new GroupForwardScope for reports to avoid post-filtering moderators in msgsForwardedToMember - -- as an additional step, instead initially retrieve only moderators - -- (reuse getForwardIntroducedModerators, getForwardInvitedModerators + filters) - -- - new GroupForwardScope for excluding members on XGrpMemRestrict - forwardMsgs :: GroupForwardScope -> NonEmpty (ChatMessage 'Json) -> CM () - forwardMsgs groupForwardScope fwdMsgs = do - ms <- buildMemberList - let GroupMember {memberId} = m - memberName = Just $ memberShortenedName m - events = L.map (\cm -> XGrpMsgForward memberId memberName cm brokerTs) fwdMsgs - unless (null ms) $ void $ sendGroupMessages_ user gInfo ms events + createDeliveryTasks :: GroupInfo -> GroupMember -> [NewMessageDeliveryTask] -> CM ShouldDeleteGroupConns + createDeliveryTasks gInfo'@GroupInfo {groupId = gId} m' newDeliveryTasks = do + let relayRemovedTask_ = find (\NewMessageDeliveryTask {jobScope} -> isRelayRemoved jobScope) newDeliveryTasks + createdDeliveryTasks <- case relayRemovedTask_ of + Nothing -> do + withStore' $ \db -> + forM_ newDeliveryTasks $ \newTask -> + createMsgDeliveryTask db gInfo' m' newTask + pure newDeliveryTasks + Just relayRemovedTask -> do + -- if relay is removed, delete all other tasks and jobs + withStore' $ \db -> do + deleteGroupDeliveryTasks db gInfo' + deleteGroupDeliveryJobs db gInfo' + createMsgDeliveryTask db gInfo' m' relayRemovedTask + pure [relayRemovedTask] + lift $ forM_ (uniqueWorkerScopes createdDeliveryTasks) $ \workerScope -> + getDeliveryTaskWorker True (gId, workerScope) + pure $ isJust relayRemovedTask_ where - buildMemberList = case groupForwardScope of - GFSAll -> do - ms <- getAllIntroducedAndInvited - pure $ filter (\mem -> memberCurrentOrPending mem && msgsForwardedToMember fwdMsgs mem) ms - GFSMain -> do - ms <- getAllIntroducedAndInvited - pure $ filter (\mem -> memberCurrent mem && msgsForwardedToMember fwdMsgs mem) ms - GFSMemberSupport scopeGMId -> do - -- moderators introduced to this invited member - introducedModMs <- - if memberCategory m == GCInviteeMember - then withStore' $ \db -> getForwardIntroducedModerators db vr user m - else pure [] - -- invited moderators to which this member was introduced - invitedModMs <- withStore' $ \db -> getForwardInvitedModerators db vr user m - let modMs = introducedModMs <> invitedModMs - moderatorFilter mem = - memberCurrent mem - && maxVersion (memberChatVRange mem) >= groupKnockingVersion - && msgsForwardedToMember fwdMsgs mem - modMs' = filter moderatorFilter modMs - if scopeGMId == groupMemberId' m - then pure modMs' - else - withStore' (\db -> getForwardScopeMember db vr user m scopeGMId) >>= \case - Just scopeMem | msgsForwardedToMember fwdMsgs scopeMem -> pure $ scopeMem : modMs' - _ -> pure modMs' + uniqueWorkerScopes :: [NewMessageDeliveryTask] -> [DeliveryWorkerScope] + uniqueWorkerScopes createdDeliveryTasks = + let workerScopes = map (\NewMessageDeliveryTask {jobScope} -> toWorkerScope jobScope) createdDeliveryTasks + in foldr' addWorkerScope [] workerScopes where - getAllIntroducedAndInvited = do - ChatConfig {highlyAvailable} <- asks config - -- members introduced to this invited member - introducedMembers <- - if memberCategory m == GCInviteeMember - then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable - else pure [] - -- invited members to which this member was introduced - invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable - pure $ introducedMembers <> invitedMembers + addWorkerScope workerScope acc + | workerScope `elem` acc = acc + | otherwise = workerScope : acc RCVD msgMeta msgRcpt -> withAckMessage' "group rcvd" agentConnId msgMeta $ groupMsgReceived gInfo m conn msgMeta msgRcpt @@ -1484,13 +1457,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = memberCanSend :: GroupMember -> Maybe MsgScope -> - CM (Maybe GroupForwardScope, ShouldDeleteGroupConns) -> - CM (Maybe GroupForwardScope, ShouldDeleteGroupConns) + CM (Maybe DeliveryJobScope) -> + CM (Maybe DeliveryJobScope) memberCanSend m@GroupMember {memberRole} msgScope a = case msgScope of Just MSMember {} -> a Nothing | memberRole > GRObserver || memberPending m -> a - | otherwise -> messageError "member is not allowed to send messages" $> (Nothing, False) + | otherwise -> messageError "member is not allowed to send messages" $> Nothing processConnMERR :: ConnectionEntity -> Connection -> AgentErrorType -> CM () processConnMERR connEntity conn err = do @@ -1725,7 +1698,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure (fileId, aci) processFDMessage fileId aci fileDescr - groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> CM (Maybe GroupForwardScope) + groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> CM (Maybe DeliveryJobScope) groupMessageFileDescription g@GroupInfo {groupId} GroupMember {memberId} sharedMsgId fileDescr = do (fileId, aci) <- withStore $ \db -> do fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId @@ -1739,7 +1712,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- for example failure on not approved relays (CEFileNotApproved). -- we catch error, so that even if processFDMessage fails, message can still be forwarded. processFDMessage fileId aci fileDescr `catchAllErrors` \_ -> pure () - pure $ Just $ toGroupForwardScope g scopeInfo + pure $ Just $ infoToDeliveryScope g scopeInfo else messageError "x.msg.file.descr: file of another member" $> Nothing _ -> messageError "x.msg.file.descr: invalid file description part" $> Nothing @@ -1860,7 +1833,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = else pure Nothing mapM_ toView cEvt_ - groupMsgReaction :: GroupInfo -> GroupMember -> SharedMsgId -> MemberId -> Maybe MsgScope -> MsgReaction -> Bool -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) + groupMsgReaction :: GroupInfo -> GroupMember -> SharedMsgId -> MemberId -> Maybe MsgScope -> MsgReaction -> Bool -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) groupMsgReaction g m@GroupMember {memberRole} sharedMsgId itemMemberId scope_ reaction add RcvMessage {msgId} brokerTs | groupFeatureAllowed SGFReactions g = do rs <- withStore' $ \db -> getGroupReactions db g m itemMemberId sharedMsgId False @@ -1871,11 +1844,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | memberRole >= GRModerator || scopeMemberId == memberId' m -> withStore $ \db -> do liftIO $ setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs - Just . GFSMemberSupport <$> getScopeMemberIdViaMemberId db user g m scopeMemberId + Just . DJSMemberSupport <$> getScopeMemberIdViaMemberId db user g m scopeMemberId | otherwise -> pure Nothing Nothing -> do withStore' $ \db -> setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs - pure $ Just GFSMain + pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}} else pure Nothing | otherwise = pure Nothing where @@ -1892,7 +1865,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let ci' = CChatItem md ci {reactions} r = ACIReaction SCTGroup SMDRcv (GroupChat g scopeInfo) $ CIReaction (CIGroupRcv m) ci' brokerTs reaction toView $ CEvtChatItemReaction user add r - pure $ Just $ toGroupForwardScope g scopeInfo + pure $ Just $ infoToDeliveryScope g scopeInfo else pure Nothing reactionAllowed :: Bool -> MsgReaction -> [MsgReaction] -> Bool @@ -1904,9 +1877,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ChatErrorStore (SEChatItemSharedMsgIdNotFound sharedMsgId) -> handle sharedMsgId e -> throwError e - newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> CM (Maybe GroupForwardScope) + newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded = do - (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m msgScope_ + (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m content msgScope_ if blockedByAdmin m' then createBlockedByAdmin gInfo' m' scopeInfo $> Nothing else @@ -1920,7 +1893,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure Nothing Nothing -> do createContentItem gInfo' m' scopeInfo - pure $ Just $ toGroupForwardScope gInfo scopeInfo + pure $ Just $ infoToDeliveryScope gInfo scopeInfo where rejected gInfo' m' scopeInfo f = newChatItem gInfo' m' scopeInfo (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False timed' gInfo' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo' itemTTL @@ -1966,7 +1939,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo' memberId sharedMsgId) sharedMsgId_ groupMsgToView cInfo ci' {reactions} - groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> Maybe MsgScope -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM (Maybe GroupForwardScope) + groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> Maybe MsgScope -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM (Maybe DeliveryJobScope) groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc mentions msgScope_ msg@RcvMessage {msgId} brokerTs ttl_ live_ | prohibitedSimplexLinks gInfo m ft_ = messageWarning ("x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks) $> Nothing @@ -1977,14 +1950,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- Chat item and update message which created it will have different sharedMsgId in this case... let timed_ = rcvGroupCITimed gInfo ttl_ mentions' = if memberBlocked m then [] else mentions - (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m msgScope_ + (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m mc msgScope_ (ci, cInfo) <- saveRcvChatItem' user (CDGroupRcv gInfo' scopeInfo m') msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions' ci' <- withStore' $ \db -> do createChatItemVersion db (chatItemId' ci) brokerTs mc updateGroupChatItem db user groupId ci content True live Nothing ci'' <- blockedMemberCI gInfo' m' ci' toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci'') - pure $ Just $ toGroupForwardScope gInfo scopeInfo + pure $ Just $ infoToDeliveryScope gInfo scopeInfo where content = CIRcvMsgContent mc ts@(_, ft_) = msgContentTexts mc @@ -2009,14 +1982,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupCIMentions db gInfo ci' ciMentions toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo scopeInfo) ci') startUpdatedTimedItemThread user (ChatRef CTGroup groupId $ toChatScope <$> scopeInfo) ci ci' - pure $ Just $ toGroupForwardScope gInfo scopeInfo + pure $ Just $ infoToDeliveryScope gInfo scopeInfo else do toView $ CEvtChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo scopeInfo) ci) pure Nothing else messageError "x.msg.update: group member attempted to update a message of another member" $> Nothing _ -> messageError "x.msg.update: group member attempted invalid message update" $> Nothing - groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) + groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) groupMessageDelete gInfo@GroupInfo {membership} m@GroupMember {memberId, memberRole = senderRole} sharedMsgId sndMemberId_ scope_ RcvMessage {msgId} brokerTs = do let msgMemberId = fromMaybe memberId sndMemberId_ withStore' (\db -> runExceptT $ getGroupMemberCIBySharedMsgId db user gInfo msgMemberId sharedMsgId) >>= \case @@ -2045,32 +2018,32 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just (MSMember scopeMemberId) -> withStore $ \db -> do liftIO $ createCIModeration db gInfo m msgMemberId sharedMsgId msgId brokerTs - Just . GFSMemberSupport <$> getScopeMemberIdViaMemberId db user gInfo m scopeMemberId + Just . DJSMemberSupport <$> getScopeMemberIdViaMemberId db user gInfo m scopeMemberId Nothing -> do withStore' $ \db -> createCIModeration db gInfo m msgMemberId sharedMsgId msgId brokerTs - pure $ Just GFSMain + pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}} where - moderate :: GroupMember -> CChatItem 'CTGroup -> CM (Maybe GroupForwardScope) + moderate :: GroupMember -> CChatItem 'CTGroup -> CM (Maybe DeliveryJobScope) moderate mem cci = case sndMemberId_ of Just sndMemberId | sameMemberId sndMemberId mem -> checkRole mem $ do - groupForwardScope <- delete cci (Just m) + jobScope <- delete cci (Just m) archiveMessageReports cci m - pure $ Just groupForwardScope + pure $ Just jobScope | otherwise -> messageError "x.msg.del: message of another member with incorrect memberId" $> Nothing _ -> messageError "x.msg.del: message of another member without memberId" $> Nothing checkRole GroupMember {memberRole} a | senderRole < GRModerator || senderRole < memberRole = messageError "x.msg.del: message of another member with insufficient member permissions" $> Nothing | otherwise = a - delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM GroupForwardScope + delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM DeliveryJobScope delete cci byGroupMember = do scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci) deletions <- if groupFeatureMemberAllowed SGFFullDelete m gInfo then deleteGroupCIs user gInfo scopeInfo [cci] byGroupMember brokerTs else markGroupCIsDeleted user gInfo scopeInfo [cci] byGroupMember brokerTs toView $ CEvtChatItemsDeleted user deletions False False - pure $ toGroupForwardScope gInfo scopeInfo + pure $ infoToDeliveryScope gInfo scopeInfo archiveMessageReports :: CChatItem 'CTGroup -> GroupMember -> CM () archiveMessageReports (CChatItem _ ci) byMember = do ciIds <- withStore' $ \db -> markMessageReportsDeleted db user gInfo ci byMember brokerTs @@ -2211,7 +2184,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure () receiveFileChunk ft Nothing meta chunk - xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> CM (Maybe GroupForwardScope) + xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> CM (Maybe DeliveryJobScope) xFileCancelGroup g@GroupInfo {groupId} GroupMember {memberId} sharedMsgId = do (fileId, aci) <- withStore $ \db -> do fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId @@ -2225,7 +2198,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = unless (rcvFileCompleteOrCancelled ft) $ do cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync toView $ CEvtRcvFileSndCancelled user aci ft - pure $ Just $ toGroupForwardScope g scopeInfo + pure $ Just $ infoToDeliveryScope g scopeInfo else -- shouldn't happen now that query includes group member id messageError "x.file.cancel: group member attempted to cancel file of another member" $> Nothing @@ -2368,10 +2341,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Profile {displayName = n, fullName = fn, shortDescr = sd, image = i, contactLink = cl} = p Profile {displayName = n', fullName = fn', shortDescr = sd', image = i', contactLink = cl'} = p' - xInfoMember :: GroupInfo -> GroupMember -> Profile -> UTCTime -> CM (Maybe GroupForwardScope) + xInfoMember :: GroupInfo -> GroupMember -> Profile -> UTCTime -> CM (Maybe DeliveryJobScope) xInfoMember gInfo m p' brokerTs = do void $ processMemberProfileUpdate gInfo m p' True (Just brokerTs) - pure $ memberEventForwardScope m + pure $ memberEventDeliveryScope m xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> CM () xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do @@ -2391,7 +2364,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case Left _ -> messageError "x.grp.link.acpt error: referenced member does not exist" Right referencedMember -> do - (referencedMember', gInfo') <- withFastStore' $ \db -> do + (referencedMember', gInfo') <- withStore' $ \db -> do referencedMember' <- updateGroupMemberAccepted db user referencedMember (newMemberStatus referencedMember) role gInfo' <- updateGroupMembersRequireAttention db user gInfo referencedMember referencedMember' pure (referencedMember', gInfo') @@ -2750,7 +2723,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO show/log error, other events in SMP confirmation _ -> pure (conn', False) - xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) + xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _) msgScope_ msg brokerTs = do checkHostRole m memRole if sameMemberId memId (membership gInfo) @@ -2766,7 +2739,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure (updatedMember, gInfo') toView $ CEvtUnknownMemberAnnounced user gInfo' m unknownMember updatedMember memberAnnouncedToView updatedMember gInfo' - pure $ forwardScope updatedMember + pure $ deliveryJobScope updatedMember Right _ -> messageError "x.grp.mem.new error: member already exists" $> Nothing Left _ -> do (newMember, gInfo') <- withStore $ \db -> do @@ -2776,15 +2749,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = else pure gInfo pure (newMember, gInfo') memberAnnouncedToView newMember gInfo' - pure $ forwardScope newMember + pure $ deliveryJobScope newMember where initialStatus = case msgScope_ of Just (MSMember _) -> GSMemPendingReview _ -> GSMemAnnounced - forwardScope GroupMember {groupMemberId, memberStatus} + deliveryJobScope GroupMember {groupMemberId, memberStatus} | memberStatus == GSMemPendingApproval = Nothing - | memberStatus == GSMemPendingReview = Just $ GFSMemberSupport groupMemberId - | otherwise = Just GFSMain + | memberStatus == GSMemPendingReview = Just $ DJSMemberSupport groupMemberId + | otherwise = Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}} memberAnnouncedToView announcedMember@GroupMember {groupMemberId, memberProfile} gInfo' = do (announcedMember', scopeInfo) <- getMemNewChatScope announcedMember let event = RGEMemberAdded groupMemberId (fromLocalProfile memberProfile) @@ -2872,7 +2845,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = chatV = vr `peerConnChatVersion` mcvr withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode - xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) + xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg brokerTs | membershipMemId == memId = let gInfo' = gInfo {membership = membership {memberRole = memRole}} @@ -2892,13 +2865,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) groupMsgToView cInfo ci toView CEvtMemberRole {user, groupInfo = gInfo'', byMember = m', member = member {memberRole = memRole}, fromRole, toRole = memRole} - pure $ memberEventForwardScope member + pure $ memberEventDeliveryScope member checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = when (memberRole < GRAdmin || memberRole < memRole) $ throwChatError (CEGroupContactRole localDisplayName) - xGrpMemRestrict :: GroupInfo -> GroupMember -> MemberId -> MemberRestrictions -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) + xGrpMemRestrict :: GroupInfo -> GroupMember -> MemberId -> MemberRestrictions -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) xGrpMemRestrict gInfo@GroupInfo {membership = GroupMember {memberId = membershipMemId}} m@GroupMember {memberRole = senderRole} @@ -2906,9 +2879,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = MemberRestrictions {restriction} msg brokerTs - | membershipMemId == memId = - -- member shouldn't receive this message about themselves - messageError "x.grp.mem.restrict: admin blocks you" $> Nothing + | membershipMemId == memId = pure Nothing -- ignore - XGrpMemRestrict can be sent to restricted member for efficiency | otherwise = withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case Right bm@GroupMember {groupMemberId = bmId, memberRole, blockedByAdmin, memberProfile = bmp} @@ -2923,12 +2894,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs ciContent groupMsgToView cInfo ci toView CEvtMemberBlockedForAll {user, groupInfo = gInfo', byMember = m', member = bm', blocked} - pure $ memberEventForwardScope bm + pure $ memberEventDeliveryScope bm Left (SEGroupMemberNotFoundByMemberId _) -> do bm <- createUnknownMember gInfo memId Nothing bm' <- setMemberBlocked bm toView $ CEvtUnknownMemberBlocked user gInfo m bm' - pure $ Just GFSMain + pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = False}} Left e -> throwError $ ChatErrorStore e where setMemberBlocked bm = withStore' $ \db -> updateGroupMemberBlocked db user gInfo restriction bm @@ -2975,22 +2946,25 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> updateStatus introId GMIntroReConnected updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> ChatMessage 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe GroupForwardScope, ShouldDeleteGroupConns) + xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> ChatMessage 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages chatMsg msg brokerTs forwarded = do let GroupMember {memberId = membershipMemId} = membership if membershipMemId == memId then checkRole membership $ do deleteGroupLinkIfExists user gInfo - unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections gInfo False + -- TODO [channels fwd] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay + unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved let membership' = membership {memberStatus = GSMemRemoved} when withMessages $ deleteMessages gInfo membership' SMDSnd deleteMemberItem RGEUserDeleted toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages - pure (Just GFSAll, True) + pure $ Just DJSGroup {jobSpec = DJRelayRemoved} else withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case - Left _ -> messageError "x.grp.mem.del with unknown member ID" $> (Just GFSAll, False) + Left _ -> do + messageError "x.grp.mem.del with unknown member ID" + pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} Right deletedMember@GroupMember {groupMemberId, memberProfile} -> checkRole deletedMember $ do -- ? prohibit deleting member if it's the sender - sender should use x.grp.leave @@ -3007,11 +2981,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when withMessages $ deleteMessages gInfo' deletedMember' SMDRcv deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) toView $ CEvtDeletedMember user gInfo' m deletedMember' withMessages - pure (memberEventForwardScope deletedMember, False) + pure $ memberEventDeliveryScope deletedMember where checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = - messageError "x.grp.mem.del with insufficient member permissions" $> (Nothing, False) + messageError "x.grp.mem.del with insufficient member permissions" $> Nothing | otherwise = a deleteMemberItem gEvent = do (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m @@ -3032,13 +3006,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = isUserGrpFwdRelay GroupInfo {membership = GroupMember {memberRole}} = memberRole >= GRAdmin - deleteGroupConnections :: GroupInfo -> Bool -> CM () - deleteGroupConnections gInfo waitDelivery = do - -- member records are not deleted to keep history - members <- withStore' $ \db -> getGroupMembers db vr user gInfo - deleteMembersConnections' user members waitDelivery - - xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) + xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) xGrpLeave gInfo m msg brokerTs = do deleteMemberConnection m -- member record is not deleted to allow creation of "member left" chat item @@ -3051,19 +3019,20 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEMemberLeft) groupMsgToView cInfo ci toView $ CEvtLeftMember user gInfo'' m' {memberStatus = GSMemLeft} - pure $ memberEventForwardScope m + pure $ memberEventDeliveryScope m xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg brokerTs = do when (memberRole /= GROwner) $ throwChatError $ CEGroupUserRole gInfo GROwner withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemGroupDeleted - unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections gInfo False + -- TODO [channels fwd] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay + unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo m (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEGroupDeleted) groupMsgToView cInfo ci toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' - xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) + xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) xGrpInfo g@GroupInfo {groupProfile = p, businessChat} m@GroupMember {memberRole} p' msg brokerTs | memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" $> Nothing | otherwise = do @@ -3078,12 +3047,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMsgToView cInfo ci createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'' Just _ -> updateGroupPrefs_ g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' - pure $ Just GFSAll + pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} - xGrpPrefs :: GroupInfo -> GroupMember -> GroupPreferences -> CM (Maybe GroupForwardScope) + xGrpPrefs :: GroupInfo -> GroupMember -> GroupPreferences -> CM (Maybe DeliveryJobScope) xGrpPrefs g m@GroupMember {memberRole} ps' | memberRole < GROwner = messageError "x.grp.prefs with insufficient member permissions" $> Nothing - | otherwise = updateGroupPrefs_ g m ps' $> Just GFSAll + | otherwise = updateGroupPrefs_ g m ps' $> Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} updateGroupPrefs_ :: GroupInfo -> GroupMember -> GroupPreferences -> CM () updateGroupPrefs_ g@GroupInfo {groupProfile = p} m ps' = @@ -3183,8 +3152,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toViewTE $ TEContactVerificationReset user ct createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing - xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> Maybe ContactName -> ChatMessage 'Json -> UTCTime -> CM () - xGrpMsgForward gInfo@GroupInfo {groupId} m@GroupMember {memberRole, localDisplayName} memberId memberName chatMsg msgTs = do + xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> Maybe ContactName -> ChatMessage 'Json -> UTCTime -> UTCTime -> CM () + xGrpMsgForward gInfo@GroupInfo {groupId} m@GroupMember {memberRole, localDisplayName} memberId memberName chatMsg msgTs brokerTs = do when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole localDisplayName) withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case Right author -> processForwardedMsg author @@ -3198,13 +3167,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processForwardedMsg :: GroupMember -> CM () processForwardedMsg author = do let body = chatMsgToBody chatMsg - rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg + rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg brokerTs case event of - XMsgNew mc -> void $ memberCanSend author scope $ (,False) <$> newGroupContentMessage gInfo author mc rcvMsg msgTs True + XMsgNew mc -> void $ memberCanSend author scope $ (const Nothing) <$> newGroupContentMessage gInfo author mc rcvMsg msgTs True where ExtMsgContent {scope} = mcExtMsgContent mc -- file description is always allowed, to allow sending files to support scope XMsgFileDescr sharedMsgId fileDescr -> void $ groupMessageFileDescription gInfo author sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> void $ memberCanSend author msgScope $ (,False) <$> groupMessageUpdate gInfo author sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live + XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> void $ memberCanSend author msgScope $ (const Nothing) <$> groupMessageUpdate gInfo author sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live XMsgDel sharedMsgId memId scope_ -> void $ groupMessageDelete gInfo author sharedMsgId memId scope_ rcvMsg msgTs XMsgReact sharedMsgId (Just memId) scope_ reaction add -> void $ groupMsgReaction gInfo author sharedMsgId memId scope_ reaction add rcvMsg msgTs XFileCancel sharedMsgId -> void $ xFileCancelGroup gInfo author sharedMsgId @@ -3305,3 +3274,217 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = else pure Nothing else pure Nothing _ -> pure Nothing + +deleteGroupConnections :: User -> GroupInfo -> Bool -> CM () +deleteGroupConnections user gInfo waitDelivery = do + vr <- chatVersionRange + -- member records are not deleted to keep history + members <- withStore' $ \db -> getGroupMembers db vr user gInfo + deleteMembersConnections' user members waitDelivery + +startDeliveryTaskWorkers :: CM () +startDeliveryTaskWorkers = do + workerScopes <- withStore' $ \db -> getPendingDeliveryTaskScopes db + lift $ forM_ workerScopes resumeDeliveryTaskWork + +resumeDeliveryTaskWork :: DeliveryWorkerKey -> CM' () +resumeDeliveryTaskWork = void . getDeliveryTaskWorker False + +getDeliveryTaskWorker :: Bool -> DeliveryWorkerKey -> CM' Worker +getDeliveryTaskWorker hasWork deliveryKey = do + ws <- asks deliveryTaskWorkers + a <- asks smpAgent + getAgentWorker "delivery_task" hasWork a deliveryKey ws $ + runDeliveryTaskWorker a deliveryKey + +runDeliveryTaskWorker :: AgentClient -> DeliveryWorkerKey -> Worker -> CM () +runDeliveryTaskWorker a deliveryKey Worker {doWork} = do + vr <- chatVersionRange + -- TODO [channels fwd] in future may be required to read groupInfo and user on each iteration for up to date state + -- TODO - same for delivery jobs (runDeliveryJobWorker) + gInfo <- withStore $ \db -> do + user <- getUserByGroupId db groupId + getGroupInfo db vr user groupId + forever $ do + lift $ waitForWork doWork + runDeliveryTaskOperation vr gInfo + where + (groupId, workerScope) = deliveryKey + runDeliveryTaskOperation :: VersionRangeChat -> GroupInfo -> CM () + runDeliveryTaskOperation vr gInfo = do + withWork_ a doWork (withStore' $ \db -> getNextDeliveryTask db deliveryKey) $ \task -> + processDeliveryTask task + `catchAllErrors` \e -> do + withStore' $ \db -> setDeliveryTaskErrStatus db (deliveryTaskId task) (tshow e) + eToView e + where + processDeliveryTask :: MessageDeliveryTask -> CM () + processDeliveryTask task@MessageDeliveryTask {jobScope} = + case jobScopeImpliedSpec jobScope of + DJDeliveryJob _includePending -> + withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do + let (body, taskIds, largeTaskIds) = batchDeliveryTasks1 vr maxEncodedMsgLength nextTasks + withStore' $ \db -> do + createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body + forM_ taskIds $ \taskId -> updateDeliveryTaskStatus db taskId DTSProcessed + forM_ largeTaskIds $ \taskId -> setDeliveryTaskErrStatus db taskId "large" + lift . void $ getDeliveryJobWorker True deliveryKey + where + singleSenderGMId_ :: NonEmpty MessageDeliveryTask -> Maybe GroupMemberId + singleSenderGMId_ (MessageDeliveryTask {senderGMId = senderGMId'} :| ts) + | all (\MessageDeliveryTask {senderGMId} -> senderGMId == senderGMId') ts = Just senderGMId' + | otherwise = Nothing + DJRelayRemoved + | workerScope /= DWSGroup -> + throwChatError $ CEInternalError "delivery task worker: relay removed task in wrong worker scope" + | otherwise -> do + let MessageDeliveryTask {senderGMId, senderMemberId, senderMemberName, brokerTs, chatMessage} = task + fwdEvt = XGrpMsgForward senderMemberId (Just senderMemberName) chatMessage brokerTs + cm = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent = fwdEvt} + body = chatMsgToBody cm + withStore' $ \db -> do + createMsgDeliveryJob db gInfo jobScope (Just senderGMId) body + updateDeliveryTaskStatus db (deliveryTaskId task) DTSProcessed + lift . void $ getDeliveryJobWorker True deliveryKey + +startDeliveryJobWorkers :: CM () +startDeliveryJobWorkers = do + workerScopes <- withStore' $ \db -> getPendingDeliveryJobScopes db + lift $ forM_ workerScopes resumeDeliveryJobWork + +resumeDeliveryJobWork :: DeliveryWorkerKey -> CM' () +resumeDeliveryJobWork = void . getDeliveryJobWorker False + +getDeliveryJobWorker :: Bool -> DeliveryWorkerKey -> CM' Worker +getDeliveryJobWorker hasWork deliveryKey = do + ws <- asks deliveryJobWorkers + a <- asks smpAgent + getAgentWorker "delivery_job" hasWork a deliveryKey ws $ + runDeliveryJobWorker a deliveryKey + +runDeliveryJobWorker :: AgentClient -> DeliveryWorkerKey -> Worker -> CM () +runDeliveryJobWorker a deliveryKey Worker {doWork} = do + vr <- chatVersionRange + (user, gInfo) <- withStore $ \db -> do + user <- getUserByGroupId db groupId + gInfo <- getGroupInfo db vr user groupId + pure (user, gInfo) + forever $ do + lift $ waitForWork doWork + runDeliveryJobOperation vr user gInfo + where + (groupId, workerScope) = deliveryKey + runDeliveryJobOperation :: VersionRangeChat -> User -> GroupInfo -> CM () + runDeliveryJobOperation vr user gInfo = do + withWork_ a doWork (withStore' $ \db -> getNextDeliveryJob db deliveryKey) $ \job -> + processDeliveryJob job + `catchAllErrors` \e -> do + withStore' $ \db -> setDeliveryJobErrStatus db (deliveryJobId job) (tshow e) + eToView e + where + processDeliveryJob :: MessageDeliveryJob -> CM () + processDeliveryJob job = + case jobScopeImpliedSpec jobScope of + DJDeliveryJob _includePending -> do + sendBodyToMembers + withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete + DJRelayRemoved + | workerScope /= DWSGroup -> + throwChatError $ CEInternalError "delivery job worker: relay removed job in wrong worker scope" + | otherwise -> do + sendBodyToMembers + deleteGroupConnections user gInfo True + withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete + where + MessageDeliveryJob {jobId, jobScope, singleSenderGMId_, body, cursorGMId_ = startingCursor} = job + sendBodyToMembers :: CM () + sendBodyToMembers + | useRelays gInfo = -- channel + case jobScope of + -- there's no member review in channels, so job spec includePending is ignored + DJSGroup {} -> sendLoop startingCursor + where + dbBatchSize = 1000 -- TODO [channels fwd] review, make configurable + sendLoop :: Maybe GroupMemberId -> CM () + sendLoop cursorGMId_ = do + mems <- withStore' $ \db -> getGroupMembersByCursor db vr user gInfo cursorGMId_ singleSenderGMId_ dbBatchSize + let cursorGMId_' = groupMemberId' $ last mems + unless (null mems) $ deliver body mems + withStore' $ \db -> updateDeliveryJobCursor db jobId cursorGMId_' + unless (length mems < dbBatchSize) $ sendLoop (Just cursorGMId_') + DJSMemberSupport scopeGMId -> do + -- for member support scope we just load all recipients in one go, without cursor + modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo + let moderatorFilter mem = + memberCurrent mem + && maxVersion (memberChatVRange mem) >= groupKnockingVersion + && Just (groupMemberId' mem) /= singleSenderGMId_ + modMs' = filter moderatorFilter modMs + mems <- + if Just scopeGMId == singleSenderGMId_ + then pure modMs' + else do + scopeMem <- withStore $ \db -> getGroupMemberById db vr user scopeGMId + pure $ scopeMem : modMs' + unless (null mems) $ deliver body mems + | otherwise = -- fully connected group + case singleSenderGMId_ of + Nothing -> throwChatError $ CEInternalError "delivery job worker: singleSenderGMId is required when not using relays" + Just singleSenderGMId -> do + sender <- withStore $ \db -> getGroupMemberById db vr user singleSenderGMId + mems <- buildMemberList sender + unless (null mems) $ deliver body mems + where + buildMemberList sender = case jobScope of + DJSGroup {jobSpec} + | jobSpecImpliedPending jobSpec -> + filter memberCurrentOrPending <$> getAllIntroducedAndInvited + | otherwise -> + filter memberCurrent <$> getAllIntroducedAndInvited + DJSMemberSupport scopeGMId -> do + -- moderators introduced to this invited member + introducedModMs <- + if memberCategory sender == GCInviteeMember + then withStore' $ \db -> getForwardIntroducedModerators db vr user sender + else pure [] + -- invited moderators to which this member was introduced + invitedModMs <- withStore' $ \db -> getForwardInvitedModerators db vr user sender + let modMs = introducedModMs <> invitedModMs + modMs' = filter (\mem -> memberCurrent mem && maxVersion (memberChatVRange mem) >= groupKnockingVersion) modMs + if scopeGMId == groupMemberId' sender + then pure modMs' + else + withStore' (\db -> getForwardScopeMember db vr user sender scopeGMId) >>= \case + Just scopeMem -> pure $ scopeMem : modMs' + _ -> pure modMs' + where + getAllIntroducedAndInvited = do + ChatConfig {highlyAvailable} <- asks config + -- members introduced to this invited member + introducedMembers <- + if memberCategory sender == GCInviteeMember + then withStore' $ \db -> getForwardIntroducedMembers db vr user sender highlyAvailable + else pure [] + -- invited members to which this member was introduced + invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user sender highlyAvailable + pure $ introducedMembers <> invitedMembers + where + deliver :: ByteString -> [GroupMember] -> CM () + deliver msgBody mems = + let mConns = mapMaybe (fmap snd . readyMemberConn) mems + msgReqs = foldMemConns mConns + in void $ withAgent (`sendMessages` msgReqs) + where + foldMemConns :: [Connection] -> [MsgReq] + foldMemConns mConns = snd $ foldr' addReq (lastMemIdx_, []) mConns + where + lastMemIdx_ = let len = length mConns in if len > 1 then Just len else Nothing + addReq :: Connection -> (Maybe Int, [MsgReq]) -> (Maybe Int, [MsgReq]) + addReq conn (memIdx_, reqs) = + (subtract 1 <$> memIdx_, req : reqs) + where + req = (aConnId conn, PQEncOff, MsgFlags False, vrValue_) + vrValue_ = case memIdx_ of + Nothing -> VRValue Nothing msgBody -- sending to one member, do not reference body + Just 1 -> VRValue (Just 1) msgBody + Just _ -> VRRef 1 diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 5a5f0922e7..4973e5b9bb 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -31,7 +31,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace) import Data.Int (Int64) import Data.Kind (Constraint) -import Data.List.NonEmpty (NonEmpty) +import Data.List.NonEmpty (NonEmpty (..)) import Data.Map.Strict (Map) import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) @@ -185,24 +185,6 @@ toMsgScope :: GroupInfo -> GroupChatScopeInfo -> MsgScope toMsgScope GroupInfo {membership} = \case GCSIMemberSupport {groupMember_} -> MSMember $ memberId' $ fromMaybe membership groupMember_ -data GroupForwardScope - = GFSAll -- message should be forwarded to all group members, even pending (e.g. XGrpDel, XGrpInfo) - | GFSMain -- message should be forwarded to current group members only (e.g. regular messages in group) - | GFSMemberSupport GroupMemberId - deriving (Eq, Ord, Show) - -toGroupForwardScope :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupForwardScope -toGroupForwardScope GroupInfo {membership} = \case - Nothing -> GFSMain - Just GCSIMemberSupport {groupMember_} -> GFSMemberSupport $ groupMemberId' $ fromMaybe membership groupMember_ - -memberEventForwardScope :: GroupMember -> Maybe GroupForwardScope -memberEventForwardScope m@GroupMember {memberRole, memberStatus} - | memberStatus == GSMemPendingApproval = Nothing - | memberStatus == GSMemPendingReview = Just $ GFSMemberSupport $ groupMemberId' m - | memberRole >= GRModerator = Just GFSAll - | otherwise = Just GFSMain - chatInfoToRef :: ChatInfo c -> Maybe ChatRef chatInfoToRef = \case DirectChat Contact {contactId} -> Just $ ChatRef CTDirect contactId Nothing @@ -1163,7 +1145,8 @@ data SndMessage = SndMessage data NewRcvMessage e = NewRcvMessage { chatMsgEvent :: ChatMsgEvent e, - msgBody :: MsgBody + msgBody :: MsgBody, + brokerTs :: UTCTime } deriving (Show) diff --git a/src/Simplex/Chat/Messages/Batch.hs b/src/Simplex/Chat/Messages/Batch.hs index c1c45d7b0a..2c3bd2b87d 100644 --- a/src/Simplex/Chat/Messages/Batch.hs +++ b/src/Simplex/Chat/Messages/Batch.hs @@ -7,13 +7,21 @@ module Simplex.Chat.Messages.Batch ( MsgBatch (..), batchMessages, + batchDeliveryTasks1, ) where import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import Data.Int (Int64) +import Data.List (foldl') +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.List.NonEmpty as L import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..)) +import Simplex.Chat.Delivery import Simplex.Chat.Messages +import Simplex.Chat.Protocol +import Simplex.Chat.Types (VersionRangeChat) data MsgBatch = MsgBatch ByteString [SndMessage] @@ -49,3 +57,38 @@ batchMessages maxLen = addBatch . foldr addToBatch ([], [], 0, 0) [msg] -> body msg msgs -> B.concat ["[", B.intercalate "," (map body msgs), "]"] body SndMessage {msgBody} = msgBody + +-- | Batches delivery tasks into (batch, [taskIds], [largeTaskIds]). +batchDeliveryTasks1 :: VersionRangeChat -> Int -> NonEmpty MessageDeliveryTask -> (ByteString, [Int64], [Int64]) +batchDeliveryTasks1 vr maxLen = toResult . foldl' addToBatch ([], [], [], 0, 0) . L.toList + where + addToBatch :: ([ByteString], [Int64], [Int64], Int, Int) -> MessageDeliveryTask -> ([ByteString], [Int64], [Int64], Int, Int) + addToBatch (msgBodies, taskIds, largeTaskIds, len, n) task + -- too large: skip msgBody, record taskId in largeTaskIds + | msgLen > maxLen = (msgBodies, taskIds, taskId : largeTaskIds, len, n) + -- fits: include in batch + | batchLen <= maxLen = (msgBody : msgBodies, taskId : taskIds, largeTaskIds, len', n + 1) + -- doesn’t fit: stop adding further messages + | otherwise = (msgBodies, taskIds, largeTaskIds, len, n) + where + MessageDeliveryTask {taskId, senderMemberId, senderMemberName, brokerTs, chatMessage, messageFromChannel = _messageFromChannel} = task + -- TODO [channels fwd] handle messageFromChannel (null memberId in XGrpMsgForward) + msgBody = + let fwdEvt = XGrpMsgForward senderMemberId (Just senderMemberName) chatMessage brokerTs + cm = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent = fwdEvt} + in chatMsgToBody cm + msgLen = B.length msgBody + len' + | n == 0 = msgLen + | otherwise = msgLen + len + 1 -- 1 accounts for comma + batchLen + | n == 0 = len' + | otherwise = len' + 2 -- 2 accounts for opening and closing brackets + toResult :: ([ByteString], [Int64], [Int64], Int, Int) -> (ByteString, [Int64], [Int64]) + toResult (msgBodies, taskIds, largeTaskIds, _, _) = + (encodeMessages (reverse msgBodies), reverse taskIds, reverse largeTaskIds) + encodeMessages :: [ByteString] -> ByteString + encodeMessages = \case + [] -> mempty + [msg] -> msg + msgs -> B.concat ["[", B.intercalate "," msgs, "]"] diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index d6b826491e..0916d18eab 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -34,11 +34,10 @@ import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w, w2c) import qualified Data.ByteString.Lazy.Char8 as LB import Data.Either (fromRight) -import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe, mapMaybe) +import Data.Maybe (fromMaybe) import Data.String import Data.Text (Text) import qualified Data.Text as T @@ -53,7 +52,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) -import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) import Simplex.Messaging.Compression (Compressed, compress1, decompress1) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -313,6 +312,8 @@ data ChatMessage e = ChatMessage data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMessage e) +type MessageFromChannel = Bool + data ChatMsgEvent (e :: MsgEncoding) where XMsgNew :: MsgContainer -> ChatMsgEvent 'Json XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr} -> ChatMsgEvent 'Json @@ -392,32 +393,6 @@ isForwardedGroupMsg ev = case ev of XGrpPrefs _ -> True _ -> False --- applied after building list of messages to forward and building list of group members to forward to, see Chat; --- --- this filters out members if any of forwarded events in batch is an XGrpMemRestrict event referring to them, --- but practically XGrpMemRestrict is not batched with other events so it wouldn't prevent forwarding of other events --- to these members; --- --- same for reports (MCReport) - they are not batched with other events, so we can safely filter out --- members with role less than moderator when forwarding -msgsForwardedToMember :: NonEmpty (ChatMessage 'Json) -> GroupMember -> Bool -msgsForwardedToMember fwdMsgs GroupMember {memberId, memberRole} = - (memberId `notElem` restrictMemberIds) && (not hasReport || memberRole >= GRModerator) - where - restrictMemberIds = mapMaybe restrictMemberId $ L.toList fwdMsgs - restrictMemberId :: ChatMessage 'Json -> Maybe MemberId - restrictMemberId ChatMessage {chatMsgEvent} = - case chatMsgEvent of - XGrpMemRestrict mId _ -> Just mId - _ -> Nothing - hasReport = any isReportEvent fwdMsgs - isReportEvent ChatMessage {chatMsgEvent} = - case chatMsgEvent of - XMsgNew mc -> case mcExtMsgContent mc of - ExtMsgContent {content = MCReport {}} -> True - _ -> False - _ -> False - data MsgReaction = MREmoji {emoji :: MREmojiChar} | MRUnknown {tag :: Text, json :: J.Object} deriving (Eq, Show) @@ -1222,6 +1197,9 @@ instance ToJSON (ChatMessage 'Json) where instance FromJSON (ChatMessage 'Json) where parseJSON v = appJsonToCM <$?> parseJSON v +instance FromField (ChatMessage 'Json) where + fromField = blobFieldDecoder J.eitherDecodeStrict' + data ContactShortLinkData = ContactShortLinkData { profile :: Profile, message :: Maybe MsgContent, diff --git a/src/Simplex/Chat/Store/Delivery.hs b/src/Simplex/Chat/Store/Delivery.hs new file mode 100644 index 0000000000..632231adef --- /dev/null +++ b/src/Simplex/Chat/Store/Delivery.hs @@ -0,0 +1,362 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TypeOperators #-} + +module Simplex.Chat.Store.Delivery + ( createMsgDeliveryTask, + deleteGroupDeliveryTasks, + deleteGroupDeliveryJobs, + getPendingDeliveryTaskScopes, + getNextDeliveryTask, + getNextDeliveryTasks, + updateDeliveryTaskStatus, + setDeliveryTaskErrStatus, + deleteDoneDeliveryTasks, + createMsgDeliveryJob, + getPendingDeliveryJobScopes, + getNextDeliveryJob, + updateDeliveryJobStatus, + setDeliveryJobErrStatus, + getGroupMembersByCursor, + updateDeliveryJobCursor, + deleteDoneDeliveryJobs, + ) +where + +import Control.Monad.Except +import Data.ByteString.Char8 (ByteString) +import Data.Either (rights) +import Data.Int (Int64) +import Data.Text (Text) +import Data.Time.Clock (UTCTime, getCurrentTime) +import Simplex.Chat.Delivery +import Simplex.Chat.Protocol hiding (Binary) +import Simplex.Chat.Store.Groups (getGroupMemberById) +import Simplex.Chat.Store.Shared +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Store.AgentStore (getWorkItem, getWorkItems, maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Util (firstRow') +#if defined(dbPostgres) +import Database.PostgreSQL.Simple (Only (..), (:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple (Only (..), (:.) (..)) +import Database.SQLite.Simple.QQ (sql) +#endif + +type DeliveryJobScopeRow = (DeliveryWorkerScope, Maybe DeliveryJobSpecTag, Maybe BoolInt, Maybe GroupMemberId) + +jobScopeRow_ :: DeliveryJobScope -> DeliveryJobScopeRow +jobScopeRow_ = \case + DJSGroup {jobSpec} -> case jobSpec of + DJDeliveryJob {includePending} -> (DWSGroup, Just DJSTDeliveryJob, Just (BI includePending), Nothing) + DJRelayRemoved -> (DWSGroup, Just DJSTRelayRemoved, Nothing, Nothing) + DJSMemberSupport {supportGMId} -> (DWSMemberSupport, Nothing, Nothing, Just supportGMId) + +toJobScope_ :: DeliveryJobScopeRow -> Maybe DeliveryJobScope +toJobScope_ = \case + (DWSGroup, Just DJSTDeliveryJob, Just (BI includePending), Nothing) -> Just $ DJSGroup {jobSpec = DJDeliveryJob {includePending}} + (DWSGroup, Just DJSTRelayRemoved, Nothing, Nothing) -> Just $ DJSGroup {jobSpec = DJRelayRemoved} + (DWSMemberSupport, Nothing, Nothing, Just supportGMId) -> Just $ DJSMemberSupport {supportGMId} + _ -> Nothing + +createMsgDeliveryTask :: DB.Connection -> GroupInfo -> GroupMember -> NewMessageDeliveryTask -> IO () +createMsgDeliveryTask db gInfo sender newTask = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + INSERT INTO delivery_tasks ( + group_id, + worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, + sender_group_member_id, message_id, message_from_channel, task_status, + created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?,?,?) + |] + ((Only groupId) :. jobScopeRow_ jobScope :. (groupMemberId' sender, messageId, BI messageFromChannel, DTSNew, currentTs, currentTs)) + where + GroupInfo {groupId} = gInfo + NewMessageDeliveryTask {messageId, jobScope, messageFromChannel} = newTask + +deleteGroupDeliveryTasks :: DB.Connection -> GroupInfo -> IO () +deleteGroupDeliveryTasks db GroupInfo {groupId} = + DB.execute db "DELETE FROM delivery_tasks WHERE group_id = ?" (Only groupId) + +deleteGroupDeliveryJobs :: DB.Connection -> GroupInfo -> IO () +deleteGroupDeliveryJobs db GroupInfo {groupId} = + DB.execute db "DELETE FROM delivery_jobs WHERE group_id = ?" (Only groupId) + +getPendingDeliveryTaskScopes :: DB.Connection -> IO [DeliveryWorkerKey] +getPendingDeliveryTaskScopes db = + DB.query + db + [sql| + SELECT DISTINCT group_id, worker_scope + FROM delivery_tasks + WHERE failed = 0 AND task_status = ? + |] + (Only DTSNew) + +getNextDeliveryTask :: DB.Connection -> DeliveryWorkerKey -> IO (Either StoreError (Maybe MessageDeliveryTask)) +getNextDeliveryTask db deliveryKey = do + getWorkItem "delivery task" getTaskId (getMsgDeliveryTask_ db) (markDeliveryTaskFailed_ db) + where + (groupId, workerScope) = deliveryKey + getTaskId :: IO (Maybe Int64) + getTaskId = + maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT delivery_task_id + FROM delivery_tasks + WHERE group_id = ? AND worker_scope = ? + AND failed = 0 AND task_status = ? + ORDER BY delivery_task_id ASC + LIMIT 1 + |] + (groupId, workerScope, DTSNew) + +type MessageDeliveryTaskRow = (Only Int64) :. DeliveryJobScopeRow :. (GroupMemberId, MemberId, ContactName, UTCTime, ChatMessage 'Json, BoolInt) + +getMsgDeliveryTask_ :: DB.Connection -> Int64 -> IO (Either StoreError MessageDeliveryTask) +getMsgDeliveryTask_ db taskId = + firstRow' toTask (SEDeliveryTaskNotFound taskId) $ + DB.query + db + [sql| + SELECT + t.delivery_task_id, + t.worker_scope, t.job_scope_spec_tag, t.job_scope_include_pending, t.job_scope_support_gm_id, + m.group_member_id, m.member_id, p.display_name, msg.broker_ts, msg.msg_body, t.message_from_channel + FROM delivery_tasks t + JOIN messages msg ON msg.message_id = t.message_id + JOIN group_members m ON m.group_member_id = t.sender_group_member_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + WHERE t.delivery_task_id = ? + |] + (Only taskId) + where + toTask :: MessageDeliveryTaskRow -> Either StoreError MessageDeliveryTask + toTask ((Only taskId') :. jobScopeRow :. (senderGMId, senderMemberId, senderMemberName, brokerTs, chatMessage, BI messageFromChannel)) = + case toJobScope_ jobScopeRow of + Just jobScope -> Right $ MessageDeliveryTask {taskId = taskId', jobScope, senderGMId, senderMemberId, senderMemberName, brokerTs, chatMessage, messageFromChannel} + Nothing -> Left $ SEInvalidDeliveryTask taskId' + +markDeliveryTaskFailed_ :: DB.Connection -> Int64 -> IO () +markDeliveryTaskFailed_ db taskId = + DB.execute db "UPDATE delivery_tasks SET failed = 1 where delivery_task_id = ?" (Only taskId) + +-- TODO [channels fwd] possible optimization is to read and add tasks to batch iteratively to avoid reading too many tasks +-- passed MessageDeliveryTask defines the jobScope to search for +getNextDeliveryTasks :: DB.Connection -> GroupInfo -> MessageDeliveryTask -> IO (Either StoreError [Either StoreError MessageDeliveryTask]) +getNextDeliveryTasks db gInfo task = + getWorkItems "message delivery task" getTaskIds (getMsgDeliveryTask_ db) (markDeliveryTaskFailed_ db) + where + GroupInfo {groupId, useRelays} = gInfo + MessageDeliveryTask {jobScope, senderGMId} = task + getTaskIds :: IO [Int64] + getTaskIds + | useRelays = + map fromOnly + <$> DB.query + db + [sql| + SELECT delivery_task_id + FROM delivery_tasks + WHERE group_id = ? + AND worker_scope = ? + AND job_scope_spec_tag IS NOT DISTINCT FROM ? + AND job_scope_include_pending IS NOT DISTINCT FROM ? + AND job_scope_support_gm_id IS NOT DISTINCT FROM ? + AND failed = 0 + AND task_status = ? + ORDER BY delivery_task_id ASC + |] + ((Only groupId) :. jobScopeRow_ jobScope :. (Only DTSNew)) + | otherwise = + -- For fully connected groups we guarantee a singleSenderGMId for a delivery job by additionally filtering + -- on sender_group_member_id here, so that the job can then retrieve less members as recipients, + -- optimizing for this single sender (see processDeliveryJob -> getForwardIntroducedMembers, etc.). + -- We do this optimization in the job to decrease load on admins using mobile devices for clients. + map fromOnly + <$> DB.query + db + [sql| + SELECT delivery_task_id + FROM delivery_tasks + WHERE group_id = ? + AND worker_scope = ? + AND job_scope_spec_tag IS NOT DISTINCT FROM ? + AND job_scope_include_pending IS NOT DISTINCT FROM ? + AND job_scope_support_gm_id IS NOT DISTINCT FROM ? + AND sender_group_member_id = ? + AND failed = 0 + AND task_status = ? + ORDER BY delivery_task_id ASC + |] + ((Only groupId) :. jobScopeRow_ jobScope :. (senderGMId, DTSNew)) + +updateDeliveryTaskStatus :: DB.Connection -> Int64 -> DeliveryTaskStatus -> IO () +updateDeliveryTaskStatus db taskId status = updateDeliveryTaskStatus_ db taskId status Nothing + +setDeliveryTaskErrStatus :: DB.Connection -> Int64 -> Text -> IO () +setDeliveryTaskErrStatus db taskId errReason = updateDeliveryTaskStatus_ db taskId DTSError (Just errReason) + +updateDeliveryTaskStatus_ :: DB.Connection -> Int64 -> DeliveryTaskStatus -> Maybe Text -> IO () +updateDeliveryTaskStatus_ db taskId status errReason_ = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE delivery_tasks SET task_status = ?, task_err_reason = ?, updated_at = ? WHERE delivery_task_id = ?" + (status, errReason_, currentTs, taskId) + +deleteDoneDeliveryTasks :: DB.Connection -> UTCTime -> IO () +deleteDoneDeliveryTasks db createdAtCutoff = do + DB.execute + db + [sql| + DELETE FROM delivery_tasks + WHERE created_at <= ? + AND (task_status IN (?,?) OR failed = 1) + |] + (createdAtCutoff, DTSProcessed, DTSError) + +createMsgDeliveryJob :: DB.Connection -> GroupInfo -> DeliveryJobScope -> Maybe GroupMemberId -> ByteString -> IO () +createMsgDeliveryJob db gInfo jobScope singleSenderGMId_ body = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + INSERT INTO delivery_jobs ( + group_id, + worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, + single_sender_group_member_id, body, job_status, created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?,?) + |] + ((Only groupId) :. jobScopeRow_ jobScope :. (singleSenderGMId_, Binary body, DJSPending, currentTs, currentTs)) + where + GroupInfo {groupId} = gInfo + +getPendingDeliveryJobScopes :: DB.Connection -> IO [DeliveryWorkerKey] +getPendingDeliveryJobScopes db = + DB.query + db + [sql| + SELECT DISTINCT group_id, worker_scope + FROM delivery_jobs + WHERE failed = 0 AND job_status = ? + |] + (Only DJSPending) + +type MessageDeliveryJobRow = (Only Int64) :. DeliveryJobScopeRow :. (Maybe GroupMemberId, Binary ByteString, Maybe GroupMemberId) + +getNextDeliveryJob :: DB.Connection -> DeliveryWorkerKey -> IO (Either StoreError (Maybe MessageDeliveryJob)) +getNextDeliveryJob db deliveryKey = do + getWorkItem "delivery job" getJobId getJob markJobFailed + where + (groupId, workerScope) = deliveryKey + getJobId :: IO (Maybe Int64) + getJobId = + maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT delivery_job_id + FROM delivery_jobs + WHERE group_id = ? AND worker_scope = ? + AND failed = 0 AND job_status = ? + ORDER BY delivery_job_id ASC + LIMIT 1 + |] + (groupId, workerScope, DJSPending) + getJob :: Int64 -> IO (Either StoreError MessageDeliveryJob) + getJob jobId = + firstRow' toDeliveryJob (SEDeliveryJobNotFound jobId) $ + DB.query + db + [sql| + SELECT + delivery_job_id, + worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, + single_sender_group_member_id, body, cursor_group_member_id + FROM delivery_jobs + WHERE delivery_job_id = ? + |] + (Only jobId) + where + toDeliveryJob :: MessageDeliveryJobRow -> Either StoreError MessageDeliveryJob + toDeliveryJob ((Only jobId') :. jobScopeRow :. (singleSenderGMId_, Binary body, cursorGMId_)) = + case toJobScope_ jobScopeRow of + Just jobScope -> Right $ MessageDeliveryJob {jobId = jobId', jobScope, singleSenderGMId_, body, cursorGMId_} + Nothing -> Left $ SEInvalidDeliveryJob jobId' + markJobFailed :: Int64 -> IO () + markJobFailed jobId = + DB.execute db "UPDATE delivery_jobs SET failed = 1 where delivery_job_id = ?" (Only jobId) + +updateDeliveryJobStatus :: DB.Connection -> Int64 -> DeliveryJobStatus -> IO () +updateDeliveryJobStatus db jobId status = updateDeliveryJobStatus_ db jobId status Nothing + +setDeliveryJobErrStatus :: DB.Connection -> Int64 -> Text -> IO () +setDeliveryJobErrStatus db jobId errReason = updateDeliveryJobStatus_ db jobId DJSError (Just errReason) + +updateDeliveryJobStatus_ :: DB.Connection -> Int64 -> DeliveryJobStatus -> Maybe Text -> IO () +updateDeliveryJobStatus_ db jobId status errReason_ = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE delivery_jobs SET job_status = ?, job_err_reason = ?, updated_at = ? WHERE delivery_job_id = ?" + (status, errReason_, currentTs, jobId) + +-- TODO [channels fwd] possible improvement is to prioritize owners and "active" members +getGroupMembersByCursor :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Maybe GroupMemberId -> Maybe GroupMemberId -> Int -> IO [GroupMember] +getGroupMembersByCursor db vr user GroupInfo {groupId} cursorGMId_ singleSenderGMId_ count = do + memberIds <- + map fromOnly <$> case cursorGMId_ of + Nothing -> + DB.query + db + (query <> orderLimit) + (groupId, singleSenderGMId_, GSMemIntroduced, GSMemIntroInvited, GSMemAccepted, GSMemAnnounced, GSMemConnected, GSMemComplete, count) + Just cursorGMId -> + DB.query + db + (query <> " AND group_member_id > ?" <> orderLimit) + (groupId, singleSenderGMId_, GSMemIntroduced, GSMemIntroInvited, GSMemAccepted, GSMemAnnounced, GSMemConnected, GSMemComplete, cursorGMId, count) + rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds + where + query = + [sql| + SELECT group_member_id + FROM group_members + WHERE group_id = ? + AND group_member_id IS DISTINCT FROM ? + AND member_status IN (?,?,?,?,?,?) + |] + orderLimit = " ORDER BY group_member_id ASC LIMIT ?" + +updateDeliveryJobCursor :: DB.Connection -> Int64 -> GroupMemberId -> IO () +updateDeliveryJobCursor db jobId cursorGMId = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE delivery_jobs SET cursor_group_member_id = ?, updated_at = ? WHERE delivery_job_id = ?" + (cursorGMId, currentTs, jobId) + +deleteDoneDeliveryJobs :: DB.Connection -> UTCTime -> IO () +deleteDoneDeliveryJobs db createdAtCutoff = do + DB.execute + db + [sql| + DELETE FROM delivery_jobs + WHERE created_at <= ? + AND (job_status IN (?,?) OR failed = 1) + |] + (createdAtCutoff, DJSComplete, DJSError) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 1b218df56d..3d8bdead72 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -175,7 +175,7 @@ import Data.Text (Text) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Messages -import Simplex.Chat.Protocol (MsgMention (..), groupForwardVersion) +import Simplex.Chat.Protocol hiding (Binary) import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared import Simplex.Chat.Types @@ -359,6 +359,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc pure GroupInfo { groupId, + useRelays = False, localDisplayName = ldn, groupProfile, localAlias = "", @@ -432,6 +433,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ pure ( GroupInfo { groupId, + useRelays = False, localDisplayName, groupProfile, localAlias = "", diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index c152bdc4bf..f6d0e7da72 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -286,7 +286,7 @@ getLastRcvMsgInfo db connId = RcvMsgInfo {msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta} createNewRcvMessage :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewRcvMessage e -> Maybe SharedMsgId -> Maybe GroupMemberId -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage -createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody} sharedMsgId_ authorMember forwardedByMember = +createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody, brokerTs} sharedMsgId_ authorMember forwardedByMember = case connOrGroupId of ConnectionId connId -> liftIO $ insertRcvMsg (Just connId) Nothing GroupId groupId -> case sharedMsgId_ of @@ -314,10 +314,12 @@ createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody} share db [sql| INSERT INTO messages - (msg_sent, chat_msg_event, msg_body, created_at, updated_at, connection_id, group_id, shared_msg_id, author_group_member_id, forwarded_by_group_member_id) - VALUES (?,?,?,?,?,?,?,?,?,?) + (msg_sent, chat_msg_event, msg_body, broker_ts, created_at, updated_at, connection_id, group_id, + shared_msg_id, author_group_member_id, forwarded_by_group_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - (MDRcv, toCMEventTag chatMsgEvent, DB.Binary msgBody, currentTs, currentTs, connId_, groupId_, sharedMsgId_, authorMember, forwardedByMember) + ((MDRcv, toCMEventTag chatMsgEvent, DB.Binary msgBody, brokerTs, currentTs, currentTs, connId_, groupId_) + :. (sharedMsgId_, authorMember, forwardedByMember)) msgId <- insertedRowId db pure RcvMessage {msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgBody, authorMember, forwardedByMember} @@ -509,17 +511,17 @@ setSupportChatTs db groupMemberId chatTs = setSupportChatMemberAttention :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> Int64 -> IO (GroupInfo, GroupMember) setSupportChatMemberAttention db vr user g m memberAttention = do - m' <- updateGMAttention m + m' <- updateGMAttention g' <- updateGroupMembersRequireAttention db user g m m' pure (g', m') where - updateGMAttention m@GroupMember {groupMemberId} = do + updateGMAttention = do currentTs <- getCurrentTime DB.execute db "UPDATE group_members SET support_chat_items_member_attention = ?, updated_at = ? WHERE group_member_id = ?" (memberAttention, currentTs, groupMemberId' m) - m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId + m_ <- runExceptT $ getGroupMemberById db vr user (groupMemberId' m) pure $ either (const m) id m_ -- Left shouldn't happen, but types require it createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId @@ -2037,7 +2039,7 @@ setDirectChatItemsDeleteAt db User {userId} contactId itemIds currentTs = forM i pure (chatItemId, deleteAt) updateGroupChatItemsRead :: DB.Connection -> User -> GroupInfo -> IO () -updateGroupChatItemsRead db User {userId} GroupInfo {groupId, membership} = do +updateGroupChatItemsRead db User {userId} GroupInfo {groupId} = do currentTs <- getCurrentTime DB.execute db @@ -2082,10 +2084,11 @@ updateSupportChatItemsRead db vr user@User {userId} g@GroupInfo {groupId, member UPDATE group_members SET support_chat_items_unread = 0, support_chat_items_member_attention = 0, - support_chat_items_mentions = 0 + support_chat_items_mentions = 0, + updated_at = ? WHERE group_member_id = ? |] - (Only groupMemberId) + (currentTs, groupMemberId) m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId pure $ either (const m) id m_ -- Left shouldn't happen, but types require it diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index ce1987db15..4fbed16753 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -16,6 +16,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250721_indexes import Simplex.Chat.Store.Postgres.Migrations.M20250729_member_contact_requests import Simplex.Chat.Store.Postgres.Migrations.M20250801_via_group_link_uri import Simplex.Chat.Store.Postgres.Migrations.M20250802_chat_peer_type +import Simplex.Chat.Store.Postgres.Migrations.M20250813_delivery_tasks import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -31,7 +32,8 @@ schemaMigrations = ("20250721_indexes", m20250721_indexes, Just down_m20250721_indexes), ("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests), ("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri), - ("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type) + ("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type), + ("20250813_delivery_tasks", m20250813_delivery_tasks, Just down_m20250813_delivery_tasks) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250813_delivery_tasks.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250813_delivery_tasks.hs new file mode 100644 index 0000000000..688c0aee76 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250813_delivery_tasks.hs @@ -0,0 +1,128 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250813_delivery_tasks where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250813_delivery_tasks :: Text +m20250813_delivery_tasks = + T.pack + [r| +CREATE TABLE delivery_tasks ( + delivery_task_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_id BIGINT NOT NULL REFERENCES groups ON DELETE CASCADE, + worker_scope TEXT NOT NULL, + job_scope_spec_tag TEXT, + job_scope_include_pending SMALLINT, + job_scope_support_gm_id BIGINT REFERENCES group_members(group_member_id) ON DELETE CASCADE, + sender_group_member_id BIGINT NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, + message_id BIGINT REFERENCES messages ON DELETE CASCADE, + message_from_channel SMALLINT NOT NULL DEFAULT 0, + task_status TEXT NOT NULL, + task_err_reason TEXT, + failed SMALLINT DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); + +CREATE INDEX idx_delivery_tasks_group_id ON delivery_tasks(group_id); +CREATE INDEX idx_delivery_tasks_job_scope_support_gm_id ON delivery_tasks(job_scope_support_gm_id); +CREATE INDEX idx_delivery_tasks_sender_group_member_id ON delivery_tasks(sender_group_member_id); +CREATE INDEX idx_delivery_tasks_message_id ON delivery_tasks(message_id); + +CREATE INDEX idx_delivery_tasks_next ON delivery_tasks( + group_id, + worker_scope, + failed, + task_status +); +CREATE INDEX idx_delivery_tasks_next_for_job_scope ON delivery_tasks( + group_id, + worker_scope, + job_scope_spec_tag, + job_scope_include_pending, + job_scope_support_gm_id, + failed, + task_status +); +CREATE INDEX idx_delivery_tasks_next_for_job_scope_sender ON delivery_tasks( + group_id, + worker_scope, + job_scope_spec_tag, + job_scope_include_pending, + job_scope_support_gm_id, + sender_group_member_id, + failed, + task_status +); +CREATE INDEX idx_delivery_tasks_created_at ON delivery_tasks(created_at); + + + +CREATE TABLE delivery_jobs ( + delivery_job_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_id BIGINT NOT NULL REFERENCES groups ON DELETE CASCADE, + worker_scope TEXT NOT NULL, + job_scope_spec_tag TEXT, + job_scope_include_pending SMALLINT, + job_scope_support_gm_id BIGINT REFERENCES group_members(group_member_id) ON DELETE CASCADE, + single_sender_group_member_id BIGINT REFERENCES group_members(group_member_id) ON DELETE CASCADE, + body BYTEA, + cursor_group_member_id BIGINT, + job_status TEXT NOT NULL, + job_err_reason TEXT, + failed SMALLINT DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT (now()), + updated_at TIMESTAMPTZ NOT NULL DEFAULT (now()) +); + +CREATE INDEX idx_delivery_jobs_group_id ON delivery_jobs(group_id); +CREATE INDEX idx_delivery_jobs_job_scope_support_gm_id ON delivery_jobs(job_scope_support_gm_id); +CREATE INDEX idx_delivery_jobs_single_sender_group_member_id ON delivery_jobs(single_sender_group_member_id); + +CREATE INDEX idx_delivery_jobs_next ON delivery_jobs( + group_id, + worker_scope, + failed, + job_status +); +CREATE INDEX idx_delivery_jobs_created_at ON delivery_jobs(created_at); + + + +ALTER TABLE messages ADD COLUMN broker_ts TIMESTAMPTZ; +|] + +down_m20250813_delivery_tasks :: Text +down_m20250813_delivery_tasks = + T.pack + [r| +ALTER TABLE messages DROP COLUMN broker_ts; + + + +DROP INDEX idx_delivery_jobs_group_id; +DROP INDEX idx_delivery_jobs_job_scope_support_gm_id; +DROP INDEX idx_delivery_jobs_single_sender_group_member_id; + +DROP INDEX idx_delivery_jobs_next; +DROP INDEX idx_delivery_jobs_created_at; + +DROP TABLE delivery_jobs; + + + +DROP INDEX idx_delivery_tasks_group_id; +DROP INDEX idx_delivery_tasks_job_scope_support_gm_id; +DROP INDEX idx_delivery_tasks_sender_group_member_id; +DROP INDEX idx_delivery_tasks_message_id; + +DROP INDEX idx_delivery_tasks_next; +DROP INDEX idx_delivery_tasks_next_for_job_scope; +DROP INDEX idx_delivery_tasks_next_for_job_scope_sender; +DROP INDEX idx_delivery_tasks_created_at; + +DROP TABLE delivery_tasks; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index c0560de376..695d7d393f 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -432,6 +432,66 @@ ALTER TABLE test_chat_schema.contacts ALTER COLUMN contact_id ADD GENERATED ALWA +CREATE TABLE test_chat_schema.delivery_jobs ( + delivery_job_id bigint NOT NULL, + group_id bigint NOT NULL, + worker_scope text NOT NULL, + job_scope_spec_tag text, + job_scope_include_pending smallint, + job_scope_support_gm_id bigint, + single_sender_group_member_id bigint, + body bytea, + cursor_group_member_id bigint, + job_status text NOT NULL, + job_err_reason text, + failed smallint DEFAULT 0, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + + +ALTER TABLE test_chat_schema.delivery_jobs ALTER COLUMN delivery_job_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME test_chat_schema.delivery_jobs_delivery_job_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + +CREATE TABLE test_chat_schema.delivery_tasks ( + delivery_task_id bigint NOT NULL, + group_id bigint NOT NULL, + worker_scope text NOT NULL, + job_scope_spec_tag text, + job_scope_include_pending smallint, + job_scope_support_gm_id bigint, + sender_group_member_id bigint NOT NULL, + message_id bigint, + message_from_channel smallint DEFAULT 0 NOT NULL, + task_status text NOT NULL, + task_err_reason text, + failed smallint DEFAULT 0, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + + +ALTER TABLE test_chat_schema.delivery_tasks ALTER COLUMN delivery_task_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME test_chat_schema.delivery_tasks_delivery_task_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + CREATE TABLE test_chat_schema.display_names ( user_id bigint NOT NULL, local_display_name text NOT NULL, @@ -707,7 +767,8 @@ CREATE TABLE test_chat_schema.messages ( shared_msg_id bytea, shared_msg_id_user smallint, author_group_member_id bigint, - forwarded_by_group_member_id bigint + forwarded_by_group_member_id bigint, + broker_ts timestamp with time zone ); @@ -1274,6 +1335,16 @@ ALTER TABLE ONLY test_chat_schema.contacts +ALTER TABLE ONLY test_chat_schema.delivery_jobs + ADD CONSTRAINT delivery_jobs_pkey PRIMARY KEY (delivery_job_id); + + + +ALTER TABLE ONLY test_chat_schema.delivery_tasks + ADD CONSTRAINT delivery_tasks_pkey PRIMARY KEY (delivery_task_id); + + + ALTER TABLE ONLY test_chat_schema.display_names ADD CONSTRAINT display_names_pkey PRIMARY KEY (user_id, local_display_name); @@ -1837,6 +1908,58 @@ CREATE INDEX idx_contacts_xcontact_id ON test_chat_schema.contacts USING btree ( +CREATE INDEX idx_delivery_jobs_created_at ON test_chat_schema.delivery_jobs USING btree (created_at); + + + +CREATE INDEX idx_delivery_jobs_group_id ON test_chat_schema.delivery_jobs USING btree (group_id); + + + +CREATE INDEX idx_delivery_jobs_job_scope_support_gm_id ON test_chat_schema.delivery_jobs USING btree (job_scope_support_gm_id); + + + +CREATE INDEX idx_delivery_jobs_next ON test_chat_schema.delivery_jobs USING btree (group_id, worker_scope, failed, job_status); + + + +CREATE INDEX idx_delivery_jobs_single_sender_group_member_id ON test_chat_schema.delivery_jobs USING btree (single_sender_group_member_id); + + + +CREATE INDEX idx_delivery_tasks_created_at ON test_chat_schema.delivery_tasks USING btree (created_at); + + + +CREATE INDEX idx_delivery_tasks_group_id ON test_chat_schema.delivery_tasks USING btree (group_id); + + + +CREATE INDEX idx_delivery_tasks_job_scope_support_gm_id ON test_chat_schema.delivery_tasks USING btree (job_scope_support_gm_id); + + + +CREATE INDEX idx_delivery_tasks_message_id ON test_chat_schema.delivery_tasks USING btree (message_id); + + + +CREATE INDEX idx_delivery_tasks_next ON test_chat_schema.delivery_tasks USING btree (group_id, worker_scope, failed, task_status); + + + +CREATE INDEX idx_delivery_tasks_next_for_job_scope ON test_chat_schema.delivery_tasks USING btree (group_id, worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, failed, task_status); + + + +CREATE INDEX idx_delivery_tasks_next_for_job_scope_sender ON test_chat_schema.delivery_tasks USING btree (group_id, worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, sender_group_member_id, failed, task_status); + + + +CREATE INDEX idx_delivery_tasks_sender_group_member_id ON test_chat_schema.delivery_tasks USING btree (sender_group_member_id); + + + CREATE INDEX idx_extra_xftp_file_descriptions_file_id ON test_chat_schema.extra_xftp_file_descriptions USING btree (file_id); @@ -2390,6 +2513,41 @@ ALTER TABLE ONLY test_chat_schema.contacts +ALTER TABLE ONLY test_chat_schema.delivery_jobs + ADD CONSTRAINT delivery_jobs_group_id_fkey FOREIGN KEY (group_id) REFERENCES test_chat_schema.groups(group_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.delivery_jobs + ADD CONSTRAINT delivery_jobs_job_scope_support_gm_id_fkey FOREIGN KEY (job_scope_support_gm_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.delivery_jobs + ADD CONSTRAINT delivery_jobs_single_sender_group_member_id_fkey FOREIGN KEY (single_sender_group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.delivery_tasks + ADD CONSTRAINT delivery_tasks_group_id_fkey FOREIGN KEY (group_id) REFERENCES test_chat_schema.groups(group_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.delivery_tasks + ADD CONSTRAINT delivery_tasks_job_scope_support_gm_id_fkey FOREIGN KEY (job_scope_support_gm_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.delivery_tasks + ADD CONSTRAINT delivery_tasks_message_id_fkey FOREIGN KEY (message_id) REFERENCES test_chat_schema.messages(message_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.delivery_tasks + ADD CONSTRAINT delivery_tasks_sender_group_member_id_fkey FOREIGN KEY (sender_group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; + + + ALTER TABLE ONLY test_chat_schema.display_names ADD CONSTRAINT display_names_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE; diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 3d5fc307ee..e501a7d627 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -139,6 +139,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250721_indexes import Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests import Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri import Simplex.Chat.Store.SQLite.Migrations.M20250802_chat_peer_type +import Simplex.Chat.Store.SQLite.Migrations.M20250813_delivery_tasks import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -277,7 +278,8 @@ schemaMigrations = ("20250721_indexes", m20250721_indexes, Just down_m20250721_indexes), ("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests), ("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri), - ("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type) + ("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type), + ("20250813_delivery_tasks", m20250813_delivery_tasks, Just down_m20250813_delivery_tasks) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs new file mode 100644 index 0000000000..dfafff674f --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs @@ -0,0 +1,172 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250813_delivery_tasks where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +-- TODO [channels fwd] add later in new migration for MemberProfileUpdate delivery jobs: +-- TODO - ALTER TABLE group_members ADD COLUMN last_profile_delivery_ts TEXT; +-- TODO - ALTER TABLE group_members ADD COLUMN join_ts TEXT; + +-- How columns correspond to types: + +-- both tables: +-- - <-> DeliveryWorkerKey = (GroupId, DeliveryWorkerScope), +-- - job_scope_spec_tag <-> Maybe DeliveryJobSpecTag (for DJSGroup job scope), +-- - job_scope_include_pending <-> Maybe Bool (for DJDeliveryJob job spec), +-- - job_scope_support_gm_id <-> Maybe GroupMemberId (for DJSMemberSupport job scope), +-- - failed <-> Bool (for internal worker use, to mark failed work items). + +-- delivery_tasks table: +-- - sender_group_member_id <-> GroupMemberId (sender of the original message that created task), +-- - message_id <-> MessageId (reference to the original message that created task), +-- - message_from_channel <-> Maybe MessageFromChannel (for MessageDeliveryTask), +-- - task_status <-> DeliveryTaskStatus, +-- - task_err_reason <-> Maybe Text (set when task status is DTSError, not encoded in status to allow filtering by DTSError in queries). + +-- delivery_jobs table: +-- - single_sender_group_member_id <-> Maybe GroupMemberId (set when all messages in job's delivery body are from the same sender), +-- - body <-> ByteString (JSON encoded batch of messages or single message packed with forwarding metadata (XGrpMsgForward)), +-- - cursor_group_member_id <-> Maybe GroupMemberId (for tracking progress of job processing buckets of recipient members), +-- - job_status <-> DeliveryJobStatus, +-- - job_err_reason <-> Maybe Text (set when job status is DJSError, not encoded in status to allow filtering by DJSError in queries). +-- +-- Pair of columns defines the scope of work for a worker. +-- +-- Both tasks and jobs are defined by the same range of worker scopes, with a delivery task worker +-- converting tasks into jobs for a delivery job worker of the same scope. +-- Each group can have up to 1 task worker and 1 job worker for each worker scope. +-- See DeliveryWorkerScope. +-- +-- Columns job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id narrow down delivery scope for a job. +-- See DeliveryJobScope. +-- +-- In some cases multiple tasks of the same job scope may be converted into a single job. +-- For example, messages referenced in DJDeliveryJob tasks of the same scope/spec can be batched for a single delivery. +-- Some tasks, for example of DJRelayRemoved specialization, are converted one-to-one into jobs. +-- +-- Delivery scopes can be expanded to create more specialized jobs. +-- For example DWSMemberProfileUpdate workers are planned to deliver senders' profiles to +-- all members satisfying criteria: sender last_profile_delivery_ts < recipient join_ts. +-- Tasks for these jobs are planned to be created based on sender and group state, rather than per message, +-- so member profile updates will be delivered separately from message deliveries. +-- See rfc doc for more details. +m20250813_delivery_tasks :: Query +m20250813_delivery_tasks = + [sql| +CREATE TABLE delivery_tasks ( + delivery_task_id INTEGER PRIMARY KEY, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + worker_scope TEXT NOT NULL, + job_scope_spec_tag TEXT, + job_scope_include_pending INTEGER, + job_scope_support_gm_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE, + sender_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, + message_id INTEGER REFERENCES messages ON DELETE CASCADE, + message_from_channel INTEGER NOT NULL DEFAULT 0, + task_status TEXT NOT NULL, + task_err_reason TEXT, + failed INTEGER DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX idx_delivery_tasks_group_id ON delivery_tasks(group_id); +CREATE INDEX idx_delivery_tasks_job_scope_support_gm_id ON delivery_tasks(job_scope_support_gm_id); +CREATE INDEX idx_delivery_tasks_sender_group_member_id ON delivery_tasks(sender_group_member_id); +CREATE INDEX idx_delivery_tasks_message_id ON delivery_tasks(message_id); + +CREATE INDEX idx_delivery_tasks_next ON delivery_tasks( + group_id, + worker_scope, + failed, + task_status +); +CREATE INDEX idx_delivery_tasks_next_for_job_scope ON delivery_tasks( + group_id, + worker_scope, + job_scope_spec_tag, + job_scope_include_pending, + job_scope_support_gm_id, + failed, + task_status +); +CREATE INDEX idx_delivery_tasks_next_for_job_scope_sender ON delivery_tasks( + group_id, + worker_scope, + job_scope_spec_tag, + job_scope_include_pending, + job_scope_support_gm_id, + sender_group_member_id, + failed, + task_status +); +CREATE INDEX idx_delivery_tasks_created_at ON delivery_tasks(created_at); + + + +CREATE TABLE delivery_jobs ( + delivery_job_id INTEGER PRIMARY KEY, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + worker_scope TEXT NOT NULL, + job_scope_spec_tag TEXT, + job_scope_include_pending INTEGER, + job_scope_support_gm_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE, + single_sender_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE, + body BLOB, + cursor_group_member_id INTEGER, + job_status TEXT NOT NULL, + job_err_reason TEXT, + failed INTEGER DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX idx_delivery_jobs_group_id ON delivery_jobs(group_id); +CREATE INDEX idx_delivery_jobs_job_scope_support_gm_id ON delivery_jobs(job_scope_support_gm_id); +CREATE INDEX idx_delivery_jobs_single_sender_group_member_id ON delivery_jobs(single_sender_group_member_id); + +CREATE INDEX idx_delivery_jobs_next ON delivery_jobs( + group_id, + worker_scope, + failed, + job_status +); +CREATE INDEX idx_delivery_jobs_created_at ON delivery_jobs(created_at); + + + +ALTER TABLE messages ADD COLUMN broker_ts TEXT; +|] + +down_m20250813_delivery_tasks :: Query +down_m20250813_delivery_tasks = + [sql| +ALTER TABLE messages DROP COLUMN broker_ts; + + + +DROP INDEX idx_delivery_jobs_group_id; +DROP INDEX idx_delivery_jobs_job_scope_support_gm_id; +DROP INDEX idx_delivery_jobs_single_sender_group_member_id; + +DROP INDEX idx_delivery_jobs_next; +DROP INDEX idx_delivery_jobs_created_at; + +DROP TABLE delivery_jobs; + + + +DROP INDEX idx_delivery_tasks_group_id; +DROP INDEX idx_delivery_tasks_job_scope_support_gm_id; +DROP INDEX idx_delivery_tasks_sender_group_member_id; +DROP INDEX idx_delivery_tasks_message_id; + +DROP INDEX idx_delivery_tasks_next; +DROP INDEX idx_delivery_tasks_next_for_job_scope; +DROP INDEX idx_delivery_tasks_next_for_job_scope_sender; +DROP INDEX idx_delivery_tasks_created_at; + +DROP TABLE delivery_tasks; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 5a76567db0..a5a494d36d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -95,6 +95,22 @@ SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT delivery_task_id + FROM delivery_tasks + WHERE group_id = ? + AND worker_scope = ? + AND job_scope_spec_tag IS NOT DISTINCT FROM ? + AND job_scope_include_pending IS NOT DISTINCT FROM ? + AND job_scope_support_gm_id IS NOT DISTINCT FROM ? + AND sender_group_member_id = ? + AND failed = 0 + AND task_status = ? + ORDER BY delivery_task_id ASC + +Plan: +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_next_for_job_scope_sender (group_id=? AND worker_scope=? AND job_scope_spec_tag=? AND job_scope_include_pending=? AND job_scope_support_gm_id=? AND sender_group_member_id=? AND failed=? AND task_status=?) + Query: UPDATE contact_profiles SET user_id = ?, updated_at = ? @@ -417,6 +433,17 @@ Plan: SEARCH cr USING INDEX idx_contact_requests_xcontact_id (user_id=? AND xcontact_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT + delivery_job_id, + worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, + single_sender_group_member_id, body, cursor_group_member_id + FROM delivery_jobs + WHERE delivery_job_id = ? + +Plan: +SEARCH delivery_jobs USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT COUNT(1) FROM ( @@ -648,6 +675,28 @@ Plan: SEARCH m USING INTEGER PRIMARY KEY (rowid=?) SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT delivery_job_id + FROM delivery_jobs + WHERE group_id = ? AND worker_scope = ? + AND failed = 0 AND job_status = ? + ORDER BY delivery_job_id ASC + LIMIT 1 + +Plan: +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_next (group_id=? AND worker_scope=? AND failed=? AND job_status=?) + +Query: + SELECT delivery_task_id + FROM delivery_tasks + WHERE group_id = ? AND worker_scope = ? + AND failed = 0 AND task_status = ? + ORDER BY delivery_task_id ASC + LIMIT 1 + +Plan: +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_next (group_id=? AND worker_scope=? AND failed=? AND task_status=?) + Query: SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.preferences, gp.member_admission FROM group_profiles gp @@ -822,8 +871,9 @@ Plan: Query: INSERT INTO messages - (msg_sent, chat_msg_event, msg_body, created_at, updated_at, connection_id, group_id, shared_msg_id, author_group_member_id, forwarded_by_group_member_id) - VALUES (?,?,?,?,?,?,?,?,?,?) + (msg_sent, chat_msg_event, msg_body, broker_ts, created_at, updated_at, connection_id, group_id, + shared_msg_id, author_group_member_id, forwarded_by_group_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1502,7 +1552,8 @@ Query: UPDATE group_members SET support_chat_items_unread = 0, support_chat_items_member_attention = 0, - support_chat_items_mentions = 0 + support_chat_items_mentions = 0, + updated_at = ? WHERE group_member_id = ? Plan: @@ -2996,6 +3047,23 @@ CORRELATED SCALAR SUBQUERY 1 SEARCH ci USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) USE TEMP B-TREE FOR ORDER BY +Query: + SELECT + t.delivery_task_id, + t.worker_scope, t.job_scope_spec_tag, t.job_scope_include_pending, t.job_scope_support_gm_id, + m.group_member_id, m.member_id, p.display_name, msg.broker_ts, msg.msg_body, t.message_from_channel + FROM delivery_tasks t + JOIN messages msg ON msg.message_id = t.message_id + JOIN group_members m ON m.group_member_id = t.sender_group_member_id + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + WHERE t.delivery_task_id = ? + +Plan: +SEARCH t USING INTEGER PRIMARY KEY (rowid=?) +SEARCH msg USING INTEGER PRIMARY KEY (rowid=?) +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT COUNT(1) FROM chat_items @@ -3990,6 +4058,22 @@ SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (m SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) +Query: + DELETE FROM delivery_jobs + WHERE created_at <= ? + AND (job_status IN (?,?) OR failed = 1) + +Plan: +SEARCH delivery_jobs USING INDEX idx_delivery_jobs_created_at (created_at True + _ -> False + mkWorkItemError errContext = SEWorkItemError {errContext} + $(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError) insertedRowId :: DB.Connection -> IO Int64 @@ -657,7 +670,7 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow - in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention, viaGroupLinkUri} + in GroupInfo {groupId, useRelays = False, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention, viaGroupLinkUri} toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup toPreparedGroup = \case diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 5d25ae64e3..c4253942d8 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -470,6 +470,7 @@ type GroupId = Int64 data GroupInfo = GroupInfo { groupId :: GroupId, + useRelays :: Bool, localDisplayName :: GroupName, groupProfile :: GroupProfile, localAlias :: Text, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 89bc58171c..dcaabe433e 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -2527,6 +2527,8 @@ viewChatError isCmd logLevel testView = \case | testView -> ["duplicate group message, group id: " <> sShow groupId <> ", message id: " <> sShow sharedMsgId] | otherwise -> [] SEUserNoteFolderNotFound -> ["no notes folder"] + SEInternalError {message} + | testView && message == "referenced group member not found" -> [] e -> ["chat db error: " <> sShow e] ChatErrorDatabase err -> case err of DBErrorEncrypted -> ["error: chat database is already encrypted"] diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 7f8761c1e1..2e281cbdea 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -152,7 +152,8 @@ chatGroupTests = do it "manually accept contact with group member" testMemberContactAccept it "manually accept contact with group member incognito" testMemberContactAcceptIncognito describe "group message forwarding" $ do - it "forward messages between invitee and introduced (x.msg.new)" testGroupMsgForward + it "forward messages between invitee and introduced (x.msg.new)" testGroupMsgForwardMessage + it "forward batched messages" testGroupMsgForwardBatched it "forward reports to moderators, don't forward to members (x.msg.new, MCReport)" testGroupMsgForwardReport it "deduplicate forwarded messages" testGroupMsgForwardDeduplicate it "forward message edit (x.msg.update)" testGroupMsgForwardEdit @@ -310,46 +311,26 @@ testGroupShared alice bob cath checkMessages = do bob <## "#team: you don't have permission to send messages" bob ##> "/rm team cath" bob <## "#team: you have insufficient permissions for this action, the required role is admin" - cath #> "#team hello" - concurrentlyN_ - [ alice <# "#team cath> hello", - bob <# "#team cath> hello" - ] alice ##> "/mr team bob admin" concurrentlyN_ [ alice <## "#team: you changed the role of bob to admin", bob <## "#team: alice changed your role from observer to admin", cath <## "#team: alice changed the role of bob from observer to admin" ] - -- remove member - bob ##> "/rm team cath" - concurrentlyN_ - [ bob <## "#team: you removed cath from the group", - alice <## "#team: bob removed cath from the group", - do - cath <## "#team: bob removed you from the group" - cath <## "use /d #team to delete the group" - ] - bob #> "#team hi" - concurrently_ - (alice <# "#team bob> hi") - (cath "#team hello" - concurrently_ - (bob <# "#team alice> hello") - (cath "#team hello" - cath <## "bad chat command: not current member" -- delete contact alice ##> "/d bob" alice <## "bob: contact is deleted" bob <## "alice (Alice) deleted contact with you" when checkMessages $ threadDelay 1000000 alice #> "#team checking connection" - bob <# "#team alice> checking connection" + concurrently_ + (bob <# "#team alice> checking connection") + (cath <# "#team alice> checking connection") when checkMessages $ threadDelay 1000000 bob #> "#team received" - alice <# "#team bob> received" + concurrently_ + (alice <# "#team bob> received") + (cath <# "#team bob> received") when checkMessages $ do alice @@@ [("@cath", "sent invitation to join group team as admin"), ("#team", "received")] bob @@@ [("@alice", "contact deleted"), ("#team", "received")] @@ -710,7 +691,7 @@ testGroupDelete :: HasCallStack => TestParams -> IO () testGroupDelete = testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do - createGroup3 "team" alice bob cath + createGroup3' "team" alice (bob, GRMember) (cath, GRMember) alice ##> "/d #team" concurrentlyN_ [ alice <## "#team: you deleted the group", @@ -978,7 +959,7 @@ testGroupRemoveAdd :: HasCallStack => TestParams -> IO () testGroupRemoveAdd = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - createGroup3 "team" alice bob cath + createGroup3' "team" alice (bob, GRMember) (cath, GRMember) threadDelay 100000 @@ -1861,7 +1842,7 @@ testDeleteMemberWithMessages :: HasCallStack => TestParams -> IO () testDeleteMemberWithMessages = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - createGroup3 "team" alice bob cath + createGroup3' "team" alice (bob, GRMember) (cath, GRMember) threadDelay 750000 alice ##> "/set delete #team on" alice <## "updated group preferences:" @@ -1899,7 +1880,7 @@ testDeleteMemberMarkMessagesDeleted :: HasCallStack => TestParams -> IO () testDeleteMemberMarkMessagesDeleted = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - createGroup3 "team" alice bob cath + createGroup3' "team" alice (bob, GRMember) (cath, GRMember) threadDelay 1000000 bob #> "#team hello" concurrently_ @@ -4855,8 +4836,8 @@ testMemberContactAcceptIncognito = cath ?#> ("@" <> bobIncognito <> " hey") bob ?<# (cathIncognito <> "> hey") -testGroupMsgForward :: HasCallStack => TestParams -> IO () -testGroupMsgForward = +testGroupMsgForwardMessage :: HasCallStack => TestParams -> IO () +testGroupMsgForwardMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath @@ -4884,6 +4865,61 @@ testGroupMsgForward = cath <# "#team bob> hi there [>>]" cath <# "#team hey team" +testGroupMsgForwardBatched :: HasCallStack => TestParams -> IO () +testGroupMsgForwardBatched = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + setupGroupForwarding alice bob cath + + bob ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 1\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 2\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 3\"}}]" + bob <# "#team test 1" + bob <# "#team test 2" + bob <# "#team test 3" + alice <# "#team bob> test 1" + alice <# "#team bob> test 2" + alice <# "#team bob> test 3" + cath <# "#team bob> test 1 [>>]" + cath <# "#team bob> test 2 [>>]" + cath <# "#team bob> test 3 [>>]" + + threadDelay 1000000 + + cath ##> "/_send #1 json [{\"msgContent\": {\"type\": \"text\", \"text\": \"test 4\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 5\"}}, {\"msgContent\": {\"type\": \"text\", \"text\": \"test 6\"}}]" + cath <# "#team test 4" + cath <# "#team test 5" + cath <# "#team test 6" + alice <# "#team cath> test 4" + alice <# "#team cath> test 5" + alice <# "#team cath> test 6" + bob <# "#team cath> test 4 [>>]" + bob <# "#team cath> test 5 [>>]" + bob <# "#team cath> test 6 [>>]" + + alice ##> "/tail #team 6" + alice <# "#team bob> test 1" + alice <# "#team bob> test 2" + alice <# "#team bob> test 3" + alice <# "#team cath> test 4" + alice <# "#team cath> test 5" + alice <# "#team cath> test 6" + + bob ##> "/tail #team 6" + bob <# "#team test 1" + bob <# "#team test 2" + bob <# "#team test 3" + bob <# "#team cath> test 4 [>>]" + bob <# "#team cath> test 5 [>>]" + bob <# "#team cath> test 6 [>>]" + + cath ##> "/tail #team 6" + cath <# "#team bob> test 1 [>>]" + cath <# "#team bob> test 2 [>>]" + cath <# "#team bob> test 3 [>>]" + cath <# "#team test 4" + cath <# "#team test 5" + cath <# "#team test 6" + testGroupMsgForwardReport :: HasCallStack => TestParams -> IO () testGroupMsgForwardReport = testChat3 aliceProfile bobProfile cathProfile $ diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 07bf06b61b..f71e735486 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -1657,11 +1657,11 @@ testJoinGroupIncognito = alice <## "group #secret_club is created" alice <## "to add members use /a secret_club or /create link #secret_club" -- alice invites bob - alice ##> "/a secret_club bob admin" + alice ##> "/a secret_club bob member" concurrentlyN_ [ alice <## "invitation to join the group #secret_club sent to bob", do - bob <## "#secret_club: alice invites you to join the group as admin" + bob <## "#secret_club: alice invites you to join the group as member" bob <## "use /j secret_club to accept" ] bob ##> "/j secret_club" @@ -1669,11 +1669,11 @@ testJoinGroupIncognito = (alice <## "#secret_club: bob joined the group") (bob <## "#secret_club: you joined the group") -- alice invites cath - alice ##> ("/a secret_club " <> cathIncognito <> " admin") + alice ##> ("/a secret_club " <> cathIncognito <> " member") concurrentlyN_ [ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito), do - cath <## "#secret_club: alice invites you to join the group as admin" + cath <## "#secret_club: alice invites you to join the group as member" cath <## ("use /j secret_club to join incognito as " <> cathIncognito) ] -- cath uses the same incognito profile when joining group, cath and bob don't merge contacts @@ -1687,15 +1687,12 @@ testJoinGroupIncognito = bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)") bob <## ("#secret_club: new member " <> cathIncognito <> " is connected") ] - -- cath cannot invite to the group because her membership is incognito - cath ##> "/a secret_club dan" - cath <## "you are using an incognito profile for this group - prohibited to invite contacts" -- alice invites dan - alice ##> "/a secret_club dan admin" + alice ##> "/a secret_club dan member" concurrentlyN_ [ alice <## "invitation to join the group #secret_club sent to dan", do - dan <## "#secret_club: alice invites you to join the group as admin" + dan <## "#secret_club: alice invites you to join the group as member" dan <## "use /j secret_club to accept" ] dan ##> "/j secret_club" @@ -1754,39 +1751,39 @@ testJoinGroupIncognito = alice ##> "/ms secret_club" alice <### [ "alice (Alice): owner, you, created group", - "bob (Bob): admin, invited, connected", - ConsoleString $ cathIncognito <> ": admin, invited, connected", - "dan (Daniel): admin, invited, connected" + "bob (Bob): member, invited, connected", + ConsoleString $ cathIncognito <> ": member, invited, connected", + "dan (Daniel): member, invited, connected" ] bob ##> "/ms secret_club" bob <### [ "alice (Alice): owner, host, connected", - "bob (Bob): admin, you, connected", - ConsoleString $ cathIncognito <> ": admin, connected", - "dan (Daniel): admin, connected" + "bob (Bob): member, you, connected", + ConsoleString $ cathIncognito <> ": member, connected", + "dan (Daniel): member, connected" ] cath ##> "/ms secret_club" cath <### [ "alice (Alice): owner, host, connected", - "bob_1 (Bob): admin, connected", - ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected", - "dan_1 (Daniel): admin, connected" + "bob_1 (Bob): member, connected", + ConsoleString $ "i " <> cathIncognito <> ": member, you, connected", + "dan_1 (Daniel): member, connected" ] dan ##> "/ms secret_club" dan <### [ "alice (Alice): owner, host, connected", - "bob (Bob): admin, connected", - ConsoleString $ cathIncognito <> ": admin, connected", - "dan (Daniel): admin, you, connected" + "bob (Bob): member, connected", + ConsoleString $ cathIncognito <> ": member, connected", + "dan (Daniel): member, you, connected" ] -- remove member - bob ##> ("/rm secret_club " <> cathIncognito) + alice ##> ("/rm secret_club " <> cathIncognito) concurrentlyN_ - [ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"), - alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), - dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), + [ alice <## ("#secret_club: you removed " <> cathIncognito <> " from the group"), + bob <## ("#secret_club: alice removed " <> cathIncognito <> " from the group"), + dan <## ("#secret_club: alice removed " <> cathIncognito <> " from the group"), do - cath <## "#secret_club: bob_1 removed you from the group" + cath <## "#secret_club: alice removed you from the group" cath <## "use /d #secret_club to delete the group" ] bob #> "#secret_club hi" From fa9bc1537c4dd87b6468a4d38cbeae5744959e64 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 14 Sep 2025 14:41:50 +0100 Subject: [PATCH 040/137] cli: option to disable backup or change backup directory on migration (#6267) * cli: option to disable backup or change backup directory on migration * update nix * update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 10 +++++----- src/Simplex/Chat/Core.hs | 8 ++++---- src/Simplex/Chat/Mobile.hs | 10 ++++++---- src/Simplex/Chat/Options.hs | 7 +++++-- src/Simplex/Chat/Options/Postgres.hs | 3 +++ src/Simplex/Chat/Options/SQLite.hs | 13 +++++++++++++ src/Simplex/Chat/Store.hs | 4 ++-- tests/ChatClient.hs | 7 ++++--- tests/MobileTests.hs | 4 ++-- tests/SchemaDump.hs | 12 ++++++------ 12 files changed, 52 insertions(+), 30 deletions(-) diff --git a/cabal.project b/cabal.project index 8a861a1909..48ac0dc24c 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 7e98b3103f4eb9a6a9a99604afb6f3a32ffc013d + tag: 8a7991a3768d632346c8d8f3c319ed64ccba93b6 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index a5f2790418..09a1407a31 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."7e98b3103f4eb9a6a9a99604afb6f3a32ffc013d" = "0g6lm65hs2kp2rsk9lqzj42nq51i5xynxrf16axma80cq0jqzxzl"; + "https://github.com/simplex-chat/simplexmq.git"."8a7991a3768d632346c8d8f3c319ed64ccba93b6" = "14p3436bvbxrcx3sp2gy97ycc7v0wzhar8vvxa6lcs8anki1va08"; "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"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e14275b75c..7fa8380c2f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -45,7 +45,7 @@ import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore (dbNew)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Entity -import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..), MigrationError) import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), ProtocolType (..), SProtocolType (..), SubscriptionMode (..), UserProtocol) @@ -115,10 +115,10 @@ defaultChatConfig = logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} -createChatDatabase :: ChatDbOpts -> MigrationConfirmation -> IO (Either MigrationError ChatDatabase) -createChatDatabase chatDbOpts confirmMigrations = runExceptT $ do - chatStore <- ExceptT $ createChatStore (toDBOpts chatDbOpts chatSuffix False) confirmMigrations - agentStore <- ExceptT $ createAgentStore (toDBOpts chatDbOpts agentSuffix False) confirmMigrations +createChatDatabase :: ChatDbOpts -> MigrationConfig -> IO (Either MigrationError ChatDatabase) +createChatDatabase chatDbOpts migrationConfig = runExceptT $ do + chatStore <- ExceptT $ createChatStore (toDBOpts chatDbOpts chatSuffix False) migrationConfig + agentStore <- ExceptT $ createAgentStore (toDBOpts chatDbOpts agentSuffix False) migrationConfig pure ChatDatabase {chatStore, agentStore} newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Bool -> IO ChatController diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index fabc45a04c..131b420bd9 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -30,7 +30,7 @@ import Simplex.Chat.Store.Profiles import Simplex.Chat.Types import Simplex.Chat.Types.Preferences (FeatureAllowed (..), FilesPreference (..), Preferences (..), emptyChatPrefs) import Simplex.Chat.View (ChatResponseEvent, serializeChatError, serializeChatResponse) -import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..)) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..)) import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction) import System.Exit (exitFailure) import System.IO (hFlush, stdout) @@ -38,15 +38,15 @@ import Text.Read (readMaybe) import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations}, createBot, maintenance} chat = +simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@ChatOpts {coreOptions = CoreChatOpts {dbOptions, logAgent, yesToUpMigrations, migrationBackupPath}, createBot, maintenance} chat = case logAgent of Just level -> do setLogLevel level withGlobalLogging logCfg initRun _ -> initRun where - initRun = createChatDatabase dbOptions confirm' >>= either exit run - confirm' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations + initRun = createChatDatabase dbOptions migrationConfig >>= either exit run + migrationConfig = MigrationConfig (if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations) migrationBackupPath exit e = do putStrLn $ "Error opening database: " <> show e exitFailure diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index f18a3900da..fc61fa6fb5 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -50,7 +50,7 @@ import Simplex.Chat.Types import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) import Simplex.Messaging.Agent.Store.Interface (closeDBStore, reopenDBStore) -import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..), MigrationError) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) @@ -254,7 +254,8 @@ mobileChatOpts dbOptions = tbqSize = 4096, deviceName = Nothing, highlyAvailable = False, - yesToUpMigrations = False + yesToUpMigrations = False, + migrationBackupPath = Just "" }, chatCmd = "", chatCmdDelay = 3, @@ -294,8 +295,9 @@ chatMigrateInit dbFilePrefix dbKey confirm = do chatMigrateInitKey :: ChatDbOpts -> Bool -> String -> Bool -> IO (Either DBMigrationResult ChatController) chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode = runExceptT $ do confirmMigrations <- liftEitherWith (const DBMInvalidConfirmation) $ strDecode $ B.pack confirm - chatStore <- migrate createChatStore (toDBOpts chatDbOpts chatSuffix keepKey) confirmMigrations - agentStore <- migrate createAgentStore (toDBOpts chatDbOpts agentSuffix keepKey) confirmMigrations + let migrationConfig = MigrationConfig confirmMigrations (Just "") + chatStore <- migrate createChatStore (toDBOpts chatDbOpts chatSuffix keepKey) migrationConfig + agentStore <- migrate createAgentStore (toDBOpts chatDbOpts agentSuffix keepKey) migrationConfig liftIO $ initialize chatStore ChatDatabase {chatStore, agentStore} where opts = mobileChatOpts $ removeDbKey chatDbOpts diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 9411c66175..2a224aca8b 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -67,7 +67,8 @@ data CoreChatOpts = CoreChatOpts tbqSize :: Natural, deviceName :: Maybe Text, highlyAvailable :: Bool, - yesToUpMigrations :: Bool + yesToUpMigrations :: Bool, + migrationBackupPath :: Maybe FilePath } data CreateBotOpts = CreateBotOpts @@ -243,6 +244,7 @@ coreChatOptsP appDir defaultDbName = do <> short 'y' <> help "Automatically confirm \"up\" database migrations" ) + migrationBackupPath <- migrationBackupPathP pure CoreChatOpts { dbOptions, @@ -268,7 +270,8 @@ coreChatOptsP appDir defaultDbName = do tbqSize, deviceName, highlyAvailable, - yesToUpMigrations + yesToUpMigrations, + migrationBackupPath } where useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p diff --git a/src/Simplex/Chat/Options/Postgres.hs b/src/Simplex/Chat/Options/Postgres.hs index f7c429e93e..46c1faa5e0 100644 --- a/src/Simplex/Chat/Options/Postgres.hs +++ b/src/Simplex/Chat/Options/Postgres.hs @@ -52,6 +52,9 @@ chatDbOptsP _appDir defaultDbName = do ) pure ChatDbOpts {dbConnstr, dbSchemaPrefix, dbPoolSize, dbCreateSchema} +migrationBackupPathP :: Parser (Maybe FilePath) +migrationBackupPathP = pure Nothing + dbString :: ChatDbOpts -> String dbString ChatDbOpts {dbConnstr} = dbConnstr diff --git a/src/Simplex/Chat/Options/SQLite.hs b/src/Simplex/Chat/Options/SQLite.hs index 0507076613..d2e79eea79 100644 --- a/src/Simplex/Chat/Options/SQLite.hs +++ b/src/Simplex/Chat/Options/SQLite.hs @@ -54,6 +54,19 @@ chatDbOptsP appDir defaultDbName = do vacuumOnMigration = not disableVacuum } +migrationBackupPathP :: Parser (Maybe FilePath) +migrationBackupPathP = + flag' Nothing + ( long "disable-backup" + <> help "Disable backup when migrating database" + ) + <|> + (fmap Just . strOption) + ( long "backup-directory" + <> help "Directory to backup database for migration" + <> value "" + ) + dbString :: ChatDbOpts -> String dbString ChatDbOpts {dbFilePrefix} = dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index d3d3395ed1..e80037be74 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -21,12 +21,12 @@ import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Messaging.Agent.Store.Common (DBStore (..), withTransaction) import Simplex.Messaging.Agent.Store.Interface (DBOpts, createDBStore) -import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, MigrationError) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfig, MigrationError) #if defined(dbPostgres) import Simplex.Chat.Store.Postgres.Migrations #else import Simplex.Chat.Store.SQLite.Migrations #endif -createChatStore :: DBOpts -> MigrationConfirmation -> IO (Either MigrationError DBStore) +createChatStore :: DBOpts -> MigrationConfig -> IO (Either MigrationError DBStore) createChatStore dbCreateOpts = createDBStore dbCreateOpts migrations diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index acf7903003..5e5c34d516 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -47,7 +47,7 @@ import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.Interface (closeDBStore) -import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..), MigrationError) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (ProtocolClientConfig (..)) import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) @@ -151,7 +151,8 @@ testCoreOpts = tbqSize = 16, deviceName = Nothing, highlyAvailable = False, - yesToUpMigrations = False + yesToUpMigrations = False, + migrationBackupPath = Nothing } #if !defined(dbPostgres) @@ -302,7 +303,7 @@ insertUser :: DBStore -> IO () insertUser st = withTransaction st (`DB.execute_` "INSERT INTO users DEFAULT VALUES") #else createDatabase TestParams {tmpPath} CoreChatOpts {dbOptions} dbPrefix = do - createChatDatabase dbOptions {dbFilePrefix = tmpPath dbPrefix} MCError + createChatDatabase dbOptions {dbFilePrefix = tmpPath dbPrefix} (MigrationConfig MCError Nothing) insertUser :: DBStore -> IO () insertUser st = withTransaction st (`DB.execute_` "INSERT INTO users (user_id) VALUES (1)") diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 0ba784a1fc..1814ccb564 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -38,7 +38,7 @@ import Simplex.Chat.Store import Simplex.Chat.Store.Profiles import Simplex.Chat.Types (AgentUserId (..), Profile (..)) import Simplex.Messaging.Agent.Store.Interface -import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..)) +import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile(..), CryptoFileArgs (..)) @@ -167,7 +167,7 @@ testChatApi ps = do let tmp = tmpPath ps dbPrefix = tmp "1" f = dbPrefix <> chatSuffix - Right st <- createChatStore (DBOpts f "myKey" False True DB.TQOff) MCYesUp + Right st <- createChatStore (DBOpts f "myKey" False True DB.TQOff) (MigrationConfig MCYesUp Nothing) Right _ <- withTransaction st $ \db -> runExceptT $ createUserRecord db (AgentUserId 1) aliceProfile {preferences = Nothing} True Right cc <- chatMigrateInit dbPrefix "myKey" "yesUp" Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "" "yesUp" diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index df1739449e..5b6fffd7dc 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -26,7 +26,7 @@ import Simplex.Messaging.Agent.Store.DB (TrackQueries (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Interface import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations -import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfirmation (..), MigrationsToRun (..), toDownMigration) +import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfig (..), MigrationConfirmation (..), MigrationsToRun (..), toDownMigration) import Simplex.Messaging.Util (ifM, tshow, whenM) import System.Directory (doesFileExist, removeFile) import System.Process (readCreateProcess, shell) @@ -63,7 +63,7 @@ testVerifySchemaDump :: IO () testVerifySchemaDump = withTmpFiles $ do savedSchema <- ifM (doesFileExist appSchema) (readFile appSchema) (pure "") savedSchema `deepseq` pure () - void $ createChatStore (DBOpts testDB "" False True TQOff) MCError + void $ createChatStore (DBOpts testDB "" False True TQOff) (MigrationConfig MCError Nothing) getSchema testDB appSchema `shouldReturn` savedSchema removeFile testDB @@ -71,14 +71,14 @@ testVerifyLintFKeyIndexes :: IO () testVerifyLintFKeyIndexes = withTmpFiles $ do savedLint <- ifM (doesFileExist appLint) (readFile appLint) (pure "") savedLint `deepseq` pure () - void $ createChatStore (DBOpts testDB "" False True TQOff) MCError + void $ createChatStore (DBOpts testDB "" False True TQOff) (MigrationConfig MCError Nothing) getLintFKeyIndexes testDB "tests/tmp/chat_lint.sql" `shouldReturn` savedLint removeFile testDB testSchemaMigrations :: IO () testSchemaMigrations = withTmpFiles $ do let noDownMigrations = dropWhileEnd (\Migration {down} -> isJust down) Store.migrations - Right st <- createDBStore (DBOpts testDB "" False True TQOff) noDownMigrations MCError + Right st <- createDBStore (DBOpts testDB "" False True TQOff) noDownMigrations (MigrationConfig MCError Nothing) mapM_ (testDownMigration st) $ drop (length noDownMigrations) Store.migrations closeDBStore st removeFile testDB @@ -150,7 +150,7 @@ saveQueryPlans = it "verify and overwrite query plans" $ \TestParams {chatQueryS updatePlans appChatQueryPlans chatQueryStats - (createChatStore (DBOpts testDB "" False True TQOff) MCError) + (createChatStore (DBOpts testDB "" False True TQOff) (MigrationConfig MCError Nothing)) (\db -> do DB.execute_ db "CREATE TABLE IF NOT EXISTS temp_conn_ids (conn_id BLOB)" DB.execute_ db "CREATE TABLE IF NOT EXISTS temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT)" @@ -159,7 +159,7 @@ saveQueryPlans = it "verify and overwrite query plans" $ \TestParams {chatQueryS updatePlans appAgentQueryPlans agentQueryStats - (createAgentStore (DBOpts testAgentDB "" False True TQOff) MCError) + (createAgentStore (DBOpts testAgentDB "" False True TQOff) (MigrationConfig MCError Nothing)) (const $ pure ()) chatSavedPlans' == chatSavedPlans `shouldBe` True agentSavedPlans' == agentSavedPlans `shouldBe` True From 7c92c7666c7c8ae7e4a15068d869c1ff66cd1900 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:06:39 +0000 Subject: [PATCH 041/137] scripts/db: update documentation, add pg -> sqlite section (#6177) * scripts/db: update doc * scripts/db: update documentation, add pg2sqlite script * scripts/db/pg2sqlite: formatiing, code quality, modularity * scripts/db: parallel pgloader --- scripts/db/README.md | 358 +++++++++++++--- scripts/db/pg2sqlite.py | 899 ++++++++++++++++++++++++++++++++++++++++ scripts/db/sqlite.load | 3 +- 3 files changed, 1197 insertions(+), 63 deletions(-) create mode 100755 scripts/db/pg2sqlite.py diff --git a/scripts/db/README.md b/scripts/db/README.md index 68d4f74ac7..333a6f4acf 100644 --- a/scripts/db/README.md +++ b/scripts/db/README.md @@ -1,96 +1,286 @@ -# Transfer data from SQLite to Postgres database +## Transfer data from SQLite to Postgres database -1. \* Decrypt SQLite database if it is encrypted. +1. Decrypt SQLite database if it is encrypted. - ```sh - sqlcipher encrypted_simplex_v1_agent.db - ``` + 1. Agent: - ```sql - PRAGMA key = 'password'; - ATTACH DATABASE 'simplex_v1_agent.db' AS plaintext KEY ''; - SELECT sqlcipher_export('plaintext'); - DETACH DATABASE plaintext; - ``` + 1. Open sqlite db: - Repeat for `simplex_v1_chat.db`. + ```sh + sqlcipher simplex_v1_agent.db + ``` + + 2. Set your db password: + + ```sql + PRAGMA key = ''; + ``` + + 3. Check if db was successfully decrypted: + + ```sh + SELECT count(*) FROM sqlite_master; + ``` + + 4. Attach new empty db: + + ```sh + ATTACH DATABASE 'simplex_v1_agent_plaintext.db' AS plaintext KEY ''; + ``` + + 5. Export opened db to attached db as plaintext: + + ```sh + SELECT sqlcipher_export('plaintext'); + ``` + + 6. Deattach the plaintext db: + + ```sh + DETACH DATABASE plaintext; + ``` + + 2. Chat: + + 1. Open sqlite db: + + ```sh + sqlcipher simplex_v1_chat.db + ``` + + 2. Set your db password: + + ```sql + PRAGMA key = ''; + ``` + + 3. Check if db was successfully decrypted: + + ```sh + SELECT count(*) FROM sqlite_master; + ``` + + 4. Attach new empty db: + + ```sh + ATTACH DATABASE 'simplex_v1_chat_plaintext.db' AS plaintext KEY ''; + ``` + + 5. Export opened db to attached db as plaintext: + + ```sh + SELECT sqlcipher_export('plaintext'); + ``` + + 6. Deattach the plaintext db: + + ```sh + DETACH DATABASE plaintext; + ``` 2. Prepare Postgres database. - - Create Postgres database. In shell: + 1. Connect to PostgreSQL databse: ```sh - createdb -O simplex simplex_v1 + psql -U postgres -h localhost ``` - Or via query. + 2. Create user with password: - - Build `simplex-chat` executable with `client_postgres` flag and run it to initialize new chat database. + ```sh + CREATE USER simplex WITH ENCRYPTED PASSWORD '123123'; + ``` - This should create `simplex_v1_agent_schema` and `simplex_v1_chat_schema` schemas in `simplex_v1` database, with `migrations` tables populated. Some tables would have initialization data - it will be truncated via pgloader command in next step. + 3. Create database: + + ```sh + CREATE DATABASE simplex_v1; + ``` + + 4. Assign permissions: + + ```sh + GRANT ALL PRIVILEGES ON DATABASE simplex_v1 TO simplex; + ``` + +3. Prepare database: + + You should build the CLI binary from the same `TAG` as the desktop. + + 1. Build CLI with PostgreSQL support: + + ```sh + cabal build -fclient_postgres exe:simplex-chat + ``` + + And rename it to: + + ```sh + mv simplex-chat simplex-chat-pg + ``` + + 2. Execute CLI: + + ```sh + ./simplex-chat-pg -d "postgresql://simplex:123123@localhost:5432/simplex_v1" --create-schema + ``` + + Press `Ctrl+C` when CLI ask for a display name. + + This should create `simplex_v1_agent_schema` and `simplex_v1_chat_schema` schemas in `simplex_v1` database, with `migrations` tables populated. Some tables would have initialization data - it will be truncated via pgloader command in next step. 3. Load data from decrypted SQLite databases to Postgres database via pgloader. Install pgloader and add it to PATH. Run in shell (substitute paths): ```sh - SQLITE_DBPATH='simplex_v1_agent.db' POSTGRES_CONN='postgres://simplex@/simplex_v1' POSTGRES_SCHEMA='simplex_v1_agent_schema' pgloader --on-error-stop sqlite.load + export POSTGRES_CONN='postgresql://simplex:123123@localhost:5432/simplex_v1' + ``` - SQLITE_DBPATH='simplex_v1_chat.db' POSTGRES_CONN='postgres://simplex@/simplex_v1' POSTGRES_SCHEMA='simplex_v1_chat_schema' pgloader --on-error-stop sqlite.load + And then: + + ```sh + SQLITE_DBPATH='simplex_v1_agent_plaintext.db' \ + POSTGRES_SCHEMA='simplex_v1_agent_schema' \ + CPU_CORES=$(nproc) WORKERS=$((CPU_CORES - 1)) pgloader --dynamic-space-size 262144 --on-error-stop sqlite.load + + SQLITE_DBPATH='simplex_v1_chat_plaintext.db' \ + POSTGRES_SCHEMA='simplex_v1_chat_schema' \ + CPU_CORES=$(nproc) WORKERS=$((CPU_CORES - 1)) pgloader --dynamic-space-size 262144 --on-error-stop sqlite.load ``` 4. Update sequences for Postgres tables. - ```sql - DO $$ - DECLARE - rec RECORD; - BEGIN - EXECUTE 'SET SEARCH_PATH TO simplex_v1_agent_schema'; + Connect to db: - FOR rec IN - SELECT - table_name, - column_name, - pg_get_serial_sequence(table_name, column_name) AS seq_name - FROM - information_schema.columns - WHERE - table_schema = 'simplex_v1_agent_schema' - AND identity_generation = 'ALWAYS' - LOOP - EXECUTE format( - 'SELECT setval(%L, (SELECT MAX(%I) FROM %I))', - rec.seq_name, rec.column_name, rec.table_name - ); - END LOOP; - END $$; + ```sh + PGPASSWORD=123123 psql -h localhost -U simplex -d simplex_v1 ``` - Repeat for `simplex_v1_chat_schema`. + Execute the following: -5. \* Compare number of rows between Postgres and SQLite tables. + 1. For `agent`: - To check number of rows for all tables in Postgres database schema run: + ```sql + DO $$ + DECLARE + rec RECORD; + BEGIN + EXECUTE 'SET SEARCH_PATH TO simplex_v1_agent_schema'; - ```sql - WITH tbl AS ( - SELECT table_schema, table_name - FROM information_schema.Tables - WHERE table_name NOT LIKE 'pg_%' - AND table_schema IN ('simplex_v1_agent_schema') - ) - SELECT - table_schema AS schema_name, - table_name, - (xpath('/row/c/text()', query_to_xml( - format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name), false, true, '' - )))[1]::text::int AS records_count - FROM tbl - ORDER BY records_count DESC; - ``` + FOR rec IN + SELECT + table_name, + column_name, + pg_get_serial_sequence(table_name, column_name) AS seq_name + FROM + information_schema.columns + WHERE + table_schema = 'simplex_v1_agent_schema' + AND identity_generation = 'ALWAYS' + LOOP + EXECUTE format( + 'SELECT setval(%L, (SELECT MAX(%I) FROM %I))', + rec.seq_name, rec.column_name, rec.table_name + ); + END LOOP; + END $$; + ``` - Repeat for `simplex_v1_chat_schema`. + 2. For `chat`: + + ```sql + DO $$ + DECLARE + rec RECORD; + BEGIN + EXECUTE 'SET SEARCH_PATH TO simplex_v1_chat_schema'; + + FOR rec IN + SELECT + table_name, + column_name, + pg_get_serial_sequence(table_name, column_name) AS seq_name + FROM + information_schema.columns + WHERE + table_schema = 'simplex_v1_chat_schema' + AND identity_generation = 'ALWAYS' + LOOP + EXECUTE format( + 'SELECT setval(%L, (SELECT MAX(%I) FROM %I))', + rec.seq_name, rec.column_name, rec.table_name + ); + END LOOP; + END $$; + ``` + +5. Compare number of rows between Postgres and SQLite tables. + + **PostgreSQL**: + + 1. For `agent`: + + ```sql + WITH tbl AS ( + SELECT table_schema, table_name + FROM information_schema.Tables + WHERE table_name NOT LIKE 'pg_%' + AND table_schema IN ('simplex_v1_agent_schema') + ) + SELECT + table_schema AS schema_name, + table_name, + (xpath('/row/c/text()', query_to_xml( + format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name), false, true, '' + )))[1]::text::int AS records_count + FROM tbl + ORDER BY records_count DESC; + ``` + + 2. For `chat`: + + ```sql + WITH tbl AS ( + SELECT table_schema, table_name + FROM information_schema.Tables + WHERE table_name NOT LIKE 'pg_%' + AND table_schema IN ('simplex_v1_chat_schema') + ) + SELECT + table_schema AS schema_name, + table_name, + (xpath('/row/c/text()', query_to_xml( + format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name), false, true, '' + )))[1]::text::int AS records_count + FROM tbl + ORDER BY records_count DESC; + ``` + + **SQLite**: + + 1. For `agent`: + + ```sh + db="simplex_v1_agent_plaintext.db" + sqlite3 "$db" "SELECT name FROM sqlite_master WHERE type='table';" | + while read table; do + count=$(sqlite3 "$db" "SELECT COUNT(*) FROM \"$table\";") + echo "$table: $count" + done | sort -k2 -nr | less + ``` + + 2. For `chat`: + + ```sh + db="simplex_v1_chat_plaintext.db" + sqlite3 "$db" "SELECT name FROM sqlite_master WHERE type='table';" | + while read table; do + count=$(sqlite3 "$db" "SELECT COUNT(*) FROM \"$table\";") + echo "$table: $count" + done | sort -k2 -nr | less + ``` 6. Build and run desktop app with Postgres backend. @@ -103,3 +293,47 @@ # or ./gradlew packageDmg -Pdatabase.backend=postgres ``` + +## Transfer data from SQLite to Postgres database + +1. Prepare sqlite db: + + 1. Download simplex-chat CLI: + + You should download the CLI binary from the same `TAG` as the desktop. + + ```sh + export TAG='v6.4.3.1' + curl -L "https://github.com/simplex-chat/simplex-chat/releases/download/${TAG}/simplex-chat-ubuntu-22_04-x86_64" -o 'simplex-chat' + ``` + + 2. Run the CLI: + + ```sh + ./simplex-chat + ``` + + Press `Ctrl+C` when CLI ask for a display name. + + 3. Move database: + + ```sh + mv ~/.simplex/simplex_v1_* ~/.local/share/simplex/ + ``` + +2. Transfer data: + + ```sh + ./pg2sqlite.py --verbose 'postgresql://simplex:123123@localhost:5432/simplex_v1' ~/.local/share/simplex/ + ``` + +4. Update BLOBs: + + ```sh + sqlite3 simplex_v1_chat.db + ``` + + ```sh + UPDATE group_members SET member_role = CAST(member_role as BLOB); + UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role AS BLOB) WHERE group_link_member_role is not null; + ``` diff --git a/scripts/db/pg2sqlite.py b/scripts/db/pg2sqlite.py new file mode 100755 index 0000000000..533b15d143 --- /dev/null +++ b/scripts/db/pg2sqlite.py @@ -0,0 +1,899 @@ +#!/usr/bin/env python3 +""" +PostgreSQL -> per-schema SQLite migration with colored, clean logging. + +Usage example: + python db_migrate.py 'postgresql://user:pass@host:5432/db' /path/to/sqlite/dir --dry-run + +Note: color output will be disabled automatically if stdout is not a TTY or if --no-color is passed. +""" + +from __future__ import annotations +import argparse +import logging +import os +import sqlite3 +import sys +import re +import datetime +from pathlib import Path +from typing import Set, List, Tuple, Dict, Optional, NamedTuple + +import psycopg +from psycopg import sql + +# ---------------------- +# Small utilities +# ---------------------- + +try: + sqlite3.register_adapter( + datetime.datetime, + lambda v: v.isoformat(sep=" ", timespec="microseconds"), + ) + sqlite3.register_adapter(datetime.date, lambda v: v.isoformat()) + sqlite3.register_adapter( + datetime.time, lambda v: v.isoformat(timespec="microseconds") + ) +except ValueError: + pass # already registered + +ANSI = { + "reset": "\x1b[0m", + "bold": "\x1b[1m", + "dim": "\x1b[2m", + "red": "\x1b[31m", + "green": "\x1b[32m", + "yellow": "\x1b[33m", + "blue": "\x1b[34m", + "magenta": "\x1b[35m", + "cyan": "\x1b[36m", + "gray": "\x1b[90m", +} + +DEFAULT_BATCH_SIZE = 10000 + +TYPE_COMPATIBILITY = { + "bytea": ["BLOB", "CHAR", "CLOB", "TEXT", "JSON"], + "int": ["INT", "NUMERIC"], + "serial": ["INT", "NUMERIC"], + "numeric": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"], + "decimal": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"], + "real": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"], + "double": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"], + "float": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"], + "money": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"], + "bool": ["BOOL", "INT", "NUMERIC"], + "varchar": ["CHAR", "CLOB", "TEXT"], + "char": ["CHAR", "CLOB", "TEXT"], + "text": ["CHAR", "CLOB", "TEXT"], + "citext": ["CHAR", "CLOB", "TEXT"], + "timestamp": ["DATE", "TIME", "CHAR", "TEXT", "DATETIME"], + "time": ["DATE", "TIME", "CHAR", "TEXT", "DATETIME"], + "date": ["DATE", "TIME", "CHAR", "TEXT", "DATETIME"], + "uuid": ["CHAR", "TEXT", "UUID", "CLOB"], + "json": ["JSON", "TEXT", "CHAR", "CLOB"], + "jsonb": ["JSON", "TEXT", "CHAR", "CLOB"], +} + + +def _sanitize_cursor_name(s: str) -> str: + return re.sub(r"[^A-Za-z0-9_]+", "_", s) + + +def supports_color(force_no: bool) -> bool: + """Return True when we should emit ANSI colors.""" + if force_no: + return False + if os.getenv("NO_COLOR"): + return False + term = os.getenv("TERM", "") + if term == "" or term.lower() == "dumb": + return False + try: + isatty = sys.stdout.isatty() + except Exception: + isatty = False + return isatty + + +class ColoredFormatter(logging.Formatter): + LEVEL_COLORS = { + logging.DEBUG: ANSI["gray"], + logging.INFO: ANSI["green"], + logging.WARNING: ANSI["yellow"], + logging.ERROR: ANSI["red"], + logging.CRITICAL: ANSI["red"] + ANSI["bold"], + } + + TAG_COLORS = { + "SKIP": ANSI["yellow"], + "SCHEMA": ANSI["blue"], + "OK": ANSI["magenta"], + } + + def __init__(self, use_color: bool = True): + super().__init__(fmt="%(message)s") + self.use_color = use_color + + def format(self, record: logging.LogRecord) -> str: + msg = super().format(record) + parts = msg.split(" ", 1) + tag = parts[0] + rest = parts[1] if len(parts) > 1 else "" + plain_label = f"[{tag}]" + + if not self.use_color: + return f"{plain_label}{(' ' + rest) if rest else ''}" + + color = self.TAG_COLORS.get( + tag.upper(), self.LEVEL_COLORS.get(record.levelno, "") + ) + reset = ANSI["reset"] + return f"{color}{plain_label}{reset}{(' ' + rest) if rest else ''}" + + +def setup_logger(verbose: bool, no_color: bool) -> logging.Logger: + use_color = supports_color(no_color) + logger = logging.getLogger("db_migrate") + logger.setLevel(logging.DEBUG if verbose else logging.INFO) + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG if verbose else logging.INFO) + handler.setFormatter(ColoredFormatter(use_color=use_color)) + logger.handlers.clear() + logger.addHandler(handler) + logging.getLogger("psycopg").setLevel(logging.WARNING) + + if verbose: + logger.debug(f"color_support: {use_color}") + try: + isatty = sys.stdout.isatty() + except Exception: + isatty = False + logger.debug( + f"TERM={os.getenv('TERM', '')!r} " + f"NO_COLOR={os.getenv('NO_COLOR')!r} " + f"isatty={isatty}" + ) + return logger + + +def quote_sqlite_identifier(name: str) -> str: + return '"' + name.replace('"', '""') + '"' + + +def quote_pg_identifier(name: str) -> str: + """Simple PG identifier quoting for building safe SQL strings.""" + return '"' + name.replace('"', '""') + '"' + + +def sqlite_decl_satisfies(pg_type: str, sqlite_decl: str) -> bool: + # Treat empty/blank SQLite declarations more permissively based on PG type. + decl_raw = sqlite_decl or "" + if not decl_raw.strip(): + pg = (pg_type or "").lower() + # arrays -> textual affinity + if pg.endswith("[]"): + return True + # integer-like + if re.search(r"\b(?:int|serial|bigint)\b", pg): + return True + # numeric/float + if re.search(r"\b(?:numeric|decimal|real|double|float|money)\b", pg): + return True + # boolean + if re.search(r"\b(?:bool|boolean)\b", pg): + return True + # binary + if re.search(r"\b(?:bytea)\b", pg): + return True + # textual/json/uuid/timestamps/dates/times + if re.search( + r"\b(?:varchar|char|text|citext|jsonb|json|uuid|timestamp|time|date)\b", pg + ): + return True + # conservative fallback: accept empty decl as permissive + return True + + decl = decl_raw.upper() + pg = (pg_type or "").lower() + + # array type + if pg.endswith("[]"): + return any(tok in decl for tok in ("TEXT", "CHAR", "CLOB", "JSON")) + + for key, allowed_types in TYPE_COMPATIBILITY.items(): + if re.search(r"\b" + re.escape(key) + r"\b", pg): + return any(tok in decl for tok in allowed_types) + + return any( + tok in decl for tok in ("TEXT", "CHAR", "CLOB", "NUMERIC", "BLOB", "INT") + ) + + +# ---------------------- +# Postgres helpers +# ---------------------- + + +def list_user_schemas(pg_cursor) -> List[str]: + pg_cursor.execute( + """ + SELECT nspname + FROM pg_namespace + WHERE nspname NOT LIKE 'pg_%' + AND nspname != 'information_schema' + AND nspname != 'public' + ORDER BY nspname; + """ + ) + return [r[0] for r in pg_cursor.fetchall()] + + +def list_tables_in_schema(pg_cursor, schema: str) -> List[str]: + pg_cursor.execute( + sql.SQL( + """ + SELECT table_name + FROM information_schema.tables + WHERE table_schema = %s + ORDER BY table_name; + """ + ), + (schema,), + ) + return [r[0] for r in pg_cursor.fetchall()] + + +def get_pg_columns(pg_cursor, schema: str, table: str) -> List[Tuple[str, str]]: + pg_cursor.execute( + sql.SQL( + """ + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_schema = %s AND table_name = %s + ORDER BY ordinal_position; + """ + ), + (schema, table), + ) + return [(r[0], r[1]) for r in pg_cursor.fetchall()] + + +# ---------------------- +# SQLite helpers +# ---------------------- + + +class SQLiteCol(NamedTuple): + cid: int + name: str + type: str + notnull: int + dflt_value: str + pk: int + + +def get_sqlite_table_info(sqlite_cursor, table_name: str) -> list[SQLiteCol]: + qname = quote_sqlite_identifier(table_name) + sqlite_cursor.execute(f"PRAGMA table_info({qname});") + return [SQLiteCol(*row) for row in sqlite_cursor.fetchall()] + + +def sqlite_table_has_autoincrement(sqlite_cursor, table_name: str) -> bool: + sqlite_cursor.execute( + "SELECT sql FROM sqlite_master WHERE type='table' AND name = ?;", (table_name,) + ) + row = sqlite_cursor.fetchone() + if not row or not row[0]: + return False + return "AUTOINCREMENT" in row[0].upper() + + +def sqlite_sequence_table_exists(sqlite_cursor) -> bool: + sqlite_cursor.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='sqlite_sequence';" + ) + exists = sqlite_cursor.fetchone() is not None + if not exists: + logging.getLogger("db_migrate").debug("sqlite_sequence table not present") + return exists + + +# ---------------------- +# Core migration logic +# ---------------------- + + +def build_select_for_sqlite_columns( + schema: str, + table: str, + sqlite_cols: list[SQLiteCol], + pg_columns_map: Dict[str, Tuple[str, str]], +) -> str: + """Return a plain SQL string (no trailing semicolon) selecting PG columns in the order of sqlite_cols. + Uses simple identifier quoting to avoid psycopg.sql objects which may vary by driver version. + """ + select_parts = [] + for col in sqlite_cols: + lc = col.name.lower() + if lc in pg_columns_map: + pg_name, pg_type = pg_columns_map[lc] + pg_type_l = (pg_type or "").lower() + if any(tok in pg_type_l for tok in ("timestamp", "time")): + expr = f"{quote_pg_identifier(pg_name)}::text" + else: + expr = f"{quote_pg_identifier(pg_name)}" + select_parts.append(expr) + else: + select_parts.append("NULL") + select_list = ", ".join(select_parts) + # Quote schema and table names simply (this is not comprehensive for every corner case but avoids psycopg.sql use) + return f"SELECT {select_list} FROM {quote_pg_identifier(schema)}.{quote_pg_identifier(table)}" + + +def validate_column_compatibility( + sqlite_cols: list[SQLiteCol], + pg_columns_map: Dict[str, Tuple[str, str]], + schema: str, + table: str, +) -> None: + for col in sqlite_cols: + lc = col.name.lower() + if lc not in pg_columns_map: + continue + pg_type = pg_columns_map[lc][1] + if not sqlite_decl_satisfies(pg_type, col.type): + raise ValueError( + f"Type mismatch for {schema}.{table}.{col.name}: " + f"PG='{pg_type}' vs SQLite='{col.type}'" + ) + + +def process_row_for_sqlite(row: Tuple, sqlite_cols: list[SQLiteCol]) -> Tuple: + out = [] + for i, val in enumerate(row): + col = sqlite_cols[i] + decl_raw = col.type or "" + decl = decl_raw.upper() + decl_is_empty = decl_raw.strip() == "" + + if isinstance(val, memoryview): + b = val.tobytes() + if "BLOB" in decl or decl_is_empty: + out.append(sqlite3.Binary(b)) + else: + try: + out.append(b.decode("utf-8")) + except UnicodeDecodeError as e: + raise ValueError( + f"UTF-8 decode failed for column '{col.name}': {e}" + ) + elif isinstance(val, (bytes, bytearray)): + b = bytes(val) + if "BLOB" in decl or decl_is_empty: + out.append(sqlite3.Binary(b)) + else: + try: + out.append(b.decode("utf-8")) + except UnicodeDecodeError as e: + raise ValueError( + f"UTF-8 decode failed for column '{col.name}': {e}" + ) + else: + out.append(val) + return tuple(out) + + +def fetch_and_validate_row_batches( + pg_cursor, + select_sql: str, + sqlite_cols: list[SQLiteCol], + batch_size: int = DEFAULT_BATCH_SIZE, +): + pg_cursor.execute(select_sql) + total_rows_seen = 0 + while True: + rows = pg_cursor.fetchmany(batch_size) + if not rows: + break + validated_batch = [] + for row in rows: + total_rows_seen += 1 + try: + validated_batch.append(process_row_for_sqlite(row, sqlite_cols)) + except ValueError as e: + raise ValueError(f"Row {total_rows_seen} validation error: {e}") + yield validated_batch + + +def _get_postgres_sequence_info( + pg_cursor, schema: str, table: str, pk_col: str +) -> Optional[tuple[str, int]]: + """Get PostgreSQL sequence name and last_value for a table primary key, if any. + + Returns (sequence_name_text, last_value) or None. + """ + pg_cursor.execute( + """ + SELECT data_type + FROM information_schema.columns + WHERE table_schema = %s AND table_name = %s AND column_name = %s; + """, + (schema, table, pk_col), + ) + r = pg_cursor.fetchone() + if not r: + return None + + pg_type = (r[0] or "").lower() + # Match whole words to avoid matching 'interval' etc. + if not re.search(r"\b(?:int|serial|bigint)\b", pg_type): + return None + + # Get sequence name text (NULL if none) + pg_cursor.execute( + "SELECT pg_get_serial_sequence(%s, %s);", + (f"{schema}.{table}", pk_col), + ) + row = pg_cursor.fetchone() + seq_name = row[0] if row else None + if not seq_name: + return None + + try: + # Read last_value by casting the pg_get_serial_sequence result to regclass. + pg_cursor.execute( + "SELECT last_value FROM pg_get_serial_sequence(%s, %s)::regclass;", + (f"{schema}.{table}", pk_col), + ) + rr = pg_cursor.fetchone() + if rr and rr[0] is not None: + return (seq_name, int(rr[0])) + except Exception: + # Swallow errors (best-effort); callers treat missing info as absent + pass + + return None + + +def _get_sqlite_max_pk_value(sqlite_cursor, table: str, pk_col: str) -> Optional[int]: + """Get the maximum primary key value from SQLite table.""" + logger = logging.getLogger("db_migrate") + try: + sqlite_cursor.execute( + f"SELECT MAX({quote_sqlite_identifier(pk_col)}) " + f"FROM {quote_sqlite_identifier(table)};" + ) + r3 = sqlite_cursor.fetchone() + if r3 and r3[0] is not None: + return int(r3[0]) + except Exception: + logger.debug("Failed to read sqlite max pk", exc_info=True) + return None + + +def _update_sqlite_sequence( + sqlite_conn: sqlite3.Connection, table: str, sequence_value: int +) -> bool: + """Update SQLite sqlite_sequence with new value. + + IMPORTANT: This function does not commit; caller must commit/rollback. + """ + s_cur = sqlite_conn.cursor() + s_cur.execute( + "UPDATE sqlite_sequence SET seq = ? WHERE name = ?;", + (sequence_value, table), + ) + if s_cur.rowcount == 0: + s_cur.execute( + "INSERT INTO sqlite_sequence(name, seq) VALUES (?, ?);", + (table, sequence_value), + ) + return True + + +def try_update_sqlite_sequence( + pg_cursor, + sqlite_conn: sqlite3.Connection, + schema: str, + pg_table: str, # PostgreSQL table name + sqlite_table: str, # SQLite table name + sqlite_cols: list[SQLiteCol], +) -> bool: + """Update SQLite sequence table based on PostgreSQL sequence values.""" + logger = logging.getLogger("db_migrate") + # Check if table has a single primary key + pks = [c for c in sqlite_cols if c.pk] + if len(pks) != 1: + return False + pk_col = pks[0].name + + # Check if SQLite has AUTOINCREMENT (use sqlite_table) + s_cur = sqlite_conn.cursor() + if not sqlite_table_has_autoincrement(s_cur, sqlite_table): + return False + if not sqlite_sequence_table_exists(s_cur): + return False + + # Get PostgreSQL sequence info (use pg_table) + seq_info = _get_postgres_sequence_info(pg_cursor, schema, pg_table, pk_col) + pg_last_value = seq_info[1] if seq_info else None + + # Get SQLite max PK value (use sqlite_table) + sqlite_max = _get_sqlite_max_pk_value(s_cur, sqlite_table, pk_col) + + # Determine the value to use + if pg_last_value is not None and sqlite_max is not None: + candidate = max(pg_last_value, sqlite_max) + elif pg_last_value is not None: + candidate = pg_last_value + elif sqlite_max is not None: + candidate = sqlite_max + else: + return False + + updated = _update_sqlite_sequence(sqlite_conn, sqlite_table, candidate) + if updated: + try: + sqlite_conn.commit() + except Exception: + logger.debug("Failed to commit sqlite_sequence update", exc_info=True) + # Let caller proceed; treat as best-effort + return updated + + +# ---------------------- +# Flow: per-schema migration +# ---------------------- + + +def migrate_schema( + pg_conn, + sqlite_dir: str, + schema: str, + skipped_tables: Set[str], + logger: logging.Logger, + dry_run: bool = False, + batch_size: int = DEFAULT_BATCH_SIZE, +) -> Tuple[int, int]: + """Migrate a single schema. Returns (tables_migrated, rows_inserted_total).""" + processed_schema = schema[:-7] if schema.endswith("_schema") else schema + sqlite_db_path = Path(sqlite_dir) / f"{processed_schema}.db" + if not sqlite_db_path.is_file(): + logger.error(f"Missing SQLite DB: {sqlite_db_path}") + return 0, 0 + + tables_migrated = 0 + rows_inserted = 0 + + sqlite_path = sqlite_db_path.resolve() + if dry_run: + uri = sqlite_path.as_uri() + "?mode=ro" + conn_args = {"database": uri, "uri": True} + else: + conn_args = {"database": str(sqlite_path)} + + with sqlite3.connect(**conn_args) as sqlite_conn: + sqlite_cur = sqlite_conn.cursor() + sqlite_cur.execute("SELECT name FROM sqlite_master WHERE type='table';") + sqlite_tables = [r[0] for r in sqlite_cur.fetchall()] + # O(1) lookup map for matching by lowercase name + sqlite_table_map = {t.lower(): t for t in sqlite_tables} + + with pg_conn.cursor() as pg_cur: + # Ensure we start in a clean state for this connection + try: + pg_conn.rollback() + except Exception: + # Ignore rollback failure; connection is newly opened most likely + logger.debug( + "pg_conn.rollback() at schema start ignored", exc_info=True + ) + + # Get table list for this schema, but catch/rollback on failure + try: + tables = list_tables_in_schema(pg_cur, schema) + except Exception as e: + logger.error("ERROR %s (list tables): %s", schema, e) + logger.debug("Traceback (list tables):", exc_info=True) + try: + pg_conn.rollback() + except Exception: + logger.debug( + "pg_conn.rollback() failed after list tables error", + exc_info=True, + ) + return 0, 0 + + for table in tables: + # Defensive: ensure connection is in a clean state before any new PG work. + # A prior error can leave the connection in an aborted transaction; calling + # rollback() clears that and allows subsequent SELECTs to run. + try: + pg_conn.rollback() + except Exception: + # ignore: best-effort cleanup + logger.debug( + "pg_conn.rollback() ignored at start of table loop", + exc_info=True, + ) + + if table.lower() in skipped_tables: + logger.info("SKIP %s.%s (explicit)", schema, table) + continue + if table.lower() not in sqlite_table_map: + logger.info("SKIP %s.%s (no target table)", schema, table) + continue + + matched_table = sqlite_table_map[table.lower()] + logger.info("OK %s.%s -> %s", schema, table, matched_table) + + # Fetch PG columns, but defend against aborted transaction here + try: + pg_cols = get_pg_columns(pg_cur, schema, table) + except Exception as e: + logger.error("ERROR %s.%s (get columns): %s", schema, table, e) + logger.debug("Traceback (get columns):", exc_info=True) + try: + pg_conn.rollback() + except Exception: + logger.debug( + "pg_conn.rollback() failed after get columns error", + exc_info=True, + ) + continue + + pg_map: Dict[str, Tuple[str, str]] = { + name.lower(): (name, dtype) for name, dtype in pg_cols + } + + sqlite_info = get_sqlite_table_info(sqlite_cur, matched_table) + if not sqlite_info: + logger.warning(f"SKIP {schema}.{table} (no sqlite info)") + continue + + # Warn once about schema drift (extra columns) + pg_col_names = {name.lower() for name, _ in pg_cols} + extra_in_sqlite = { + c.name for c in sqlite_info if c.name.lower() not in pg_col_names + } + if extra_in_sqlite: + logger.warning( + f"Schema drift: {schema}.{table} has extra SQLite columns {extra_in_sqlite}" + ) + + try: + validate_column_compatibility(sqlite_info, pg_map, schema, table) + except ValueError as e: + logger.error(f"ERROR {schema}.{table} (type mismatch): {e}") + continue + + select_sql = build_select_for_sqlite_columns( + schema, table, sqlite_info, pg_map + ) + + csr_name = _sanitize_cursor_name(f"csr_{schema}_{table}") + try: + with pg_conn.cursor(name=csr_name) as data_cur: + batch_gen = fetch_and_validate_row_batches( + data_cur, select_sql, sqlite_info, batch_size=batch_size + ) + first_batch = next(batch_gen, None) + + if not first_batch: + logger.info(f"SKIP {schema}.{table} (no rows)") + # data_cur will be closed automatically on leaving the 'with' + continue + + if dry_run: + count = len(first_batch) + for batch in batch_gen: + count += len(batch) + inserted = 0 + logger.info( + f"DRY {schema}.{table} rows_validated={count}" + ) + else: + cur = sqlite_conn.cursor() + quoted_table = quote_sqlite_identifier(matched_table) + col_names = [c.name for c in sqlite_info] + quoted_cols = ", ".join( + quote_sqlite_identifier(c) for c in col_names + ) + placeholders = ", ".join(["?"] * len(col_names)) + + cur.execute("BEGIN;") + try: + cur.execute(f"DELETE FROM {quoted_table};") + inserted = 0 + if first_batch: + cur.executemany( + f"INSERT INTO {quoted_table} ({quoted_cols}) VALUES ({placeholders});", + first_batch, + ) + inserted += len(first_batch) + for batch in batch_gen: + if not batch: + continue + cur.executemany( + f"INSERT INTO {quoted_table} ({quoted_cols}) VALUES ({placeholders});", + batch, + ) + inserted += len(batch) + except Exception: + cur.execute("ROLLBACK;") + raise + else: + cur.execute("COMMIT;") + except ValueError as e: + logger.error("ERROR %s.%s (row validation): %s", schema, table, e) + try: + pg_conn.rollback() + except Exception: + logger.debug( + "pg_conn.rollback() failed after row validation error", + exc_info=True, + ) + continue + except Exception as e: + logger.error("ERROR %s.%s (select failed): %s", schema, table, e) + logger.debug("Traceback (select failed):", exc_info=True) + try: + pg_conn.rollback() + except Exception: + logger.debug( + "pg_conn.rollback() failed after select failed", + exc_info=True, + ) + continue + + rows_inserted += inserted + tables_migrated += 1 + logger.info(f"DONE {schema}.{table} rows={inserted}") + + if not dry_run: + try: + if try_update_sqlite_sequence( + pg_cur, + sqlite_conn, + schema, + table, + matched_table, + sqlite_info, + ): + logger.info( + "SEQ %s.%s sqlite_sequence updated", schema, table + ) + except Exception as e: + logger.warning( + "SEQ %s.%s update failed (ignored): %s", schema, table, e + ) + + return tables_migrated, rows_inserted + + +def migrate_data( + pg_conn_str: str, + sqlite_dir: str, + schema_filter: Optional[str], + dry_run: bool, + skip_tables: str, + logger: logging.Logger, + batch_size: int, +) -> None: + skipped_tables = {t.strip().lower() for t in skip_tables.split(",") if t.strip()} + total_tables = 0 + total_rows = 0 + total_errors = 0 + + # List schemas once with a short-lived connection + with psycopg.connect(pg_conn_str) as tmp_conn: + with tmp_conn.cursor() as cur: + schemas = list_user_schemas(cur) + + if schema_filter: + schemas = [s for s in schemas if s == schema_filter] + if not schemas: + logger.error("No schemas to process") + return + + for schema in schemas: + logger.info("SCHEMA %s", schema) + if dry_run: + logger.info("(dry-run) validating only — no writes will be performed") + + # Use a fresh connection per-schema to isolate failures/aborted transactions + try: + with psycopg.connect(pg_conn_str) as pg_conn: + migrated_tables, inserted_rows = migrate_schema( + pg_conn, + sqlite_dir, + schema, + skipped_tables, + logger, + dry_run=dry_run, + batch_size=batch_size, + ) + except Exception as e: + logger.error("Schema %s failed: %s", schema, e) + logger.debug("Traceback (schema failure):", exc_info=True) + total_errors += 1 + continue + + total_tables += migrated_tables + total_rows += inserted_rows + + logger.info("---") + logger.info( + f"SUMMARY: schemas={len(schemas)} " + f"tables_migrated={total_tables} " + f"rows_inserted={total_rows} " + f"errors={total_errors}" + ) + + +# ---------------------- +# CLI +# ---------------------- + + +def parse_args(argv): + p = argparse.ArgumentParser( + description="Migrate Postgres data into per-schema SQLite DBs (non-invasive)." + ) + p.add_argument("pg_conn", help="Postgres connection string") + p.add_argument( + "sqlite_dir", help="Directory containing per-schema sqlite .db files" + ) + p.add_argument( + "--schema", help="Only migrate this schema (exact match)", default=None + ) + p.add_argument( + "--dry-run", + help="Validate and report only; do not write to sqlite", + action="store_true", + ) + p.add_argument("--verbose", help="Verbose logging (debug)", action="store_true") + p.add_argument("--no-color", help="Disable ANSI color output", action="store_true") + p.add_argument( + "--batch-size", + type=int, + default=DEFAULT_BATCH_SIZE, + help="Batch size for data fetching", + ) + p.add_argument( + "--skip-tables", + help="Comma-separated list of tables to skip", + default="migrations,servers_stats", + ) + + return p.parse_args(argv[1:]) + + +def main(argv): + args = parse_args(argv) + logger = setup_logger(args.verbose, args.no_color) + + sqlite_dir_path = Path(args.sqlite_dir) + if not sqlite_dir_path.is_dir(): + logger.error("SQLite directory does not exist or is not a directory.") + raise SystemExit(1) + + try: + migrate_data( + args.pg_conn, + str(sqlite_dir_path), + args.schema, + args.dry_run, + args.skip_tables, + logger, + args.batch_size, + ) + except Exception as e: + logger.error(f"Fatal error: {e}") + raise SystemExit(2) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/scripts/db/sqlite.load b/scripts/db/sqlite.load index 105e8c39d2..7f60e72c2f 100644 --- a/scripts/db/sqlite.load +++ b/scripts/db/sqlite.load @@ -10,7 +10,8 @@ LOAD DATABASE -- pgloader implementation doesn't find "GENERATED ALWAYS AS IDENTITY" sequences, -- instead we reset sequences manually via custom query after load reset no sequences, - data only + data only, + workers = {{WORKERS}} EXCLUDING TABLE NAMES LIKE 'migrations', 'sqlite_sequence' From c07c17689606c27442360e114532e7cce60dd07d Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 16 Sep 2025 21:58:28 +0100 Subject: [PATCH 042/137] core: batch load queues for subscriptions with PostgreSQL client (#6272) * core: batch load queues for subscriptions with PostgreSQL client * update simplexmq * fix, update simplexmq * update query plans --- cabal.project | 2 +- scripts/db/README.md | 76 ++++--------------- scripts/desktop/build-lib-mac.sh | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Options/Postgres.hs | 4 +- .../SQLite/Migrations/agent_query_plans.txt | 25 +++--- tests/ChatClient.hs | 2 +- tests/PostgresSchemaDump.hs | 6 +- 8 files changed, 35 insertions(+), 84 deletions(-) diff --git a/cabal.project b/cabal.project index df72f5da32..a414dbcd5b 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 112cd9d5f44055b6d9b12cb918a1f108f78726f8 + tag: 7b7616ce7e3ba40eb101d20c5dccd79a14de5580 source-repository-package type: git diff --git a/scripts/db/README.md b/scripts/db/README.md index 333a6f4acf..c204e036c4 100644 --- a/scripts/db/README.md +++ b/scripts/db/README.md @@ -10,33 +10,13 @@ sqlcipher simplex_v1_agent.db ``` - 2. Set your db password: + 2. Run in sqlcipher: ```sql - PRAGMA key = ''; - ``` - - 3. Check if db was successfully decrypted: - - ```sh - SELECT count(*) FROM sqlite_master; - ``` - - 4. Attach new empty db: - - ```sh - ATTACH DATABASE 'simplex_v1_agent_plaintext.db' AS plaintext KEY ''; - ``` - - 5. Export opened db to attached db as plaintext: - - ```sh - SELECT sqlcipher_export('plaintext'); - ``` - - 6. Deattach the plaintext db: - - ```sh + PRAGMA key = ''; -- Set your db password + SELECT count(*) FROM sqlite_master; -- Check if db was successfully decrypted + ATTACH DATABASE 'simplex_v1_agent_plaintext.db' AS plaintext KEY ''; -- Attach new empty db + SELECT sqlcipher_export('plaintext'); -- Export opened db to attached db as plaintext DETACH DATABASE plaintext; ``` @@ -48,33 +28,13 @@ sqlcipher simplex_v1_chat.db ``` - 2. Set your db password: + 2. Run in sqlcipher: ```sql PRAGMA key = ''; - ``` - - 3. Check if db was successfully decrypted: - - ```sh SELECT count(*) FROM sqlite_master; - ``` - - 4. Attach new empty db: - - ```sh ATTACH DATABASE 'simplex_v1_chat_plaintext.db' AS plaintext KEY ''; - ``` - - 5. Export opened db to attached db as plaintext: - - ```sh SELECT sqlcipher_export('plaintext'); - ``` - - 6. Deattach the plaintext db: - - ```sh DETACH DATABASE plaintext; ``` @@ -86,22 +46,14 @@ psql -U postgres -h localhost ``` - 2. Create user with password: + 2. Run in psql: - ```sh - CREATE USER simplex WITH ENCRYPTED PASSWORD '123123'; - ``` - - 3. Create database: - - ```sh - CREATE DATABASE simplex_v1; - ``` - - 4. Assign permissions: - - ```sh - GRANT ALL PRIVILEGES ON DATABASE simplex_v1 TO simplex; + ```sql + CREATE USER simplex WITH ENCRYPTED PASSWORD '123123'; -- Create user with password + -- or + -- CREATE USER simplex; + CREATE DATABASE simplex_v1; -- Create database + GRANT ALL PRIVILEGES ON DATABASE simplex_v1 TO simplex; -- Assign permissions ``` 3. Prepare database: @@ -294,7 +246,7 @@ ./gradlew packageDmg -Pdatabase.backend=postgres ``` -## Transfer data from SQLite to Postgres database +## Transfer data from Postgres to SQLite database 1. Prepare sqlite db: diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index a3a824f81b..6e3415bd80 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -28,7 +28,7 @@ rm -rf $BUILD_DIR if [[ "$DATABASE_BACKEND" == "postgres" ]]; then echo "Building with postgres backend..." - cabal build -f client_postgres lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -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' + cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -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/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index c8404afde4..cccdc96785 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."112cd9d5f44055b6d9b12cb918a1f108f78726f8" = "18jp2jrq7a4giqvqpvd3jjrbznwwl1dsh1ymmq3197xdd515wxr1"; + "https://github.com/simplex-chat/simplexmq.git"."7b7616ce7e3ba40eb101d20c5dccd79a14de5580" = "0ccscn8xx09fj8n7721cn8ynligjniwrpzgj2hi1348j1vm8h2j8"; "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"; diff --git a/src/Simplex/Chat/Options/Postgres.hs b/src/Simplex/Chat/Options/Postgres.hs index 46c1faa5e0..13af13b20a 100644 --- a/src/Simplex/Chat/Options/Postgres.hs +++ b/src/Simplex/Chat/Options/Postgres.hs @@ -42,7 +42,7 @@ chatDbOptsP _appDir defaultDbName = do ( long "pool-size" <> metavar "DB_POOL_SIZE" <> help "Database connection pool size" - <> value 1 + <> value 10 <> showDefault ) dbCreateSchema <- @@ -81,7 +81,7 @@ mobileDbOpts schemaPrefix connstr = do ChatDbOpts { dbConnstr, dbSchemaPrefix, - dbPoolSize = 1, + dbPoolSize = 10, dbCreateSchema = True } diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index 41f01b3b51..b2b4cd87ae 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -327,16 +327,6 @@ Plan: SEARCH r USING INTEGER PRIMARY KEY (rowid=?) SEARCH s USING INTEGER PRIMARY KEY (rowid=?) -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 = ? - -Plan: -SEARCH connections USING PRIMARY KEY (conn_id=?) - Query: SELECT DISTINCT s.xftp_host, s.xftp_port, s.xftp_key_hash @@ -384,13 +374,13 @@ SEARCH s USING INTEGER PRIMARY KEY (rowid=?) USE TEMP B-TREE FOR DISTINCT Query: - SELECT DISTINCT c.host, c.port, COALESCE(c.server_key_hash, s.key_hash) + SELECT DISTINCT c.conn_id, c.host, c.port, COALESCE(c.server_key_hash, s.key_hash) FROM commands c LEFT JOIN servers s ON s.host = c.host AND s.port = c.port - WHERE conn_id = ? + ORDER BY c.conn_id Plan: -SEARCH c USING INDEX idx_commands_conn_id (conn_id=?) +SCAN c USING INDEX idx_commands_conn_id SEARCH s USING PRIMARY KEY (host=? AND port=?) LEFT-JOIN USE TEMP B-TREE FOR DISTINCT @@ -469,6 +459,15 @@ 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 = ? diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 5e5c34d516..3f005cb915 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -297,7 +297,7 @@ startTestChat ps cfg opts@ChatOpts {coreOptions} dbPrefix = do createDatabase :: TestParams -> CoreChatOpts -> String -> IO (Either MigrationError ChatDatabase) #if defined(dbPostgres) createDatabase _params CoreChatOpts {dbOptions} dbPrefix = do - createChatDatabase dbOptions {dbSchemaPrefix = "client_" <> dbPrefix} MCError + createChatDatabase dbOptions {dbSchemaPrefix = "client_" <> dbPrefix} (MigrationConfig MCError Nothing) insertUser :: DBStore -> IO () insertUser st = withTransaction st (`DB.execute_` "INSERT INTO users DEFAULT VALUES") diff --git a/tests/PostgresSchemaDump.hs b/tests/PostgresSchemaDump.hs index f1b509ac6f..468cdb19d5 100644 --- a/tests/PostgresSchemaDump.hs +++ b/tests/PostgresSchemaDump.hs @@ -14,7 +14,7 @@ import Data.Maybe (fromJust, isJust) import Simplex.Messaging.Agent.Store.Postgres (closeDBStore, createDBStore) import Simplex.Messaging.Agent.Store.Postgres.Common (DBOpts (..)) import qualified Simplex.Messaging.Agent.Store.Postgres.Migrations as Migrations -import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfirmation (..), MigrationsToRun (..), toDownMigration) +import Simplex.Messaging.Agent.Store.Shared (Migration (..), MigrationConfig (..), MigrationConfirmation (..), MigrationsToRun (..), toDownMigration) import Simplex.Messaging.Util (ifM, whenM) import System.Directory (doesFileExist, removeFile) import System.Process (readCreateProcess, shell) @@ -32,12 +32,12 @@ postgresSchemaDumpTest migrations skipComparisonForDownMigrations testDBOpts@DBO testVerifySchemaDump = do savedSchema <- ifM (doesFileExist srcSchemaPath) (readFile srcSchemaPath) (pure "") savedSchema `deepseq` pure () - void $ createDBStore testDBOpts migrations MCConsole + void $ createDBStore testDBOpts migrations (MigrationConfig MCConsole Nothing) getSchema srcSchemaPath `shouldReturn` savedSchema testSchemaMigrations = do let noDownMigrations = dropWhileEnd (\Migration {down} -> isJust down) migrations - st <- createDBStore testDBOpts noDownMigrations MCYesUpDown >>= \case + st <- createDBStore testDBOpts noDownMigrations (MigrationConfig MCYesUpDown Nothing) >>= \case Right st -> pure st Left e -> error $ show e mapM_ (testDownMigration st) $ drop (length noDownMigrations) migrations From 6a8f973eca740f011991f5b6c1d8776941481877 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 17 Sep 2025 07:57:02 +0000 Subject: [PATCH 043/137] core: autoincrement for delivery tasks and jobs id (#6277) --- .../Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs | 4 ++-- src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs index dfafff674f..3163619477 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250813_delivery_tasks.hs @@ -56,7 +56,7 @@ m20250813_delivery_tasks :: Query m20250813_delivery_tasks = [sql| CREATE TABLE delivery_tasks ( - delivery_task_id INTEGER PRIMARY KEY, + delivery_task_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, worker_scope TEXT NOT NULL, job_scope_spec_tag TEXT, @@ -107,7 +107,7 @@ CREATE INDEX idx_delivery_tasks_created_at ON delivery_tasks(created_at); CREATE TABLE delivery_jobs ( - delivery_job_id INTEGER PRIMARY KEY, + delivery_job_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, worker_scope TEXT NOT NULL, job_scope_spec_tag TEXT, diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 356990bdee..6fad0d669e 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -693,7 +693,7 @@ CREATE TABLE chat_item_mentions( display_name TEXT NOT NULL ); CREATE TABLE delivery_tasks( - delivery_task_id INTEGER PRIMARY KEY, + delivery_task_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, worker_scope TEXT NOT NULL, job_scope_spec_tag TEXT, @@ -709,7 +709,7 @@ CREATE TABLE delivery_tasks( updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); CREATE TABLE delivery_jobs( - delivery_job_id INTEGER PRIMARY KEY, + delivery_job_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, worker_scope TEXT NOT NULL, job_scope_spec_tag TEXT, From fa0103c7f5eb211293936fc601d02823d51f47c8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 17 Sep 2025 09:13:28 +0100 Subject: [PATCH 044/137] core: update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 48ac0dc24c..0452f09216 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 8a7991a3768d632346c8d8f3c319ed64ccba93b6 + tag: 54a2a6c9051f610b8c7533369d9e9cce81af06ad source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 09a1407a31..1ef1b2f51f 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."8a7991a3768d632346c8d8f3c319ed64ccba93b6" = "14p3436bvbxrcx3sp2gy97ycc7v0wzhar8vvxa6lcs8anki1va08"; + "https://github.com/simplex-chat/simplexmq.git"."54a2a6c9051f610b8c7533369d9e9cce81af06ad" = "02c6hh5vffm23gz2gkn6rq2ifcyj5c9s6kz0kyk055i78gsfhp43"; "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"; From 429ec9d21a0ab24ddd19240c52023f1b3bec3b43 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 18 Sep 2025 17:52:47 +0100 Subject: [PATCH 045/137] directory: listings for web pages (#6281) * directory: listings for web pages * refactor * tests --- apps/simplex-directory-service/Main.hs | 8 +- .../src/Directory/Events.hs | 14 +- .../src/Directory/Listing.hs | 97 +++++++++ .../src/Directory/Options.hs | 9 + .../src/Directory/Service.hs | 185 +++++++++++------- .../src/Directory/Store.hs | 144 +++++++++----- .../src/Directory/Util.hs | 31 +++ simplex-chat.cabal | 6 + tests/Bots/DirectoryTests.hs | 83 ++++++-- 9 files changed, 437 insertions(+), 140 deletions(-) create mode 100644 apps/simplex-directory-service/src/Directory/Listing.hs create mode 100644 apps/simplex-directory-service/src/Directory/Util.hs diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs index 2091ab444b..e2b96f5677 100644 --- a/apps/simplex-directory-service/Main.hs +++ b/apps/simplex-directory-service/Main.hs @@ -17,5 +17,9 @@ main = do then directoryServiceCLI st opts else do env <- newServiceState opts - let cfg = terminalChatConfig {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}} - simplexChatCore cfg (mkChatOpts opts) $ directoryService st opts env + let chatHooks = + defaultChatHooks + { postStartHook = Just $ directoryStartHook st opts, + acceptMember = Just $ acceptMemberHook opts env + } + simplexChatCore (terminalChatConfig {chatHooks}) (mkChatOpts opts) $ directoryService st opts env diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 8ae7f60b3d..1f075c677c 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -135,6 +135,7 @@ data DirectoryCmdTag (r :: DirectoryRole) where DCInviteOwnerToGroup_ :: DirectoryCmdTag 'DRAdmin -- DCAddBlockedWord_ :: DirectoryCmdTag 'DRAdmin -- DCRemoveBlockedWord_ :: DirectoryCmdTag 'DRAdmin + DCPromoteGroup_ :: DirectoryCmdTag 'DRSuperUser DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser deriving instance Show (DirectoryCmdTag r) @@ -157,7 +158,7 @@ data DirectoryCmd (r :: DirectoryRole) where DCMemberRole :: UserGroupRegId -> Maybe GroupName -> Maybe GroupMemberRole -> DirectoryCmd 'DRUser DCGroupFilter :: UserGroupRegId -> Maybe GroupName -> Maybe DirectoryMemberAcceptance -> DirectoryCmd 'DRUser DCShowUpgradeGroupLink :: GroupId -> Maybe GroupName -> DirectoryCmd 'DRUser - DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRAdmin + DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId, promote :: Maybe Bool} -> DirectoryCmd 'DRAdmin DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin @@ -167,6 +168,7 @@ data DirectoryCmd (r :: DirectoryRole) where DCInviteOwnerToGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin -- DCAddBlockedWord :: Text -> DirectoryCmd 'DRAdmin -- DCRemoveBlockedWord :: Text -> DirectoryCmd 'DRAdmin + DCPromoteGroup :: GroupId -> GroupName -> Bool -> DirectoryCmd 'DRSuperUser DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser DCUnknownCommand :: DirectoryCmd 'DRUser DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r @@ -211,6 +213,7 @@ directoryCmdP = "invite" -> au DCInviteOwnerToGroup_ -- "block_word" -> au DCAddBlockedWord_ -- "unblock_word" -> au DCRemoveBlockedWord_ + "promote" -> su DCPromoteGroup_ "exec" -> su DCExecuteCommand_ "x" -> su DCExecuteCommand_ _ -> fail "bad command tag" @@ -270,7 +273,8 @@ directoryCmdP = DCApproveGroup_ -> do (groupId, displayName) <- gc (,) groupApprovalId <- A.space *> A.decimal - pure DCApproveGroup {groupId, displayName, groupApprovalId} + promote <- Just <$> (" promote=" *> onOffP) <|> pure Nothing + pure DCApproveGroup {groupId, displayName, groupApprovalId, promote} DCRejectGroup_ -> gc DCRejectGroup DCSuspendGroup_ -> gc DCSuspendGroup DCResumeGroup_ -> gc DCResumeGroup @@ -283,12 +287,17 @@ directoryCmdP = DCInviteOwnerToGroup_ -> gc DCInviteOwnerToGroup -- DCAddBlockedWord_ -> DCAddBlockedWord <$> wordP -- DCRemoveBlockedWord_ -> DCRemoveBlockedWord <$> wordP + DCPromoteGroup_ -> do + (groupId, displayName) <- gc (,) + promote <- A.space *> onOffP + pure $ DCPromoteGroup groupId displayName promote DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (spacesP *> A.takeText) where gc f = f <$> (spacesP *> A.decimal) <*> (A.char ':' *> displayNameTextP) gc_ f = f <$> (spacesP *> A.decimal) <*> optional (A.char ':' *> displayNameTextP) -- wordP = spacesP *> A.takeTill isSpace spacesP = A.takeWhile1 isSpace + onOffP = (A.string "on" $> True) <|> (A.string "off" $> False) directoryCmdTag :: DirectoryCmd r -> Text directoryCmdTag = \case @@ -314,6 +323,7 @@ directoryCmdTag = \case DCInviteOwnerToGroup {} -> "invite" -- DCAddBlockedWord _ -> "block_word" -- DCRemoveBlockedWord _ -> "unblock_word" + DCPromoteGroup {} -> "promote" DCExecuteCommand _ -> "exec" DCUnknownCommand -> "unknown" DCCommandError _ -> "error" diff --git a/apps/simplex-directory-service/src/Directory/Listing.hs b/apps/simplex-directory-service/src/Directory/Listing.hs new file mode 100644 index 0000000000..a05e82285d --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Listing.hs @@ -0,0 +1,97 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Directory.Listing where + +import Control.Applicative ((<|>)) +import Control.Concurrent.STM +import Control.Monad +import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import Data.ByteString (ByteString) +import Data.ByteString.Base64 as B64 +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy as LB +import Data.Maybe (fromMaybe) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) +import Directory.Store +import Simplex.Chat.Types +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON) +import System.Directory +import System.FilePath + +listingFileName :: String +listingFileName = "listing.json" + +promotedFileName :: String +promotedFileName = "promoted.json" + +listingImageFolder :: String +listingImageFolder = "images" + +data DirectoryEntryType = DETGroup + { admission :: Maybe GroupMemberAdmission, + summary :: GroupSummary + } + +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "DET") ''DirectoryEntryType) + +data DirectoryEntry = DirectoryEntry + { entryType :: DirectoryEntryType, + displayName :: Text, + shortDescr :: Maybe Text, + welcomeMessage :: Maybe Text, + imageFile :: Maybe String + } + +$(JQ.deriveJSON defaultJSON ''DirectoryEntry) + +data DirectoryListing = DirectoryListing {entries :: [DirectoryEntry]} + +$(JQ.deriveJSON defaultJSON ''DirectoryListing) + +type ImageFileData = ByteString + +groupDirectoryEntry :: GroupInfoSummary -> (DirectoryEntry, Maybe (FilePath, ImageFileData)) +groupDirectoryEntry (GIS GroupInfo {groupId, groupProfile} summary) = + let GroupProfile {displayName, shortDescr, description, image, memberAdmission} = groupProfile + entryType = DETGroup memberAdmission summary + imgData = imgFileData =<< image + in (DirectoryEntry {entryType, displayName, shortDescr, welcomeMessage = description, imageFile = fst <$> imgData}, imgData) + where + imgFileData (ImageData img) = + let (img', imgExt) = + fromMaybe (img, ".jpg") $ + (,".jpg") <$> T.stripPrefix "data:image/jpg;base64," img + <|> (,".png") <$> T.stripPrefix "data:image/png;base64," img + imgFile = listingImageFolder show groupId <> imgExt + in case B64.decode $ encodeUtf8 img' of + Right img'' -> Just (imgFile, img'') + Left _ -> Nothing + +generateListing :: DirectoryStore -> FilePath -> [GroupInfoSummary] -> IO () +generateListing st dir gs = do + gs' <- filterListedGroups st gs + removePathForcibly (dir listingImageFolder) + createDirectoryIfMissing True (dir listingImageFolder) + gs'' <- forM gs' $ \g@(GIS GroupInfo {groupId} _) -> do + let (g', img) = groupDirectoryEntry g + forM_ img $ \(imgFile, imgData) -> B.writeFile (dir imgFile) imgData + pure (groupId, g') + saveListing listingFileName gs'' + saveListing promotedFileName =<< filterPromotedGroups st gs'' + where + saveListing f = LB.writeFile (dir f) . J.encode . DirectoryListing . map snd + +filterPromotedGroups :: DirectoryStore -> [(GroupId, DirectoryEntry)] -> IO [(GroupId, DirectoryEntry)] +filterPromotedGroups st gs = do + pgs <- readTVarIO $ promotedGroups st + pure $ filter (\g -> fst g `S.member` pgs) gs diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 7ad3512fe9..e0052b3b1e 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -33,6 +33,7 @@ data DirectoryOpts = DirectoryOpts serviceName :: T.Text, runCLI :: Bool, searchResults :: Int, + webFolder :: Maybe FilePath, testing :: Bool } @@ -124,6 +125,13 @@ directoryOpts appDir defaultDbName = do ( long "run-cli" <> help "Run directory service as CLI" ) + webFolder <- + optional $ + strOption + ( long "web-folder" + <> metavar "WEB_FOLDER" + <> help "Folder to store static web assets" + ) pure DirectoryOpts { coreOptions, @@ -140,6 +148,7 @@ directoryOpts appDir defaultDbName = do serviceName = T.pack serviceName, runCLI, searchResults = 10, + webFolder, testing = False } diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index f95b04dee1..7bbe4e43a4 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -15,14 +15,14 @@ module Directory.Service directoryService, directoryServiceCLI, newServiceState, - acceptMemberHook + directoryStartHook, + acceptMemberHook, ) where import Control.Concurrent (forkIO) import Control.Concurrent.Async import Control.Concurrent.STM -import qualified Control.Exception as E import Control.Logger.Simple import Control.Monad import Control.Monad.Except @@ -41,20 +41,22 @@ import Data.Time.LocalTime (getCurrentTimeZone) import Directory.BlockedWords import Directory.Captcha import Directory.Events +import Directory.Listing import Directory.Options import Directory.Search import Directory.Store +import Directory.Util import Simplex.Chat.Bot import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller import Simplex.Chat.Core -import Simplex.Chat.Markdown (FormattedText (..), Format (..), parseMaybeMarkdownList, viewName) +import Simplex.Chat.Markdown (Format (..), FormattedText (..), parseMaybeMarkdownList, viewName) import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Store (GroupLink (..)) import Simplex.Chat.Store.Direct (getContact) -import Simplex.Chat.Store.Groups (getGroupInfo, getGroupLink, getGroupSummary, setGroupCustomData) +import Simplex.Chat.Store.Groups (getGroupInfo, getGroupLink, getGroupSummary, getUserGroupsWithSummary, setGroupCustomData) import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo) import Simplex.Chat.Store.Shared (StoreError (..)) import Simplex.Chat.Terminal (terminalChatConfig) @@ -63,14 +65,11 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) -import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..)) -import Simplex.Messaging.Agent.Store.Common (withTransaction) -import Simplex.Messaging.Agent.Protocol (SConnectionMode (..), sameConnReqContact, sameShortLinkContact) -import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact) import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) +import Simplex.Messaging.Util (safeDecodeUtf8, tshow, unlessM, whenM, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) import System.Exit (exitFailure) import System.Process (readProcess) @@ -147,7 +146,7 @@ directoryServiceCLI st opts = do env <- newServiceState opts eventQ <- newTQueueIO let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) - chatHooks = defaultChatHooks {postStartHook = Just postStartHook, eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env} + chatHooks = defaultChatHooks {postStartHook = Just $ directoryStartHook st opts, eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env} race_ (simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing) (processEvents eventQ env) @@ -156,18 +155,22 @@ directoryServiceCLI st opts = do (cc, resp) <- atomically $ readTQueue eventQ u_ <- readTVarIO (currentUser cc) forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp - postStartHook cc = - readTVarIO (currentUser cc) >>= \case - Nothing -> putStrLn "No current user" >> exitFailure - Just User {userId, profile = p@LocalProfile {preferences}} -> do - let cmds = fromMaybe [] $ preferences >>= commands_ - unless (cmds == directoryCommands) $ do - let prefs = (fromMaybe emptyChatPrefs preferences) {files = Just FilesPreference {allow = FANo}, commands = Just directoryCommands} :: Preferences - p' = (fromLocalProfile p) {displayName = serviceName opts, peerType = Just CPTBot, preferences = Just prefs} :: Profile - liftIO $ sendChatCmd cc (APIUpdateProfile userId p') >>= \case - Right CRUserProfileUpdated {} -> putStrLn "Updated directory commands" - Right r -> putStrLn ("Error: unexpected response " <> show r) >> exitFailure - Left e -> putStrLn ("Error: " <> show e) >> exitFailure + +directoryStartHook :: DirectoryStore -> DirectoryOpts -> ChatController -> IO () +directoryStartHook st opts cc = + readTVarIO (currentUser cc) >>= \case + Nothing -> putStrLn "No current user" >> exitFailure + Just user@User {userId, profile = p@LocalProfile {preferences}} -> do + forM_ (webFolder opts) $ updateGroupListingFiles cc st user + let cmds = fromMaybe [] $ preferences >>= commands_ + unless (cmds == directoryCommands) $ do + let prefs = (fromMaybe emptyChatPrefs preferences) {files = Just FilesPreference {allow = FANo}, commands = Just directoryCommands} :: Preferences + p' = (fromLocalProfile p) {displayName = serviceName opts, peerType = Just CPTBot, preferences = Just prefs} :: Profile + liftIO $ + sendChatCmd cc (APIUpdateProfile userId p') >>= \case + Right CRUserProfileUpdated {} -> putStrLn "Updated directory commands" + Right r -> putStrLn ("Error: unexpected response " <> show r) >> exitFailure + Left e -> putStrLn ("Error: " <> show e) >> exitFailure directoryCommands :: [ChatBotCommand] directoryCommands = @@ -323,7 +326,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName void $ addGroupReg st ct g GRSProposed r <- sendChatCmd cc $ APIJoinGroup groupId MFNone sendMessage cc ct $ case r of - Right CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" + Right CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" _ -> "Error joining group " <> displayName <> ", please re-send the invitation!" deContactConnected :: Contact -> IO () @@ -392,7 +395,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case Right CRGroupLinkCreated {groupLink = GroupLink {connLinkContact = gLink}} -> do - setGroupStatus st gr GRSPendingUpdate + setGroupStatus st opts cc user gr GRSPendingUpdate notifyOwner gr "Created the public link to join the group via this directory service that is always online.\n\n\ @@ -431,7 +434,9 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName GPServiceLinkError -> do notifyOwner gr $ ("Error: " <> serviceName <> " has no group link for " <> userGroupRef) - <> " after profile was updated" <> byMember <> ". Please report the error to the developers." + <> " after profile was updated" + <> byMember + <> ". Please report the error to the developers." logError $ "Error: no group link for " <> userGroupRef GRSPendingApproval n -> processProfileChange gr byMember False $ n + 1 GRSActive -> processProfileChange gr byMember True 1 @@ -451,7 +456,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just DGReserved -> notifyOwner gr $ groupAlreadyListed toGroup _ -> do let gaId = 1 - setGroupStatus st gr $ GRSPendingApproval gaId + setGroupStatus st opts cc user gr $ GRSPendingApproval gaId notifyOwner gr $ ("Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message" <> byMember) <> ".\nYou will be notified once the group is added to the directory - it may take up to 48 hours." @@ -461,18 +466,18 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName groupRef = groupReference toGroup groupProfileUpdate >>= \case GPNoServiceLink -> do - setGroupStatus st gr GRSPendingUpdate + setGroupStatus st opts cc user gr GRSPendingUpdate notifyOwner gr $ ("The group profile is updated for " <> userGroupRef <> byMember <> ", but no link is added to the welcome message.\n\n") <> "The group will remain hidden from the directory until the group link is added and the group is re-approved." GPServiceLinkRemoved -> do - setGroupStatus st gr GRSPendingUpdate + setGroupStatus st opts cc user gr GRSPendingUpdate notifyOwner gr $ ("The group link for " <> userGroupRef <> " is removed from the welcome message" <> byMember) <> ".\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." notifyAdminUsers $ "The group link is removed from " <> groupRef <> ", de-listed." GPServiceLinkAdded _ -> do - setGroupStatus st gr $ GRSPendingApproval n' + setGroupStatus st opts cc user gr $ GRSPendingApproval n' notifyOwner gr $ ("The group link is added to " <> userGroupRef <> byMember) <> "!\nIt is hidden from the directory until approved." @@ -485,7 +490,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName <> "!\nThe group is listed in directory." notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> " - only link or whitespace changes.\nThe group remained listed in directory." | otherwise -> do - setGroupStatus st gr $ GRSPendingApproval n' + setGroupStatus st opts cc user gr $ GRSPendingApproval n' notifyOwner gr $ ("The group " <> userGroupRef <> " is updated" <> byMember) <> "!\nIt is hidden from the directory until approved." @@ -523,7 +528,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just Nothing -> sendToApprove toGroup gr gaId dePendingMember :: GroupInfo -> GroupMember -> IO () - dePendingMember g@GroupInfo {groupProfile = GroupProfile {displayName}} m + dePendingMember g@GroupInfo {groupProfile = GroupProfile {displayName}} m | memberRequiresCaptcha a m = sendMemberCaptcha g m Nothing captchaNotice 0 | otherwise = approvePendingMember a g m where @@ -600,7 +605,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName useMemberFilter image $ passCaptcha a sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () - sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do + sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId, promoted} gaId = do + -- TODO account for promotion ct_ <- getContact' cc user dbContactId gr_ <- getGroupAndSummary cc user dbGroupId let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_ @@ -608,9 +614,10 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ <> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:") msg = maybe (MCText text) (\image -> MCImage {text, image}) image' + promote <- readTVarIO promoted withAdminUsers $ \cId -> do sendComposedMessage' cc cId Nothing msg - sendMessage' cc cId $ "/approve " <> tshow dbGroupId <> ":" <> viewName displayName <> " " <> tshow gaId + sendMessage' cc cId $ "/approve " <> tshow dbGroupId <> ":" <> viewName displayName <> " " <> tshow gaId <> if promote then " promote=on" else "" deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do @@ -621,14 +628,14 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName when (ctId `isOwner` gr) $ do readTVarIO (groupRegStatus gr) >>= \case GRSSuspendedBadRoles -> when (rStatus == GRSOk) $ do - setGroupStatus st gr GRSActive + setGroupStatus st opts cc user gr GRSActive notifyOwner gr $ uCtRole <> ".\n\nThe group is listed in the directory again." notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suCtRole GRSPendingApproval gaId -> when (rStatus == GRSOk) $ do sendToApprove g gr gaId notifyOwner gr $ uCtRole <> ".\n\nThe group is submitted for approval." GRSActive -> when (rStatus /= GRSOk) $ do - setGroupStatus st gr GRSSuspendedBadRoles + setGroupStatus st opts cc user gr GRSSuspendedBadRoles notifyOwner gr $ uCtRole <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole _ -> pure () @@ -647,7 +654,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName readTVarIO (groupRegStatus gr) >>= \case GRSSuspendedBadRoles -> when (serviceRole == GRAdmin) $ whenContactIsOwner gr $ do - setGroupStatus st gr GRSActive + setGroupStatus st opts cc user gr GRSActive notifyOwner gr $ uSrvRole <> ".\n\nThe group is listed in the directory again." notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole GRSPendingApproval gaId -> when (serviceRole == GRAdmin) $ @@ -655,7 +662,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName sendToApprove g gr gaId notifyOwner gr $ uSrvRole <> ".\n\nThe group is submitted for approval." GRSActive -> when (serviceRole /= GRAdmin) $ do - setGroupStatus st gr GRSSuspendedBadRoles + setGroupStatus st opts cc user gr GRSSuspendedBadRoles notifyOwner gr $ uSrvRole <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole _ -> pure () @@ -672,7 +679,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g withGroupReg g "contact removed" $ \gr -> do when (ctId `isOwner` gr) $ do - setGroupStatus st gr GRSRemoved + setGroupStatus st opts cc user gr GRSRemoved notifyOwner gr $ "You are removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." @@ -681,7 +688,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g withGroupReg g "contact left" $ \gr -> do when (ctId `isOwner` gr) $ do - setGroupStatus st gr GRSRemoved + setGroupStatus st opts cc user gr GRSRemoved notifyOwner gr $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." @@ -689,7 +696,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deServiceRemovedFromGroup g = do logInfo $ "service removed from group " <> viewGroupName g withGroupReg g "service removed" $ \gr -> do - setGroupStatus st gr GRSRemoved + setGroupStatus st opts cc user gr GRSRemoved notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." @@ -697,7 +704,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deGroupDeleted g = do logInfo $ "group removed " <> viewGroupName g withGroupReg g "group removed" $ \gr -> do - setGroupStatus st gr GRSRemoved + setGroupStatus st opts cc user gr GRSRemoved notifyOwner gr $ "The group " <> userGroupReference gr g <> " is deleted.\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group is deleted)." @@ -817,12 +824,13 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName withGroupLinkResult groupRef (sendChatCmd cc $ APIGetGroupLink groupId) $ \GroupLink {connLinkContact = gLink@(CCLink _ sLnk_), acceptMemberRole, shortLinkDataSet, shortLinkLargeDataSet = BoolDef slLargeDataSet} -> do let shouldBeUpgraded = isNothing sLnk_ || not shortLinkDataSet || not slLargeDataSet - sendReply $ T.unlines $ - [ "The link to join the group " <> groupRef <> ":", - groupLinkText gLink, - "New member role: " <> strEncodeTxt acceptMemberRole - ] - <> ["The link is being upgraded..." | shouldBeUpgraded] + sendReply $ + T.unlines $ + [ "The link to join the group " <> groupRef <> ":", + groupLinkText gLink, + "New member role: " <> strEncodeTxt acceptMemberRole + ] + <> ["The link is being upgraded..." | shouldBeUpgraded] when shouldBeUpgraded $ do let send = sendComposedMessage cc ct Nothing . MCText . T.unlines withGroupLinkResult groupRef (sendChatCmd cc $ APIAddGroupShortLink groupId) $ @@ -830,13 +838,16 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName (Just _, Just _) -> send ["The group link is upgraded for: " <> groupRef, "No changes to group needed."] (Nothing, Just sLnk) -> - sendComposedMessages cc (SRDirect $ contactId' ct) - [ MCText $ T.unlines - [ "Please replace the old link in welcome message of your group " <> groupRef, - "If this is the only change, the group will remain listed in directory without re-approval.", - "", - "The new link:" - ], + sendComposedMessages + cc + (SRDirect $ contactId' ct) + [ MCText $ + T.unlines + [ "Please replace the old link in welcome message of your group " <> groupRef, + "If this is the only change, the group will remain listed in directory without re-approval.", + "", + "The new link:" + ], MCText $ strEncodeTxt sLnk ] (_, Nothing) -> @@ -924,8 +935,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deAdminCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRAdmin -> IO () deAdminCommand ct ciId cmd | knownCt `elem` adminUsers || knownCt `elem` superUsers = case cmd of - DCApproveGroup {groupId, displayName = n, groupApprovalId} -> - withGroupAndReg sendReply groupId n $ \g gr@GroupReg {userGroupRegId = ugrId} -> + DCApproveGroup {groupId, displayName = n, groupApprovalId, promote} -> + withGroupAndReg sendReply groupId n $ \g gr@GroupReg {userGroupRegId = ugrId, promoted} -> readTVarIO (groupRegStatus gr) >>= \case GRSPendingApproval gaId | gaId == groupApprovalId -> do @@ -935,7 +946,17 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName _ -> do getGroupRolesStatus g gr >>= \case Just GRSOk -> do - setGroupStatus st gr GRSActive + setGroupStatus st opts cc user gr GRSActive + forM_ promote $ \promo -> + if promo -- admins can unpromote, only super-user can promote when approving + then + unlessM (readTVarIO promoted) $ + if knownCt `elem` superUsers + then setGroupPromoted st opts cc user gr True + else sendReply "You cannot promote groups" + else do + whenM (readTVarIO promoted) $ setGroupPromoted st opts cc user gr False + notifyOtherSuperUsers $ "Group promotion is disabled for " <> groupRef let approved = "The group " <> userGroupReference' gr n <> " is approved" notifyOwner gr $ (approved <> " and listed in directory - please moderate it!\n") @@ -970,7 +991,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName withGroupAndReg sendReply groupId gName $ \_ gr -> readTVarIO (groupRegStatus gr) >>= \case GRSActive -> do - setGroupStatus st gr GRSSuspended + setGroupStatus st opts cc user gr GRSSuspended let suspended = "The group " <> userGroupReference' gr gName <> " is suspended" notifyOwner gr $ suspended <> " and hidden from directory. Please contact the administrators." sendReply "Group suspended!" @@ -981,7 +1002,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName withGroupAndReg sendReply groupId gName $ \_ gr -> readTVarIO (groupRegStatus gr) >>= \case GRSSuspended -> do - setGroupStatus st gr GRSActive + setGroupStatus st opts cc user gr GRSActive let groupStr = "The group " <> userGroupReference' gr gName notifyOwner gr $ groupStr <> " is listed in the directory again!" sendReply "Group listing resumed!" @@ -1002,7 +1023,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Right () -> do let groupRef = groupReference' groupId gName owner <- groupOwnerInfo groupRef ctId - let invited = " invited " <> owner <> " to owners' group " <> viewName ogName + let invited = " invited " <> owner <> " to owners' group " <> viewName ogName notifyOtherSuperUsers $ viewName (localDisplayName' ct) <> invited sendReply $ "you" <> invited Left err -> sendReply err @@ -1039,7 +1060,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName r -> contErr r r -> contErr r where - alreadyMember = isJust . find ((Just ctId == ) . memberContactId) + alreadyMember = isJust . find ((Just ctId ==) . memberContactId) contErr r = do let err = "error inviting contact ID " <> tshow ctId <> " to owners' group: " <> tshow r putStrLn $ T.unpack err @@ -1053,6 +1074,15 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () deSuperUserCommand ct ciId cmd | knownContact ct `elem` superUsers = case cmd of + DCPromoteGroup groupId gName promote' -> + withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {groupRegStatus, promoted} -> do + status <- readTVarIO groupRegStatus + promote <- readTVarIO promoted + when (promote' /= promote) $ setGroupPromoted st opts cc user gr promote' + let msg = + "Group promotion " + <> (if promote' then "enabled" <> (if status == GRSActive then "." else ", but the group is not listed.") else "disabled.") + sendReply msg DCExecuteCommand cmdStr -> sendChatCmdStr cc cmdStr >>= \case Right r -> do @@ -1102,30 +1132,35 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let text = T.unlines $ [tshow useGroupId <> ". Error: getGroup. Please notify the developers."] <> maybeToList ownerStr_ <> [statusStr] sendComposedMessage cc ct Nothing $ MCText text +setGroupStatus :: DirectoryStore -> DirectoryOpts -> ChatController -> User -> GroupReg -> GroupRegStatus -> IO () +setGroupStatus st opts cc u gr grStatus' = do + let status' = grDirectoryStatus grStatus' + status <- setGroupStatusStore st gr grStatus' + forM_ (webFolder opts) $ \dir -> + when ((status == DSListed || status' == DSListed) && status /= status') $ updateGroupListingFiles cc st u dir + +setGroupPromoted :: DirectoryStore -> DirectoryOpts -> ChatController -> User -> GroupReg -> Bool -> IO () +setGroupPromoted st opts cc u gr grPromoted' = do + (status, grPromoted) <- setGroupPromotedStore st gr grPromoted' + forM_ (webFolder opts) $ \dir -> + when (status == DSListed && grPromoted' /= grPromoted) $ updateGroupListingFiles cc st u dir + +updateGroupListingFiles :: ChatController -> DirectoryStore -> User -> FilePath -> IO () +updateGroupListingFiles cc st u dir = + withDB' "generateListing" cc (\db -> getUserGroupsWithSummary db (vr cc) u Nothing Nothing) >>= \case + Just gs -> generateListing st dir gs + Nothing -> putStrLn "generateListing error: failed to read groups" + getContact' :: ChatController -> User -> ContactId -> IO (Maybe Contact) getContact' cc user ctId = withDB "getContact" cc $ \db -> getContact db (vr cc) user ctId getGroup :: ChatController -> User -> GroupId -> IO (Maybe GroupInfo) getGroup cc user gId = withDB "getGroupInfo" cc $ \db -> getGroupInfo db (vr cc) user gId -withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Maybe a) -withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a - -withDB :: Text -> ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a) -withDB cxt ChatController {chatStore} action = do - r_ :: Either ChatError a <- withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors - case r_ of - Right r -> pure $ Just r - Left e -> Nothing <$ logError ("Database error: " <> cxt <> " " <> tshow e) - getGroupAndSummary :: ChatController -> User -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) getGroupAndSummary cc user gId = withDB "getGroupAndSummary" cc $ \db -> (,) <$> getGroupInfo db (vr cc) user gId <*> liftIO (getGroupSummary db user gId) -vr :: ChatController -> VersionRangeChat -vr ChatController {config = ChatConfig {chatVRange}} = chatVRange -{-# INLINE vr #-} - getGroupLink' :: ChatController -> User -> GroupInfo -> IO (Maybe GroupLink) getGroupLink' cc user gInfo = withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index 9498fedf95..16f047202f 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -13,11 +13,14 @@ module Directory.Store GroupApprovalId, DirectoryGroupData (..), DirectoryMemberAcceptance (..), + DirectoryStatus (..), ProfileCondition (..), restoreDirectoryStore, addGroupReg, delGroupReg, - setGroupStatus, + setGroupStatusStore, + setGroupPromotedStore, + grDirectoryStatus, setGroupRegOwner, getGroupReg, getUserGroupReg, @@ -31,13 +34,14 @@ module Directory.Store noJoinFilter, basicJoinFilter, moderateJoinFilter, - strongJoinFilter + strongJoinFilter, ) where +import Control.Applicative ((<|>)) import Control.Concurrent.STM import Control.Monad -import Data.Aeson ((.=), (.:)) +import Data.Aeson ((.:), (.=)) import qualified Data.Aeson.KeyMap as JM import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.Types as JT @@ -55,23 +59,32 @@ import Data.Text (Text) import Simplex.Chat.Types import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) -import Simplex.Messaging.Util (ifM) -import System.Directory (doesFileExist, renameFile) +import Simplex.Messaging.Util (ifM, whenM) +import System.Directory import System.IO (BufferMode (..), Handle, IOMode (..), hSetBuffering, openFile) data DirectoryStore = DirectoryStore - { groupRegs :: TVar [GroupReg], - listedGroups :: TVar (Set GroupId), + { groupRegs :: TVar [GroupReg], -- most recent first, reversed when listed + listedGroups :: TVar (Set GroupId), -- includes promoted + promotedGroups :: TVar (Set GroupId), reservedGroups :: TVar (Set GroupId), directoryLogFile :: Maybe Handle } +data DirectoryStoreData = DirectoryStoreData + { groupRegs_ :: [GroupReg], + listedGroups_ :: Set GroupId, + promotedGroups_ :: Set GroupId, + reservedGroups_ :: Set GroupId + } + data GroupReg = GroupReg { dbGroupId :: GroupId, userGroupRegId :: UserGroupRegId, dbContactId :: ContactId, dbOwnerMemberId :: TVar (Maybe GroupMemberId), - groupRegStatus :: TVar GroupRegStatus + groupRegStatus :: TVar GroupRegStatus, + promoted :: TVar Bool } data GroupRegData = GroupRegData @@ -79,7 +92,8 @@ data GroupRegData = GroupRegData userGroupRegId_ :: UserGroupRegId, dbContactId_ :: ContactId, dbOwnerMemberId_ :: Maybe GroupMemberId, - groupRegStatus_ :: GroupRegStatus + groupRegStatus_ :: GroupRegStatus, + promoted_ :: Bool } data DirectoryGroupData = DirectoryGroupData @@ -140,7 +154,7 @@ data GroupRegStatus | GRSSuspended | GRSSuspendedBadRoles | GRSRemoved - deriving (Show) + deriving (Eq, Show) pendingApproval :: GroupRegStatus -> Bool pendingApproval = \case @@ -153,6 +167,7 @@ groupRemoved = \case _ -> False data DirectoryStatus = DSListed | DSReserved | DSRegistered | DSRemoved + deriving (Eq) groupRegStatusText :: GroupRegStatus -> Text groupRegStatusText = \case @@ -195,7 +210,7 @@ addGroupReg st ct GroupInfo {groupId} grStatus = do pure $ userGroupRegId_ grData where addGroupReg_ = do - let grData = GroupRegData {dbGroupId_ = groupId, userGroupRegId_ = 1, dbContactId_ = ctId, dbOwnerMemberId_ = Nothing, groupRegStatus_ = grStatus} + let grData = GroupRegData {dbGroupId_ = groupId, userGroupRegId_ = 1, dbContactId_ = ctId, dbOwnerMemberId_ = Nothing, groupRegStatus_ = grStatus, promoted_ = False} gr <- dataToGroupReg grData atomically $ stateTVar (groupRegs st) $ \grs -> let ugrId = 1 + foldl' maxUgrId 0 grs @@ -208,25 +223,38 @@ addGroupReg st ct GroupInfo {groupId} grStatus = do | otherwise = mx delGroupReg :: DirectoryStore -> GroupReg -> IO () -delGroupReg st GroupReg {dbGroupId = gId, groupRegStatus} = do +delGroupReg st gr@GroupReg {dbGroupId = gId, groupRegStatus} = do logGDelete st gId atomically $ writeTVar groupRegStatus GRSRemoved - atomically $ unlistGroup st gId + atomically $ unlistGroup st gr atomically $ modifyTVar' (groupRegs st) $ filter ((gId /=) . dbGroupId) -setGroupStatus :: DirectoryStore -> GroupReg -> GroupRegStatus -> IO () -setGroupStatus st gr grStatus = do - logGUpdateStatus st (dbGroupId gr) grStatus +setGroupStatusStore :: DirectoryStore -> GroupReg -> GroupRegStatus -> IO DirectoryStatus +setGroupStatusStore st gr grStatus' = do + logGUpdateStatus st (dbGroupId gr) grStatus' atomically $ do - writeTVar (groupRegStatus gr) grStatus - updateListing st $ dbGroupId gr + grStatus <- swapTVar (groupRegStatus gr) grStatus' + updateListing st gr + pure $ grDirectoryStatus grStatus where - updateListing = case grDirectoryStatus grStatus of + status' = grDirectoryStatus grStatus' + updateListing = case status' of DSListed -> listGroup DSReserved -> reserveGroup DSRegistered -> unlistGroup DSRemoved -> unlistGroup +setGroupPromotedStore :: DirectoryStore -> GroupReg -> Bool -> IO (DirectoryStatus, Bool) +setGroupPromotedStore st gr grPromoted' = do + let gId = dbGroupId gr + logGUpdatePromotion st gId grPromoted' + atomically $ do + grPromoted <- swapTVar (promoted gr) grPromoted' + status <- grDirectoryStatus <$> readTVar (groupRegStatus gr) + let update = if status == DSListed && grPromoted' then S.insert else S.delete + modifyTVar' (promotedGroups st) $ update gId + pure (status, grPromoted) + setGroupRegOwner :: DirectoryStore -> GroupReg -> GroupMember -> IO () setGroupRegOwner st gr owner = do let memberId = groupMemberId' owner @@ -247,31 +275,39 @@ filterListedGroups st gs = do lgs <- readTVarIO $ listedGroups st pure $ filter (\(GIS GroupInfo {groupId} _) -> groupId `S.member` lgs) gs -listGroup :: DirectoryStore -> GroupId -> STM () -listGroup st gId = do +listGroup :: DirectoryStore -> GroupReg -> STM () +listGroup st gr = do + let gId = dbGroupId gr modifyTVar' (listedGroups st) $ S.insert gId + whenM (readTVar $ promoted gr) $ modifyTVar' (promotedGroups st) $ S.insert gId modifyTVar' (reservedGroups st) $ S.delete gId -reserveGroup :: DirectoryStore -> GroupId -> STM () -reserveGroup st gId = do +reserveGroup :: DirectoryStore -> GroupReg -> STM () +reserveGroup st gr = do + let gId = dbGroupId gr modifyTVar' (listedGroups st) $ S.delete gId + modifyTVar' (promotedGroups st) $ S.delete gId modifyTVar' (reservedGroups st) $ S.insert gId -unlistGroup :: DirectoryStore -> GroupId -> STM () -unlistGroup st gId = do +unlistGroup :: DirectoryStore -> GroupReg -> STM () +unlistGroup st gr = do + let gId = dbGroupId gr modifyTVar' (listedGroups st) $ S.delete gId + modifyTVar' (promotedGroups st) $ S.delete gId modifyTVar' (reservedGroups st) $ S.delete gId data DirectoryLogRecord = GRCreate GroupRegData | GRDelete GroupId | GRUpdateStatus GroupId GroupRegStatus + | GRUpdatePromotion GroupId Bool | GRUpdateOwner GroupId GroupMemberId data DLRTag = GRCreate_ | GRDelete_ | GRUpdateStatus_ + | GRUpdatePromotion_ | GRUpdateOwner_ logDLR :: DirectoryStore -> DirectoryLogRecord -> IO () @@ -286,6 +322,9 @@ logGDelete st = logDLR st . GRDelete logGUpdateStatus :: DirectoryStore -> GroupId -> GroupRegStatus -> IO () logGUpdateStatus st gId = logDLR st . GRUpdateStatus gId +logGUpdatePromotion :: DirectoryStore -> GroupId -> Bool -> IO () +logGUpdatePromotion st gId = logDLR st . GRUpdatePromotion gId + logGUpdateOwner :: DirectoryStore -> GroupId -> GroupMemberId -> IO () logGUpdateOwner st gId = logDLR st . GRUpdateOwner gId @@ -294,12 +333,14 @@ instance StrEncoding DLRTag where GRCreate_ -> "GCREATE" GRDelete_ -> "GDELETE" GRUpdateStatus_ -> "GSTATUS" + GRUpdatePromotion_ -> "GPROMOTE" GRUpdateOwner_ -> "GOWNER" strP = A.takeTill (== ' ') >>= \case "GCREATE" -> pure GRCreate_ "GDELETE" -> pure GRDelete_ "GSTATUS" -> pure GRUpdateStatus_ + "GPROMOTE" -> pure GRUpdatePromotion_ "GOWNER" -> pure GRUpdateOwner_ _ -> fail "invalid DLRTag" @@ -308,30 +349,34 @@ instance StrEncoding DirectoryLogRecord where GRCreate gr -> strEncode (GRCreate_, gr) GRDelete gId -> strEncode (GRDelete_, gId) GRUpdateStatus gId grStatus -> strEncode (GRUpdateStatus_, gId, grStatus) + GRUpdatePromotion gId promoted -> strEncode (GRUpdatePromotion_, gId, promoted) GRUpdateOwner gId grOwnerId -> strEncode (GRUpdateOwner_, gId, grOwnerId) strP = strP_ >>= \case GRCreate_ -> GRCreate <$> strP GRDelete_ -> GRDelete <$> strP GRUpdateStatus_ -> GRUpdateStatus <$> A.decimal <*> _strP + GRUpdatePromotion_ -> GRUpdatePromotion <$> A.decimal <*> _strP GRUpdateOwner_ -> GRUpdateOwner <$> A.decimal <* A.space <*> A.decimal instance StrEncoding GroupRegData where - strEncode GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = - B.unwords + strEncode GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_, promoted_} = + B.unwords $ [ "group_id=" <> strEncode dbGroupId_, "user_group_id=" <> strEncode userGroupRegId_, "contact_id=" <> strEncode dbContactId_, "owner_member_id=" <> strEncode dbOwnerMemberId_, "status=" <> strEncode groupRegStatus_ ] + <> ["promoted=" <> strEncode promoted_ | promoted_] strP = do dbGroupId_ <- "group_id=" *> strP_ userGroupRegId_ <- "user_group_id=" *> strP_ dbContactId_ <- "contact_id=" *> strP_ dbOwnerMemberId_ <- "owner_member_id=" *> strP_ groupRegStatus_ <- "status=" *> strP - pure GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} + promoted_ <- (" promoted=" *> strP) <|> pure False + pure GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_, promoted_} instance StrEncoding GroupRegStatus where strEncode = \case @@ -356,16 +401,18 @@ instance StrEncoding GroupRegStatus where _ -> fail "invalid GroupRegStatus" dataToGroupReg :: GroupRegData -> IO GroupReg -dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = do +dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_, promoted_} = do dbOwnerMemberId <- newTVarIO dbOwnerMemberId_ groupRegStatus <- newTVarIO groupRegStatus_ + promoted <- newTVarIO promoted_ pure GroupReg { dbGroupId = dbGroupId_, userGroupRegId = userGroupRegId_, dbContactId = dbContactId_, dbOwnerMemberId, - groupRegStatus + groupRegStatus, + promoted } restoreDirectoryStore :: Maybe FilePath -> IO DirectoryStore @@ -383,8 +430,8 @@ restoreDirectoryStore = \case h <- writeDirectoryData f grs -- compact mkDirectoryStore h grs -emptyStoreData :: ([GroupReg], Set GroupId, Set GroupId) -emptyStoreData = ([], S.empty, S.empty) +emptyStoreData :: DirectoryStoreData +emptyStoreData = DirectoryStoreData [] S.empty S.empty S.empty newDirectoryStore :: Maybe Handle -> IO DirectoryStore newDirectoryStore = (`mkDirectoryStore_` emptyStoreData) @@ -393,21 +440,27 @@ mkDirectoryStore :: Handle -> [GroupRegData] -> IO DirectoryStore mkDirectoryStore h groups = foldM addGroupRegData emptyStoreData groups >>= mkDirectoryStore_ (Just h) where - addGroupRegData (!grs, !listed, !reserved) gr@GroupRegData {dbGroupId_ = gId} = do + addGroupRegData d gr@GroupRegData {dbGroupId_ = gId} = do gr' <- dataToGroupReg gr - let grs' = gr' : grs + let !grs' = gr' : groupRegs_ d pure $ case grDirectoryStatus $ groupRegStatus_ gr of - DSListed -> (grs', S.insert gId listed, reserved) - DSReserved -> (grs', listed, S.insert gId reserved) - DSRegistered -> (grs', listed, reserved) - DSRemoved -> (grs, listed, reserved) + DSListed -> + let !listed = S.insert gId $ listedGroups_ d + !promoted = (if promoted_ gr then S.insert gId else id) $ promotedGroups_ d + in d {groupRegs_ = grs', listedGroups_ = listed, promotedGroups_ = promoted} + DSReserved -> + let !reserved = S.insert gId $ reservedGroups_ d + in d {groupRegs_ = grs', reservedGroups_ = reserved} + DSRegistered -> d {groupRegs_ = grs'} + DSRemoved -> d -mkDirectoryStore_ :: Maybe Handle -> ([GroupReg], Set GroupId, Set GroupId) -> IO DirectoryStore -mkDirectoryStore_ h (grs, listed, reserved) = do - groupRegs <- newTVarIO grs - listedGroups <- newTVarIO listed - reservedGroups <- newTVarIO reserved - pure DirectoryStore {groupRegs, listedGroups, reservedGroups, directoryLogFile = h} +mkDirectoryStore_ :: Maybe Handle -> DirectoryStoreData -> IO DirectoryStore +mkDirectoryStore_ h d = do + groupRegs <- newTVarIO $ groupRegs_ d + listedGroups <- newTVarIO $ listedGroups_ d + promotedGroups <- newTVarIO $ promotedGroups_ d + reservedGroups <- newTVarIO $ reservedGroups_ d + pure DirectoryStore {groupRegs, listedGroups, promotedGroups, reservedGroups, directoryLogFile = h} readDirectoryData :: FilePath -> IO [GroupRegData] readDirectoryData f = @@ -429,6 +482,9 @@ readDirectoryData f = GRUpdateStatus gId groupRegStatus_ -> case M.lookup gId m of Just gr -> pure $ M.insert gId gr {groupRegStatus_} m Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", status update ignored.") + GRUpdatePromotion gId promoted_ -> case M.lookup gId m of + Just gr -> pure $ M.insert gId gr {promoted_} m + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", promotion update ignored.") GRUpdateOwner gId grOwnerId -> case M.lookup gId m of Just gr -> pure $ M.insert gId gr {dbOwnerMemberId_ = Just grOwnerId} m Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", owner update ignored.") diff --git a/apps/simplex-directory-service/src/Directory/Util.hs b/apps/simplex-directory-service/src/Directory/Util.hs new file mode 100644 index 0000000000..379da73003 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Util.hs @@ -0,0 +1,31 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Directory.Util where + +import qualified Control.Exception as E +import Control.Logger.Simple +import Control.Monad.Except +import Data.Text (Text) +import Simplex.Chat.Controller +import Simplex.Chat.Store.Shared (StoreError (..)) +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Store.Common (withTransaction) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Util (tshow) + +vr :: ChatController -> VersionRangeChat +vr ChatController {config = ChatConfig {chatVRange}} = chatVRange +{-# INLINE vr #-} + +withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Maybe a) +withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a + +withDB :: Text -> ChatController -> (DB.Connection -> ExceptT StoreError IO a) -> IO (Maybe a) +withDB cxt ChatController {chatStore} action = do + r_ :: Either ChatError a <- withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors + case r_ of + Right r -> pure $ Just r + Left e -> Nothing <$ logError ("Database error: " <> cxt <> " " <> tshow e) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index fd8a089193..7a9dd3a92d 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -457,10 +457,12 @@ executable simplex-directory-service Directory.BlockedWords Directory.Captcha Directory.Events + Directory.Listing Directory.Options Directory.Search Directory.Service Directory.Store + Directory.Util Paths_simplex_chat ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: @@ -468,9 +470,11 @@ executable simplex-directory-service , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , containers ==0.6.* , directory ==1.3.* + , filepath ==1.4.* , mtl >=2.3.1 && <3.0 , optparse-applicative >=0.15 && <0.17 , process >=1.6 && <1.6.18 @@ -539,10 +543,12 @@ test-suite simplex-chat-test Directory.BlockedWords Directory.Captcha Directory.Events + Directory.Listing Directory.Options Directory.Search Directory.Service Directory.Store + Directory.Util Paths_simplex_chat if flag(client_postgres) other-modules: diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 3f59863f25..168fd7bfca 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -12,8 +12,10 @@ import ChatTests.Utils import Control.Concurrent (forkIO, killThread, threadDelay) import Control.Exception (finally) import Control.Monad (forM_, when) +import qualified Data.Aeson as J import qualified Data.Text as T import Directory.Captcha +import Directory.Listing import Directory.Options import Directory.Service import Directory.Store @@ -65,8 +67,8 @@ directoryServiceTests = do it "should prohibit confirmation if a duplicate group is listed" testDuplicateProhibitConfirmation it "should prohibit when profile is updated and not send for approval" testDuplicateProhibitWhenUpdated it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval - describe "list groups" $ do - it "should list user's groups" testListUserGroups + describe "list and promote groups" $ do + it "should list and promote user's groups" $ testListUserGroups True describe "member admission" $ do it "should ask member to pass captcha screen" testCapthaScreening describe "store log" $ do @@ -77,8 +79,8 @@ directoryServiceTests = do directoryProfile :: Profile directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing} -mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> DirectoryOpts -mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup = +mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> Maybe FilePath -> DirectoryOpts +mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = DirectoryOpts { coreOptions = testCoreOpts @@ -104,6 +106,7 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup = serviceName = "SimpleX Directory", runCLI = False, searchResults = 3, + webFolder, testing = True } @@ -531,7 +534,7 @@ testSearchGroups ps = testInviteToOwnersGroup :: HasCallStack => TestParams -> IO () testInviteToOwnersGroup ps = - withDirectoryServiceCfgOwnersGroup ps testCfg True $ \superUser dsLink -> + withDirectoryServiceCfgOwnersGroup ps testCfg True Nothing $ \superUser dsLink -> withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> do bob `connectVia` dsLink registerGroupId superUser bob "privacy" "Privacy" 2 1 @@ -1060,14 +1063,15 @@ testDuplicateProhibitApproval ps = superUser <# ("'SimpleX Directory'> > " <> approve) superUser <## " The group ID 2 (privacy) is already listed in the directory." -testListUserGroups :: HasCallStack => TestParams -> IO () -testListUserGroups ps = - withDirectoryService ps $ \superUser dsLink -> +testListUserGroups :: HasCallStack => Bool -> TestParams -> IO () +testListUserGroups promote ps = + withDirectoryServiceCfgOwnersGroup ps testCfg False (Just "./tests/tmp/web") $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink cath `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" + checkListings ["privacy"] [] connectUsers bob cath fullAddMember "privacy" "Privacy" bob cath GRMember joinGroup "privacy" cath bob @@ -1075,7 +1079,9 @@ testListUserGroups ps = cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" cath <## "use @'SimpleX Directory' to send messages" registerGroupId superUser bob "security" "Security" 2 2 + checkListings ["privacy", "security"] [] registerGroupId superUser cath "anonymity" "Anonymity" 3 1 + checkListings ["privacy", "security", "anonymity"] [] listUserGroup cath "anonymity" "Anonymity" -- with de-listed group groupFound cath "anonymity" @@ -1085,8 +1091,51 @@ testListUserGroups ps = cath <## "" cath <## "The group is no longer listed in the directory." superUser <# "'SimpleX Directory'> The group ID 3 (anonymity) is de-listed (SimpleX Directory role is changed to member)." + checkListings ["privacy", "security"] [] groupNotFound cath "anonymity" listGroups superUser bob cath + when promote $ do + superUser #> "@'SimpleX Directory' /promote 1:privacy on" + superUser <# "'SimpleX Directory'> > /promote 1:privacy on" + superUser <## " Group promotion enabled." + checkListings ["privacy", "security"] ["privacy"] + bob ##> "/gp privacy privacy" + bob <## "description removed" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is updated!" + bob <## "It is hidden from the directory until approved." + cath <## "bob updated group #privacy:" + cath <## "description removed" + superUser <# "'SimpleX Directory'> The group ID 1 (privacy) is updated." + superUser <# "'SimpleX Directory'> bob submitted the group ID 1:" + superUser <## "privacy" + superUser <## "Welcome message:" + superUser <##. "Link to join the group privacy: https://localhost/g#" + superUser <## "3 members" + superUser <## "" + superUser <## "To approve send:" + superUser <# "'SimpleX Directory'> /approve 1:privacy 1 promote=on" + checkListings ["security"] [] + superUser #> "@'SimpleX Directory' /approve 1:privacy 1" + superUser <# "'SimpleX Directory'> > /approve 1:privacy 1" + superUser <## " Group approved!" + bob <# "'SimpleX Directory'> The group ID 1 (privacy) is approved and listed in directory - please moderate it!" + bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + bob <## "" + bob <## "Supported commands:" + bob <## "/'filter 1' - to configure anti-spam filter." + bob <## "/'role 1' - to set default member role." + bob <## "/'link 1' - to view/upgrade group link." + checkListings ["privacy", "security"] ["privacy"] + +checkListings :: [T.Text] -> [T.Text] -> IO () +checkListings listed promoted = do + checkListing listingFileName listed + checkListing promotedFileName promoted + where + checkListing f expected = do + Just (DirectoryListing gs) <- J.decodeFileStrict $ "./tests/tmp/web" f + map groupName gs `shouldBe` expected + groupName DirectoryEntry {displayName} = displayName testCapthaScreening :: HasCallStack => TestParams -> IO () testCapthaScreening ps = @@ -1176,7 +1225,7 @@ testCapthaScreening ps = testRestoreDirectory :: HasCallStack => TestParams -> IO () testRestoreDirectory ps = do - testListUserGroups ps + testListUserGroups False ps restoreDirectoryService ps 3 3 $ \superUser _dsLink -> withTestChat ps "bob" $ \bob -> withTestChat ps "cath" $ \cath -> do @@ -1294,10 +1343,10 @@ withDirectoryService :: HasCallStack => TestParams -> (TestCC -> String -> IO () withDirectoryService ps = withDirectoryServiceCfg ps testCfg withDirectoryServiceCfg :: HasCallStack => TestParams -> ChatConfig -> (TestCC -> String -> IO ()) -> IO () -withDirectoryServiceCfg ps cfg = withDirectoryServiceCfgOwnersGroup ps cfg False +withDirectoryServiceCfg ps cfg = withDirectoryServiceCfgOwnersGroup ps cfg False Nothing -withDirectoryServiceCfgOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> Bool -> (TestCC -> String -> IO ()) -> IO () -withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup test = do +withDirectoryServiceCfgOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> Bool -> Maybe FilePath -> (TestCC -> String -> IO ()) -> IO () +withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup webFolder test = do dsLink <- withNewTestChatCfg ps cfg serviceDbPrefix directoryProfile $ \ds -> withNewTestChatCfg ps cfg "super_user" aliceProfile $ \superUser -> do @@ -1315,7 +1364,7 @@ withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup test = do superUser <## "#owners: 'SimpleX Directory' joined the group" ds ##> "/ad" getContactLink ds True - withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test + withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test restoreDirectoryService :: HasCallStack => TestParams -> Int -> Int -> (TestCC -> String -> IO ()) -> IO () restoreDirectoryService ps ctCount grCount test = do @@ -1332,11 +1381,11 @@ restoreDirectoryService ps ctCount grCount test = do withDirectory ps testCfg dsLink test withDirectory :: HasCallStack => TestParams -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO () -withDirectory ps cfg dsLink = withDirectoryOwnersGroup ps cfg dsLink False +withDirectory ps cfg dsLink = withDirectoryOwnersGroup ps cfg dsLink False Nothing -withDirectoryOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> String -> Bool -> (TestCC -> String -> IO ()) -> IO () -withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup test = do - let opts = mkDirectoryOpts ps [KnownContact 2 "alice"] $ if createOwnersGroup then Just $ KnownGroup 1 "owners" else Nothing +withDirectoryOwnersGroup :: HasCallStack => TestParams -> ChatConfig -> String -> Bool -> Maybe FilePath -> (TestCC -> String -> IO ()) -> IO () +withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test = do + let opts = mkDirectoryOpts ps [KnownContact 2 "alice"] (if createOwnersGroup then Just $ KnownGroup 1 "owners" else Nothing) webFolder runDirectory cfg opts $ withTestChatCfg ps cfg "super_user" $ \superUser -> do superUser <## "1 contacts connected (use /cs for the list)" From a190d4ea9bfcd6aa71ab2d0de3edd2d06a8e0fa7 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 20 Sep 2025 19:47:50 +0100 Subject: [PATCH 046/137] website: directory page (#6283) * website: directory page * core: use markdown in directory entries * render markdown on directory page * update markdown * toggle secrets on click * update listings asynchronously * add group links to the listing * cleanup * better directory layout with pagination * script to run website * update page navigation * search * readable markdown colors, better "read less" * core: atomic update of directory listings, to avoid files unavailable * fix symlink, sort entries on page with new first * update listings every 15 min, add activeAt time * fix sorting in the page and listing url * replace simplex:/ links on desktop --- apps/simplex-directory-service/Main.hs | 11 +- .../src/Directory/Listing.hs | 87 +++- .../src/Directory/Search.hs | 8 +- .../src/Directory/Service.hs | 130 ++++-- .../src/Directory/Store.hs | 2 +- bots/api/TYPES.md | 1 + bots/src/API/Docs/Types.hs | 18 +- .../types/typescript/src/types.ts | 1 + simplex-chat.cabal | 2 + src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Store.hs | 2 - src/Simplex/Chat/Store/Groups.hs | 24 +- src/Simplex/Chat/Store/Messages.hs | 2 - src/Simplex/Chat/Types.hs | 14 +- src/Simplex/Chat/View.hs | 6 +- tests/Bots/DirectoryTests.hs | 13 +- website/run.sh | 8 + website/src/blog.html | 128 ++--- website/src/directory.html | 272 +++++++++++ website/src/img/group.svg | 12 + website/src/js/directory.js | 439 ++++++++++++++++++ website/src/js/script.js | 3 +- 22 files changed, 994 insertions(+), 191 deletions(-) create mode 100755 website/run.sh create mode 100644 website/src/directory.html create mode 100644 website/src/img/group.svg create mode 100644 website/src/js/directory.js diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs index e2b96f5677..88e7739aa0 100644 --- a/apps/simplex-directory-service/Main.hs +++ b/apps/simplex-directory-service/Main.hs @@ -5,8 +5,6 @@ module Main where import Directory.Options import Directory.Service import Directory.Store -import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) -import Simplex.Chat.Core import Simplex.Chat.Terminal (terminalChatConfig) main :: IO () @@ -15,11 +13,4 @@ main = do st <- restoreDirectoryStore directoryLog if runCLI then directoryServiceCLI st opts - else do - env <- newServiceState opts - let chatHooks = - defaultChatHooks - { postStartHook = Just $ directoryStartHook st opts, - acceptMember = Just $ acceptMemberHook opts env - } - simplexChatCore (terminalChatConfig {chatHooks}) (mkChatOpts opts) $ directoryService st opts env + else directoryService st opts terminalChatConfig diff --git a/apps/simplex-directory-service/src/Directory/Listing.hs b/apps/simplex-directory-service/src/Directory/Listing.hs index a05e82285d..b9b50cb87b 100644 --- a/apps/simplex-directory-service/src/Directory/Listing.hs +++ b/apps/simplex-directory-service/src/Directory/Listing.hs @@ -1,9 +1,11 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Directory.Listing where @@ -11,23 +13,36 @@ module Directory.Listing where import Control.Applicative ((<|>)) import Control.Concurrent.STM import Control.Monad +import Crypto.Hash (Digest, MD5) +import qualified Crypto.Hash as CH import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ +import qualified Data.ByteArray as BA import Data.ByteString (ByteString) -import Data.ByteString.Base64 as B64 +import qualified Data.ByteString.Base64 as B64 +import qualified Data.ByteString.Base64.URL as B64URL import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as LB -import Data.Maybe (fromMaybe) +import Data.List (isPrefixOf) +import Data.Maybe (catMaybes, fromMaybe) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) +import Data.Time.Clock (UTCTime, getCurrentTime) +import Data.Time.Format.ISO8601 (iso8601Show) import Directory.Store +import Simplex.Chat.Markdown import Simplex.Chat.Types +import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON) import System.Directory import System.FilePath +directoryDataPath :: String +directoryDataPath = "data" + listingFileName :: String listingFileName = "listing.json" @@ -47,9 +62,12 @@ $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "DET") ''DirectoryEntryType) data DirectoryEntry = DirectoryEntry { entryType :: DirectoryEntryType, displayName :: Text, - shortDescr :: Maybe Text, - welcomeMessage :: Maybe Text, - imageFile :: Maybe String + groupLink :: CreatedLinkContact, + shortDescr :: Maybe MarkdownList, + welcomeMessage :: Maybe MarkdownList, + imageFile :: Maybe String, + activeAt :: UTCTime, + createdAt :: UTCTime } $(JQ.deriveJSON defaultJSON ''DirectoryEntry) @@ -60,38 +78,67 @@ $(JQ.deriveJSON defaultJSON ''DirectoryListing) type ImageFileData = ByteString -groupDirectoryEntry :: GroupInfoSummary -> (DirectoryEntry, Maybe (FilePath, ImageFileData)) -groupDirectoryEntry (GIS GroupInfo {groupId, groupProfile} summary) = +groupDirectoryEntry :: GroupInfoSummary -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData)) +groupDirectoryEntry (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLink_) = let GroupProfile {displayName, shortDescr, description, image, memberAdmission} = groupProfile entryType = DETGroup memberAdmission summary - imgData = imgFileData =<< image - in (DirectoryEntry {entryType, displayName, shortDescr, welcomeMessage = description, imageFile = fst <$> imgData}, imgData) + entry groupLink = + let de = + DirectoryEntry + { entryType, + displayName, + groupLink, + shortDescr = toFormattedText <$> shortDescr, + welcomeMessage = toFormattedText <$> description, + imageFile = fst <$> imgData, + activeAt = fromMaybe createdAt chatTs, + createdAt + } + imgData = imgFileData groupLink =<< image + in (de, imgData) + in (entry . connLinkContact) <$> gLink_ where - imgFileData (ImageData img) = + imgFileData :: CreatedConnLink 'CMContact -> ImageData -> Maybe (FilePath, ByteString) + imgFileData groupLink (ImageData img) = let (img', imgExt) = fromMaybe (img, ".jpg") $ (,".jpg") <$> T.stripPrefix "data:image/jpg;base64," img <|> (,".png") <$> T.stripPrefix "data:image/png;base64," img - imgFile = listingImageFolder show groupId <> imgExt + imgName = B.unpack $ B64URL.encodeUnpadded $ BA.convert $ (CH.hash :: ByteString -> Digest MD5) $ strEncode (connFullLink groupLink) + imgFile = listingImageFolder imgName <> imgExt in case B64.decode $ encodeUtf8 img' of Right img'' -> Just (imgFile, img'') Left _ -> Nothing generateListing :: DirectoryStore -> FilePath -> [GroupInfoSummary] -> IO () generateListing st dir gs = do + createDirectoryIfMissing True dir + oldDirs <- filter ((directoryDataPath <> ".") `isPrefixOf`) <$> listDirectory dir + ts <- getCurrentTime + let newDirPath = directoryDataPath <> "." <> iso8601Show ts <> "/" + newDir = dir newDirPath gs' <- filterListedGroups st gs - removePathForcibly (dir listingImageFolder) - createDirectoryIfMissing True (dir listingImageFolder) - gs'' <- forM gs' $ \g@(GIS GroupInfo {groupId} _) -> do - let (g', img) = groupDirectoryEntry g - forM_ img $ \(imgFile, imgData) -> B.writeFile (dir imgFile) imgData - pure (groupId, g') - saveListing listingFileName gs'' - saveListing promotedFileName =<< filterPromotedGroups st gs'' + createDirectoryIfMissing True (newDir listingImageFolder) + gs'' <- + fmap catMaybes $ forM gs' $ \g@(GIS GroupInfo {groupId} _ _) -> + forM (groupDirectoryEntry g) $ \(g', img) -> do + forM_ img $ \(imgFile, imgData) -> B.writeFile (newDir imgFile) imgData + pure (groupId, g') + saveListing newDir listingFileName gs'' + saveListing newDir promotedFileName =<< filterPromotedGroups st gs'' + -- atomically update the link + let newSymLink = newDir <> ".link" + symLink = dir directoryDataPath + createDirectoryLink newDirPath newSymLink + renamePath newSymLink symLink + mapM_ (removePathForcibly . (dir )) oldDirs where - saveListing f = LB.writeFile (dir f) . J.encode . DirectoryListing . map snd + saveListing newDir f = LB.writeFile (newDir f) . J.encode . DirectoryListing . map snd filterPromotedGroups :: DirectoryStore -> [(GroupId, DirectoryEntry)] -> IO [(GroupId, DirectoryEntry)] filterPromotedGroups st gs = do pgs <- readTVarIO $ promotedGroups st pure $ filter (\g -> fst g `S.member` pgs) gs + +toFormattedText :: Text -> MarkdownList +toFormattedText t = fromMaybe [FormattedText Nothing t] $ parseMaybeMarkdownList t diff --git a/apps/simplex-directory-service/src/Directory/Search.hs b/apps/simplex-directory-service/src/Directory/Search.hs index 5b0d650444..2d4cbf9c7b 100644 --- a/apps/simplex-directory-service/src/Directory/Search.hs +++ b/apps/simplex-directory-service/src/Directory/Search.hs @@ -20,13 +20,13 @@ data SearchRequest = SearchRequest data SearchType = STAll | STRecent | STSearch Text takeTop :: Int -> [GroupInfoSummary] -> [GroupInfoSummary] -takeTop n = take n . sortOn (\(GIS _ GroupSummary {currentMembers}) -> Down currentMembers) +takeTop n = take n . sortOn (\(GIS _ GroupSummary {currentMembers} _) -> Down currentMembers) takeRecent :: Int -> [GroupInfoSummary] -> [GroupInfoSummary] -takeRecent n = take n . sortOn (\(GIS GroupInfo {createdAt} _) -> Down createdAt) +takeRecent n = take n . sortOn (\(GIS GroupInfo {createdAt} _ _) -> Down createdAt) groupIds :: [GroupInfoSummary] -> Set GroupId -groupIds = S.fromList . map (\(GIS GroupInfo {groupId} _) -> groupId) +groupIds = S.fromList . map (\(GIS GroupInfo {groupId} _ _) -> groupId) filterNotSent :: Set GroupId -> [GroupInfoSummary] -> [GroupInfoSummary] -filterNotSent sentGroups = filter (\(GIS GroupInfo {groupId} _) -> groupId `S.notMember` sentGroups) +filterNotSent sentGroups = filter (\(GIS GroupInfo {groupId} _ _) -> groupId `S.notMember` sentGroups) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 7bbe4e43a4..b7f1bb3629 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -21,7 +21,6 @@ module Directory.Service where import Control.Concurrent (forkIO) -import Control.Concurrent.Async import Control.Concurrent.STM import Control.Logger.Simple import Control.Monad @@ -54,7 +53,6 @@ import Simplex.Chat.Markdown (Format (..), FormattedText (..), parseMaybeMarkdow import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) -import Simplex.Chat.Store (GroupLink (..)) import Simplex.Chat.Store.Direct (getContact) import Simplex.Chat.Store.Groups (getGroupInfo, getGroupLink, getGroupSummary, getUserGroupsWithSummary, setGroupCustomData) import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo) @@ -69,7 +67,7 @@ import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (. import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Util (safeDecodeUtf8, tshow, unlessM, whenM, ($>>=), (<$$>)) +import Simplex.Messaging.Util (raceAny_, safeDecodeUtf8, tshow, unlessM, whenM, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) import System.Exit (exitFailure) import System.Process (readProcess) @@ -96,7 +94,8 @@ data GroupRolesStatus data ServiceState = ServiceState { searchRequests :: TMap ContactId SearchRequest, blockedWordsCfg :: BlockedWordsConfig, - pendingCaptchas :: TMap GroupMemberId PendingCaptcha + pendingCaptchas :: TMap GroupMemberId PendingCaptcha, + updateListingsJob :: TMVar ChatController } data PendingCaptcha = PendingCaptcha @@ -119,7 +118,8 @@ newServiceState opts = do searchRequests <- TM.emptyIO blockedWordsCfg <- readBlockedWordsConfig opts pendingCaptchas <- TM.emptyIO - pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas} + updateListingsJob <- newEmptyTMVarIO + pure ServiceState {searchRequests, blockedWordsCfg, pendingCaptchas, updateListingsJob} welcomeGetOpts :: IO DirectoryOpts welcomeGetOpts = do @@ -146,22 +146,41 @@ directoryServiceCLI st opts = do env <- newServiceState opts eventQ <- newTQueueIO let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp) - chatHooks = defaultChatHooks {postStartHook = Just $ directoryStartHook st opts, eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env} - race_ - (simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing) - (processEvents eventQ env) + chatHooks = defaultChatHooks {postStartHook = Just $ directoryStartHook opts env, eventHook = Just eventHook, acceptMember = Just $ acceptMemberHook opts env} + raceAny_ $ + [ simplexChatCLI' terminalChatConfig {chatHooks} (mkChatOpts opts) Nothing, + processEvents eventQ env + ] + <> updateListingsThread_ st opts env where processEvents eventQ env = forever $ do (cc, resp) <- atomically $ readTQueue eventQ u_ <- readTVarIO (currentUser cc) forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp -directoryStartHook :: DirectoryStore -> DirectoryOpts -> ChatController -> IO () -directoryStartHook st opts cc = +updateListingDelay :: Int +updateListingDelay = 15 * 60 * 1000000 -- update every 15 minutes + +updateListingsThread_ :: DirectoryStore -> DirectoryOpts -> ServiceState -> [IO ()] +updateListingsThread_ st opts env = maybe [] (\f -> [updateListingsThread f]) $ webFolder opts + where + updateListingsThread f = do + cc <- atomically $ takeTMVar $ updateListingsJob env + forever $ do + u <- readTVarIO $ currentUser cc + forM_ u $ \user -> updateGroupListingFiles cc st user f + delay <- registerDelay updateListingDelay + atomically $ void (takeTMVar $ updateListingsJob env) `orElse` unlessM (readTVar delay) retry + +listingsUpdated :: ServiceState -> ChatController -> IO () +listingsUpdated env = void . atomically . tryPutTMVar (updateListingsJob env) + +directoryStartHook :: DirectoryOpts -> ServiceState -> ChatController -> IO () +directoryStartHook opts env cc = readTVarIO (currentUser cc) >>= \case Nothing -> putStrLn "No current user" >> exitFailure - Just user@User {userId, profile = p@LocalProfile {preferences}} -> do - forM_ (webFolder opts) $ updateGroupListingFiles cc st user + Just User {userId, profile = p@LocalProfile {preferences}} -> do + listingsUpdated env cc let cmds = fromMaybe [] $ preferences >>= commands_ unless (cmds == directoryCommands) $ do let prefs = (fromMaybe emptyChatPrefs preferences) {files = Just FilesPreference {allow = FANo}, commands = Just directoryCommands} :: Preferences @@ -188,12 +207,23 @@ directoryCommands = where idParam = Just "" -directoryService :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> IO () -directoryService st opts@DirectoryOpts {testing} env user cc = do - initializeBotAddress' (not testing) cc - race_ (forever $ void getLine) . forever $ do - (_, resp) <- atomically . readTBQueue $ outputQ cc - directoryServiceEvent st opts env user cc resp +directoryService :: DirectoryStore -> DirectoryOpts -> ChatConfig -> IO () +directoryService st opts@DirectoryOpts {testing} cfg = do + env <- newServiceState opts + let chatHooks = + defaultChatHooks + { postStartHook = Just $ directoryStartHook opts env, + acceptMember = Just $ acceptMemberHook opts env + } + simplexChatCore cfg {chatHooks} (mkChatOpts opts) $ \user cc -> do + initializeBotAddress' (not testing) cc + raceAny_ $ + [ forever $ void getLine, + forever $ do + (_, resp) <- atomically . readTBQueue $ outputQ cc + directoryServiceEvent st opts env user cc resp + ] + <> updateListingsThread_ st opts env acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)) acceptMemberHook @@ -301,7 +331,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName, fullName}} = getGroups fullName >>= mapM duplicateGroup where - sameGroupNotRemoved (GIS g@GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}} _) = + sameGroupNotRemoved (GIS g@GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}} _ _) = gId /= groupId && n == displayName && fn == fullName && not (memberRemoved $ membership g) duplicateGroup [] = pure DGUnique duplicateGroup groups = do @@ -310,13 +340,13 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName then pure DGUnique else do (lgs, rgs) <- atomically $ (,) <$> readTVar (listedGroups st) <*> readTVar (reservedGroups st) - let reserved = any (\(GIS GroupInfo {groupId = gId} _) -> gId `S.member` lgs || gId `S.member` rgs) gs + let reserved = any (\(GIS GroupInfo {groupId = gId} _ _) -> gId `S.member` lgs || gId `S.member` rgs) gs if reserved then pure DGReserved else do removed <- foldM (\r -> fmap (r &&) . isGroupRemoved) True gs pure $ if removed then DGUnique else DGRegistered - isGroupRemoved (GIS GroupInfo {groupId = gId} _) = + isGroupRemoved (GIS GroupInfo {groupId = gId} _ _) = getGroupReg st gId >>= \case Just GroupReg {groupRegStatus} -> groupRemoved <$> readTVarIO groupRegStatus Nothing -> pure True @@ -395,7 +425,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case Right CRGroupLinkCreated {groupLink = GroupLink {connLinkContact = gLink}} -> do - setGroupStatus st opts cc user gr GRSPendingUpdate + setGroupStatus st env cc gr GRSPendingUpdate notifyOwner gr "Created the public link to join the group via this directory service that is always online.\n\n\ @@ -456,7 +486,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just DGReserved -> notifyOwner gr $ groupAlreadyListed toGroup _ -> do let gaId = 1 - setGroupStatus st opts cc user gr $ GRSPendingApproval gaId + setGroupStatus st env cc gr $ GRSPendingApproval gaId notifyOwner gr $ ("Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message" <> byMember) <> ".\nYou will be notified once the group is added to the directory - it may take up to 48 hours." @@ -466,18 +496,18 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName groupRef = groupReference toGroup groupProfileUpdate >>= \case GPNoServiceLink -> do - setGroupStatus st opts cc user gr GRSPendingUpdate + setGroupStatus st env cc gr GRSPendingUpdate notifyOwner gr $ ("The group profile is updated for " <> userGroupRef <> byMember <> ", but no link is added to the welcome message.\n\n") <> "The group will remain hidden from the directory until the group link is added and the group is re-approved." GPServiceLinkRemoved -> do - setGroupStatus st opts cc user gr GRSPendingUpdate + setGroupStatus st env cc gr GRSPendingUpdate notifyOwner gr $ ("The group link for " <> userGroupRef <> " is removed from the welcome message" <> byMember) <> ".\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." notifyAdminUsers $ "The group link is removed from " <> groupRef <> ", de-listed." GPServiceLinkAdded _ -> do - setGroupStatus st opts cc user gr $ GRSPendingApproval n' + setGroupStatus st env cc gr $ GRSPendingApproval n' notifyOwner gr $ ("The group link is added to " <> userGroupRef <> byMember) <> "!\nIt is hidden from the directory until approved." @@ -490,7 +520,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName <> "!\nThe group is listed in directory." notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> " - only link or whitespace changes.\nThe group remained listed in directory." | otherwise -> do - setGroupStatus st opts cc user gr $ GRSPendingApproval n' + setGroupStatus st env cc gr $ GRSPendingApproval n' notifyOwner gr $ ("The group " <> userGroupRef <> " is updated" <> byMember) <> "!\nIt is hidden from the directory until approved." @@ -628,14 +658,14 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName when (ctId `isOwner` gr) $ do readTVarIO (groupRegStatus gr) >>= \case GRSSuspendedBadRoles -> when (rStatus == GRSOk) $ do - setGroupStatus st opts cc user gr GRSActive + setGroupStatus st env cc gr GRSActive notifyOwner gr $ uCtRole <> ".\n\nThe group is listed in the directory again." notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suCtRole GRSPendingApproval gaId -> when (rStatus == GRSOk) $ do sendToApprove g gr gaId notifyOwner gr $ uCtRole <> ".\n\nThe group is submitted for approval." GRSActive -> when (rStatus /= GRSOk) $ do - setGroupStatus st opts cc user gr GRSSuspendedBadRoles + setGroupStatus st env cc gr GRSSuspendedBadRoles notifyOwner gr $ uCtRole <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole _ -> pure () @@ -654,7 +684,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName readTVarIO (groupRegStatus gr) >>= \case GRSSuspendedBadRoles -> when (serviceRole == GRAdmin) $ whenContactIsOwner gr $ do - setGroupStatus st opts cc user gr GRSActive + setGroupStatus st env cc gr GRSActive notifyOwner gr $ uSrvRole <> ".\n\nThe group is listed in the directory again." notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole GRSPendingApproval gaId -> when (serviceRole == GRAdmin) $ @@ -662,7 +692,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName sendToApprove g gr gaId notifyOwner gr $ uSrvRole <> ".\n\nThe group is submitted for approval." GRSActive -> when (serviceRole /= GRAdmin) $ do - setGroupStatus st opts cc user gr GRSSuspendedBadRoles + setGroupStatus st env cc gr GRSSuspendedBadRoles notifyOwner gr $ uSrvRole <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole _ -> pure () @@ -679,7 +709,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g withGroupReg g "contact removed" $ \gr -> do when (ctId `isOwner` gr) $ do - setGroupStatus st opts cc user gr GRSRemoved + setGroupStatus st env cc gr GRSRemoved notifyOwner gr $ "You are removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." @@ -688,7 +718,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g withGroupReg g "contact left" $ \gr -> do when (ctId `isOwner` gr) $ do - setGroupStatus st opts cc user gr GRSRemoved + setGroupStatus st env cc gr GRSRemoved notifyOwner gr $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." @@ -696,7 +726,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deServiceRemovedFromGroup g = do logInfo $ "service removed from group " <> viewGroupName g withGroupReg g "service removed" $ \gr -> do - setGroupStatus st opts cc user gr GRSRemoved + setGroupStatus st env cc gr GRSRemoved notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." @@ -704,7 +734,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deGroupDeleted g = do logInfo $ "group removed " <> viewGroupName g withGroupReg g "group removed" $ \gr -> do - setGroupStatus st opts cc user gr GRSRemoved + setGroupStatus st env cc gr GRSRemoved notifyOwner gr $ "The group " <> userGroupReference gr g <> " is deleted.\n\nThe group is no longer listed in the directory." notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (group is deleted)." @@ -925,7 +955,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName where msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0] replyMsg = (Just ciId, MCText reply) - foundGroup (GIS GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}} GroupSummary {currentMembers}) = + foundGroup (GIS GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_}} GroupSummary {currentMembers} _) = let membersStr = "_" <> tshow currentMembers <> " members_" showId = if isAdmin then tshow groupId <> ". " else "" text = showId <> groupInfoText p <> "\n" <> membersStr @@ -946,16 +976,16 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName _ -> do getGroupRolesStatus g gr >>= \case Just GRSOk -> do - setGroupStatus st opts cc user gr GRSActive + setGroupStatus st env cc gr GRSActive forM_ promote $ \promo -> if promo -- admins can unpromote, only super-user can promote when approving then unlessM (readTVarIO promoted) $ if knownCt `elem` superUsers - then setGroupPromoted st opts cc user gr True + then setGroupPromoted st env cc gr True else sendReply "You cannot promote groups" else do - whenM (readTVarIO promoted) $ setGroupPromoted st opts cc user gr False + whenM (readTVarIO promoted) $ setGroupPromoted st env cc gr False notifyOtherSuperUsers $ "Group promotion is disabled for " <> groupRef let approved = "The group " <> userGroupReference' gr n <> " is approved" notifyOwner gr $ @@ -991,7 +1021,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName withGroupAndReg sendReply groupId gName $ \_ gr -> readTVarIO (groupRegStatus gr) >>= \case GRSActive -> do - setGroupStatus st opts cc user gr GRSSuspended + setGroupStatus st env cc gr GRSSuspended let suspended = "The group " <> userGroupReference' gr gName <> " is suspended" notifyOwner gr $ suspended <> " and hidden from directory. Please contact the administrators." sendReply "Group suspended!" @@ -1002,7 +1032,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName withGroupAndReg sendReply groupId gName $ \_ gr -> readTVarIO (groupRegStatus gr) >>= \case GRSSuspended -> do - setGroupStatus st opts cc user gr GRSActive + setGroupStatus st env cc gr GRSActive let groupStr = "The group " <> userGroupReference' gr gName notifyOwner gr $ groupStr <> " is listed in the directory again!" sendReply "Group listing resumed!" @@ -1078,7 +1108,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {groupRegStatus, promoted} -> do status <- readTVarIO groupRegStatus promote <- readTVarIO promoted - when (promote' /= promote) $ setGroupPromoted st opts cc user gr promote' + when (promote' /= promote) $ setGroupPromoted st env cc gr promote' let msg = "Group promotion " <> (if promote' then "enabled" <> (if status == GRSActive then "." else ", but the group is not listed.") else "disabled.") @@ -1132,18 +1162,16 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let text = T.unlines $ [tshow useGroupId <> ". Error: getGroup. Please notify the developers."] <> maybeToList ownerStr_ <> [statusStr] sendComposedMessage cc ct Nothing $ MCText text -setGroupStatus :: DirectoryStore -> DirectoryOpts -> ChatController -> User -> GroupReg -> GroupRegStatus -> IO () -setGroupStatus st opts cc u gr grStatus' = do +setGroupStatus :: DirectoryStore -> ServiceState -> ChatController -> GroupReg -> GroupRegStatus -> IO () +setGroupStatus st env cc gr grStatus' = do let status' = grDirectoryStatus grStatus' status <- setGroupStatusStore st gr grStatus' - forM_ (webFolder opts) $ \dir -> - when ((status == DSListed || status' == DSListed) && status /= status') $ updateGroupListingFiles cc st u dir + when ((status == DSListed || status' == DSListed) && status /= status') $ listingsUpdated env cc -setGroupPromoted :: DirectoryStore -> DirectoryOpts -> ChatController -> User -> GroupReg -> Bool -> IO () -setGroupPromoted st opts cc u gr grPromoted' = do +setGroupPromoted :: DirectoryStore -> ServiceState -> ChatController -> GroupReg -> Bool -> IO () +setGroupPromoted st env cc gr grPromoted' = do (status, grPromoted) <- setGroupPromotedStore st gr grPromoted' - forM_ (webFolder opts) $ \dir -> - when (status == DSListed && grPromoted' /= grPromoted) $ updateGroupListingFiles cc st u dir + when (status == DSListed && grPromoted' /= grPromoted) $ listingsUpdated env cc updateGroupListingFiles :: ChatController -> DirectoryStore -> User -> FilePath -> IO () updateGroupListingFiles cc st u dir = diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index 16f047202f..1c8e1a24b5 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -273,7 +273,7 @@ getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVarIO (grou filterListedGroups :: DirectoryStore -> [GroupInfoSummary] -> IO [GroupInfoSummary] filterListedGroups st gs = do lgs <- readTVarIO $ listedGroups st - pure $ filter (\(GIS GroupInfo {groupId} _) -> groupId `S.member` lgs) gs + pure $ filter (\(GIS GroupInfo {groupId} _ _) -> groupId `S.member` lgs) gs listGroup :: DirectoryStore -> GroupReg -> STM () listGroup st gr = do diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index f20c7c4913..6207b1bf64 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -2159,6 +2159,7 @@ MemberSupport: **Record type**: - groupInfo: [GroupInfo](#groupinfo) - groupSummary: [GroupSummary](#groupsummary) +- groupLink: [GroupLink](#grouplink)? --- diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 83675798af..d74f6d37dd 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -29,7 +29,6 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.Protocol -import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Types @@ -69,7 +68,7 @@ chatTypesDocs = sortOn docTypeName $! snd $! mapAccumL toCTDoc (S.empty, M.empty let (tds', td_) = toTypeDef tds sumTypeInfo in case td_ of Just typeDef -> (tds', CTDoc {typeDef, typeSyntax, typeDescr}) - Nothing -> error $ "Recursive type: " <> typeName + Nothing -> error $ "Recursive type: " <> typeName toTypeDef :: (S.Set String, M.Map String APITypeDef) -> (SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text) -> ((S.Set String, M.Map String APITypeDef), Maybe APITypeDef) toTypeDef acc@(!visited, !typeDefs) (STI typeName allConstrs, jsonEncoding, consPrefix, hideConstrs, _, _) = @@ -84,7 +83,7 @@ toTypeDef acc@(!visited, !typeDefs) (STI typeName allConstrs, jsonEncoding, cons let fields = fromMaybe (error $ "Record type without fields: " <> typeName) $ L.nonEmpty fieldInfos ((visited', typeDefs'), fields') = mapAccumL (toAPIField_ typeName) (S.insert typeName visited, typeDefs) fields td = APITypeDef typeName $ ATDRecord $ L.toList fields' - in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td) + in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td) _ -> error $ "Record type with " <> show (length constrs) <> " constructors: " <> typeName STUnion -> if length constrs > 1 then toUnionType constrs else unionError constrs STUnion1 -> if length constrs == 1 then toUnionType constrs else unionError constrs @@ -98,16 +97,16 @@ toTypeDef acc@(!visited, !typeDefs) (STI typeName allConstrs, jsonEncoding, cons toUnionType constrs = let ((visited', typeDefs'), members) = mapAccumL toUnionMember (S.insert typeName visited, typeDefs) $ fromMaybe (unionError constrs) $ L.nonEmpty constrs td = APITypeDef typeName $ ATDUnion members - in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td) + in ((S.insert typeName visited', M.insert typeName td typeDefs'), Just td) toUnionMember tds RecordTypeInfo {consName, fieldInfos} = let memberTag = normalizeConsName consPrefix consName - in second (ATUnionMember memberTag) $ mapAccumL (toAPIField_ typeName) tds fieldInfos + in second (ATUnionMember memberTag) $ mapAccumL (toAPIField_ typeName) tds fieldInfos unionError constrs = error $ "Union type with " <> show (length constrs) <> " constructor(s): " <> typeName toEnumType = toEnumType_ $ normalizeConsName consPrefix toEnumType_ f constrs = let members = L.map toEnumMember $ fromMaybe (enumError constrs) $ L.nonEmpty constrs td = APITypeDef typeName $ ATDEnum members - in ((S.insert typeName visited, M.insert typeName td typeDefs), Just td) + in ((S.insert typeName visited, M.insert typeName td typeDefs), Just td) where toEnumMember RecordTypeInfo {consName, fieldInfos} = case fieldInfos of [] -> f consName @@ -121,7 +120,7 @@ toAPIField_ typeName tds (FieldInfo fieldName typeInfo) = second (APIRecordField toAPIType = \case TIType (ST name _) -> apiTypeForName name TIOptional tInfo -> second ATOptional $ toAPIType tInfo - TIArray {elemType, nonEmpty} -> second (`ATArray`nonEmpty) $ toAPIType elemType + TIArray {elemType, nonEmpty} -> second (`ATArray` nonEmpty) $ toAPIType elemType TIMap {keyType = ST name _, valueType} | name `elem` primitiveTypes -> second (ATMap (PT name)) $ toAPIType valueType | otherwise -> error $ "Non-primitive key type in " <> typeName <> ", " <> fieldName @@ -133,7 +132,7 @@ toAPIField_ typeName tds (FieldInfo fieldName typeInfo) = second (APIRecordField Nothing -> case find (\(STI name' _, _, _, _, _, _) -> name == name') chatTypesDocsData of Just sumTypeInfo -> let (tds', td_) = toTypeDef tds sumTypeInfo -- recursion to outer function, loops are resolved via type defs map lookup - in case td_ of + in case td_ of Just td -> (tds', ATDef td) Nothing -> (tds', ATRef name) Nothing -> error $ "Undefined type: " <> name @@ -352,7 +351,6 @@ chatTypesDocsData = (sti @XFTPErrorType, STUnion, "", [], "", ""), (sti @XFTPRcvFile, STRecord, "", [], "", ""), (sti @XFTPSndFile, STRecord, "", [], "", "") - -- (sti @DatabaseError, STUnion, "DB", [], "", ""), -- (sti @ChatItemInfo, STRecord, "", [], "", ""), -- (sti @ChatItemVersion, STRecord, "", [], "", ""), @@ -371,7 +369,7 @@ chatTypesDocsData = -- (sti @SendRef, STRecord, "", [], "", ""), -- (sti @SndQueueInfo, STRecord, "", [], "", ""), -- (sti @SndSwitchStatus, STEnum, "", [], "", ""), -- incorrect - ] + ] data SimplePreference = SimplePreference {allow :: FeatureAllowed} deriving (Generic) diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index bdc99ee750..97bc45f3ce 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2451,6 +2451,7 @@ export interface GroupInfo { export interface GroupInfoSummary { groupInfo: GroupInfo groupSummary: GroupSummary + groupLink?: GroupLink } export interface GroupLink { diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7a9dd3a92d..529873b7d3 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -473,8 +473,10 @@ executable simplex-directory-service , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , containers ==0.6.* + , crypton ==0.34.* , directory ==1.3.* , filepath ==1.4.* + , memory ==0.18.* , mtl >=2.3.1 && <3.0 , optparse-applicative >=0.15 && <0.17 , process >=1.6 && <1.6.18 diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0b2f16ad1d..45fd264afc 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -62,7 +62,7 @@ import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Remote.Types import Simplex.Chat.Stats (PresentedServersSummary) -import Simplex.Chat.Store (AddressSettings, ChatLockEntity, GroupLink, GroupLinkInfo, StoreError (..), UserContactLink, UserMsgReceiptSettings) +import Simplex.Chat.Store (AddressSettings, ChatLockEntity, GroupLinkInfo, StoreError (..), UserContactLink, UserMsgReceiptSettings) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index e80037be74..e81ec0cc3a 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -6,7 +6,6 @@ module Simplex.Chat.Store ChatLockEntity (..), UserMsgReceiptSettings (..), UserContactLink (..), - GroupLink (..), GroupLinkInfo (..), AddressSettings (..), AutoAccept (..), @@ -16,7 +15,6 @@ module Simplex.Chat.Store ) where -import Simplex.Chat.Store.Groups (GroupLink (..)) import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Messaging.Agent.Store.Common (DBStore (..), withTransaction) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 3d8bdead72..7e9d7b4ae2 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -18,7 +18,6 @@ module Simplex.Chat.Store.Groups GroupInfoRow, GroupMemberRow, MaybeGroupMemberRow, - GroupLink (..), toGroupInfo, toGroupMember, toMaybeGroupMember, @@ -162,7 +161,6 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) -import qualified Data.Aeson.TH as J import Data.Bifunctor (second) import Data.Bitraversable (bitraverse) import Data.Char (toLower) @@ -188,7 +186,6 @@ import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff) -import Simplex.Messaging.Parsers (defaultJSON) import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (eitherToMaybe, firstRow', safeDecodeUtf8, ($>>), ($>>=), (<$$>)) import Simplex.Messaging.Version @@ -280,16 +277,6 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do (userId, groupId) DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId) -data GroupLink = GroupLink - { userContactLinkId :: Int64, - connLinkContact :: CreatedLinkContact, - shortLinkDataSet :: Bool, - shortLinkLargeDataSet :: BoolDef, - groupLinkId :: GroupLinkId, - acceptMemberRole :: GroupMemberRole - } - deriving (Show) - getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO GroupLink getGroupLink db User {userId} gInfo@GroupInfo {groupId} = ExceptT . firstRow toGroupLink (SEGroupLinkNotFound gInfo) $ @@ -982,9 +969,12 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do search = maybe "" (map toLower) search_ getUserGroupsWithSummary :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfoSummary] -getUserGroupsWithSummary db vr user _contactId_ search_ = - getUserGroupDetails db vr user _contactId_ search_ - >>= mapM (\g@GroupInfo {groupId} -> GIS g <$> getGroupSummary db user groupId) +getUserGroupsWithSummary db vr user _contactId_ search_ = do + gs <- getUserGroupDetails db vr user _contactId_ search_ + forM gs $ \g@GroupInfo {groupId} -> do + s <- getGroupSummary db user groupId + link_ <- eitherToMaybe <$> runExceptT (getGroupLink db user g) + pure $ GIS g s link_ -- the statuses on non-current members should match memberCurrent' function getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary @@ -2905,5 +2895,3 @@ updateGroupAlias db userId g@GroupInfo {groupId} localAlias = do updatedAt <- getCurrentTime DB.execute db "UPDATE groups SET local_alias = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (localAlias, updatedAt, userId, groupId) pure (g :: GroupInfo) {localAlias = localAlias} - -$(J.deriveJSON defaultJSON ''GroupLink) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index f6d0e7da72..7eba39360f 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -2204,8 +2204,6 @@ updateGroupScopeUnreadStats db vr user g@GroupInfo {membership} scopeInfo (unrea m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId pure $ either (const m) id m_ -- Left shouldn't happen, but types require it -deriving instance Show BoolInt - setGroupChatItemsDeleteAt :: DB.Connection -> User -> GroupId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] setGroupChatItemsDeleteAt db User {userId} groupId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do let deleteAt = addUTCTime (realToFrac ttl) currentTs diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index c4253942d8..a90d457920 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -527,7 +527,17 @@ data GroupSummary = GroupSummary } deriving (Show) -data GroupInfoSummary = GIS {groupInfo :: GroupInfo, groupSummary :: GroupSummary} +data GroupInfoSummary = GIS {groupInfo :: GroupInfo, groupSummary :: GroupSummary, groupLink :: Maybe GroupLink} + deriving (Show) + +data GroupLink = GroupLink + { userContactLinkId :: Int64, + connLinkContact :: CreatedLinkContact, + shortLinkDataSet :: Bool, + shortLinkLargeDataSet :: BoolDef, + groupLinkId :: GroupLinkId, + acceptMemberRole :: GroupMemberRole + } deriving (Show) data ContactOrGroup = CGContact Contact | CGGroup GroupInfo [GroupMember] @@ -2075,6 +2085,8 @@ $(JQ.deriveJSON defaultJSON ''Group) $(JQ.deriveJSON defaultJSON ''GroupSummary) +$(JQ.deriveJSON defaultJSON ''GroupLink) + $(JQ.deriveJSON defaultJSON ''GroupInfoSummary) instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index dcaabe433e..015d4e6645 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -50,7 +50,7 @@ import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange) import Simplex.Chat.Remote.Types -import Simplex.Chat.Store (AddressSettings (..), AutoAccept (..), GroupLink (..), StoreError (..), UserContactLink (..)) +import Simplex.Chat.Store (AddressSettings (..), AutoAccept (..), StoreError (..), UserContactLink (..)) import Simplex.Chat.Styled import Simplex.Chat.Types import Simplex.Chat.Types.Preferences @@ -1365,8 +1365,8 @@ viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g Text - ldn_ (GIS GroupInfo {localDisplayName} _) = T.toLower localDisplayName - groupSS (GIS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} GroupSummary {currentMembers}) = + ldn_ (GIS GroupInfo {localDisplayName} _ _) = T.toLower localDisplayName + groupSS (GIS g@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} GroupSummary {currentMembers} _) = case memberStatus membership of GSMemInvited -> groupInvitation' g s -> membershipIncognito g <> ttyFullGroup g <> viewMemberStatus s <> alias g diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 168fd7bfca..1550a7bd9b 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -21,8 +21,7 @@ import Directory.Service import Directory.Store import GHC.IO.Handle (hClose) import Simplex.Chat.Bot.KnownContacts -import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) -import Simplex.Chat.Core +import Simplex.Chat.Controller (ChatConfig (..)) import qualified Simplex.Chat.Markdown as MD import Simplex.Chat.Options (CoreChatOpts (..)) import Simplex.Chat.Options.DB @@ -1129,11 +1128,12 @@ testListUserGroups promote ps = checkListings :: [T.Text] -> [T.Text] -> IO () checkListings listed promoted = do + threadDelay 100000 checkListing listingFileName listed checkListing promotedFileName promoted where checkListing f expected = do - Just (DirectoryListing gs) <- J.decodeFileStrict $ "./tests/tmp/web" f + Just (DirectoryListing gs) <- J.decodeFileStrict $ "./tests/tmp/web/data" f map groupName gs `shouldBe` expected groupName DirectoryEntry {displayName} = displayName @@ -1396,14 +1396,9 @@ withDirectoryOwnersGroup ps cfg dsLink createOwnersGroup webFolder test = do runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO () runDirectory cfg opts@DirectoryOpts {directoryLog} action = do st <- restoreDirectoryStore directoryLog - t <- forkIO $ bot st + t <- forkIO $ directoryService st opts cfg threadDelay 500000 action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t) - where - bot st = do - env <- newServiceState opts - let cfg' = cfg {chatHooks = defaultChatHooks {acceptMember = Just $ acceptMemberHook opts env}} - simplexChatCore cfg' (mkChatOpts opts) $ directoryService st opts env registerGroup :: TestCC -> TestCC -> String -> String -> IO () registerGroup su u n fn = registerGroupId su u n fn 1 1 diff --git a/website/run.sh b/website/run.sh new file mode 100755 index 0000000000..a0a8dd0e50 --- /dev/null +++ b/website/run.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +cd .. +./website/web.sh +cd website +npm run start diff --git a/website/src/blog.html b/website/src/blog.html index 571b240451..4a0460511a 100644 --- a/website/src/blog.html +++ b/website/src/blog.html @@ -1,36 +1,42 @@ --- layout: layouts/main.html title: "SimpleX blog: the latest news" -description: "SimpleX Chat - a private and encrypted messenger without any user IDs (not even random ones)! Make a private connection via link / QR code to send messages and make calls." +description: "SimpleX Chat - a private and encrypted messenger without any user IDs (not even random ones)! Make a +private connection via link / QR code to send messages and make calls." path: /blog templateEngineOverride: njk active_blog: true --- {% block css_links %} - + {% endblock %}
@@ -39,42 +45,48 @@ active_blog: true {% for blog in collections.blogs %} {% if not(blog.data.draft) %} -
-
-
- {% if blog.data.image %} - {% if blog.data.imageBottom %} - - {% elif blog.data.imageWide %} - - {% else %} - - {% endif %} - {% else %} - - - {% endif %} -
+
+
+
+ {% if blog.data.image %} + {% if blog.data.imageBottom %} + + {% elif blog.data.imageWide %} + + {% else %} + + {% endif %} + {% else %} + + + {% endif %}
-
-
-

- {{ blog.data.title | safe }} -

-

- {{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }} -

- {% if blog.data.previewBody %} -
- {% include blog.data.previewBody %} -
- {% elif blog.data.preview %} -

{{ blog.data.preview | safe }}

- {% endif %} +
+
+
+

+ {{ blog.data.title | safe }} +

+

+ {{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }} +

+ {% if blog.data.previewBody %} +
+ {% include blog.data.previewBody %}
- Read More + {% elif blog.data.preview %} +

{{ blog.data.preview | safe }}

+ {% endif %}
-
+ Read More +
+
{% endif %} {% endfor %} diff --git a/website/src/directory.html b/website/src/directory.html new file mode 100644 index 0000000000..247ee6ebc2 --- /dev/null +++ b/website/src/directory.html @@ -0,0 +1,272 @@ +--- +layout: layouts/main.html +title: "SimpleX Directory" +description: "Find communities on SimpleX network and create your own" +templateEngineOverride: njk +--- + +{% set lang = page.url | getlang %} +{% block js_scripts %} + + +{% endblock %} + + + +
+
+

SimpleX Directory

+

Welcome to the selected users' communities that you can join via SimpleX Chat + app.

+

SimpleX Directory is also available as a SimpleX chat bot.

+

Read about how to add your community.

+
+ + +
+
+ +
+
\ No newline at end of file diff --git a/website/src/img/group.svg b/website/src/img/group.svg new file mode 100644 index 0000000000..2a262ef38a --- /dev/null +++ b/website/src/img/group.svg @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/website/src/js/directory.js b/website/src/js/directory.js new file mode 100644 index 0000000000..2eb8db71a1 --- /dev/null +++ b/website/src/js/directory.js @@ -0,0 +1,439 @@ +const directoryDataURL = 'https://directory.simplex.chat/data/'; + +// const directoryDataURL = 'http://localhost:8080/directory-data/'; + +let allEntries = []; + +let sortedEntries = []; + +let filteredEntries = []; + +let currentSortMode = ''; + +async function initDirectory() { + const listing = await fetchJSON(directoryDataURL + 'listing.json') + const liveBtn = document.querySelector('#top-pagination .live'); + const newBtn = document.querySelector('#top-pagination .new'); + const topBtn = document.querySelector('#top-pagination .top'); + const searchInput = document.getElementById('search'); + allEntries = listing.entries + renderSortedEntries('top', byMemberCountDesc, topBtn) + window.addEventListener('hashchange', renderDirectoryPage); + searchInput.addEventListener('input', (e) => renderFilteredEntries(e.target.value)); + + liveBtn.addEventListener('click', () => renderSortedEntries('live', byActiveAtDesc, liveBtn)); + newBtn.addEventListener('click', () => renderSortedEntries('new', byCreatedAtDesc, newBtn)); + topBtn.addEventListener('click', () => renderSortedEntries('top', byMemberCountDesc, topBtn)); + + function renderSortedEntries(mode, comparator, btn) { + if (currentSortMode === mode) return; + currentSortMode = mode; + if (location.hash) location.hash = ''; + liveBtn.classList.remove('active'); + newBtn.classList.remove('active'); + topBtn.classList.remove('active'); + btn.classList.add('active'); + sortedEntries = allEntries.slice().sort(comparator); + renderFilteredEntries(searchInput.value); + } +} + +function renderDirectoryPage() { + const currentEntries = addPagination(filteredEntries); + displayEntries(currentEntries); +} + +function renderFilteredEntries(s) { + const query = s.toLowerCase().trim(); + if (query === '') { + filteredEntries = sortedEntries.slice(); + } else { + filteredEntries = sortedEntries.filter(entry => + (entry.displayName || '').toLowerCase().includes(query) + || includesQuery(entry.shortDescr, query) + || includesQuery(entry.welcomeMessage, query) + ); + } + renderDirectoryPage(); +} + +function includesQuery(field, query) { + return field + && Array.isArray(field) + && field.some(ft => { + switch (ft.format?.type) { + case 'uri': return uriIncludesQuery(ft.text, query); + case 'hyperLink': return textIncludesQuery(ft.format.showText, query) || uriIncludesQuery(ft.format.linkUri, query); + case 'simplexLink': return textIncludesQuery(ft.format.showText, query); + default: return textIncludesQuery(ft.text, query); + } + }); +} + +function textIncludesQuery(text, query) { + text ? text.toLowerCase().includes(query) : false +} + +function uriIncludesQuery(uri, query) { + if (!uri) return false; + uri = uri.toLowerCase(); + return !uri.includes('simplex') && uri.includes(query); +} + +async function fetchJSON(url) { + try { + const response = await fetch(url) + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`) + return await response.json() + } catch (error) { + console.error('Error fetching JSON:', error) + } +} + +function byMemberCountDesc(entry1, entry2) { + return entryMemberCount(entry2) - entryMemberCount(entry1); +} + +function byActiveAtDesc(entry1, entry2) { + return (roundedTs(entry2.activeAt) - roundedTs(entry1.activeAt)) * 10 + + Math.sign(byMemberCountDesc(entry1, entry2)); +} + +function byCreatedAtDesc(entry1, entry2) { + return (roundedTs(entry2.createdAt) - roundedTs(entry1.createdAt)) * 10 + + Math.sign(byMemberCountDesc(entry1, entry2)); +} + +function roundedTs(s) { + try { + // rounded to 15 minutes, which is the frequency of listing update + return Math.floor(new Date(s).valueOf() / 900000); + } catch { + return 0; + } +} + +function entryMemberCount(entry) { + return entry.entryType.type == 'group' + ? (entry.entryType.summary?.currentMembers ?? 0) + : 0 +} + +function displayEntries(entries) { + const directory = document.getElementById('directory'); + directory.innerHTML = ''; + + for (let entry of entries) { + try { + const { entryType, displayName, groupLink, shortDescr, welcomeMessage, imageFile } = entry; + const entryDiv = document.createElement('div'); + entryDiv.className = 'entry w-full flex flex-col items-start md:flex-row rounded-[4px] overflow-hidden shadow-[0px_20px_30px_rgba(0,0,0,0.12)] dark:shadow-none bg-white dark:bg-[#11182F] mb-8'; + + const textContainer = document.createElement('div'); + textContainer.className = 'text-container'; + + const nameElement = document.createElement('h2'); + nameElement.textContent = displayName; + nameElement.className = 'text-grey-black dark:text-white !text-lg md:!text-xl font-bold'; + textContainer.appendChild(nameElement); + + const welcomeMessageHTML = welcomeMessage ? renderMarkdown(welcomeMessage) : undefined; + const shortDescrHTML = shortDescr ? renderMarkdown(shortDescr) : undefined; + if (shortDescrHTML && welcomeMessageHTML?.includes(shortDescrHTML) !== true) { + const descrElement = document.createElement('p'); + descrElement.innerHTML = renderMarkdown(shortDescr); + textContainer.appendChild(descrElement); + } + + if (welcomeMessageHTML) { + const messageElement = document.createElement('p'); + messageElement.innerHTML = welcomeMessageHTML; + textContainer.appendChild(messageElement); + + const readMore = document.createElement('p'); + readMore.textContent = 'Read more'; + readMore.className = 'read-more'; + readMore.style.display = 'none'; + textContainer.appendChild(readMore); + + setTimeout(() => { + const computedStyle = window.getComputedStyle(messageElement); + const lineHeight = parseFloat(computedStyle.lineHeight); + const maxLines = 5; + const maxHeight = maxLines * lineHeight + const maxHeightPx = `${maxHeight}px`; + messageElement.style.maxHeight = maxHeightPx; + messageElement.style.overflow = 'hidden'; + + if (messageElement.scrollHeight > maxHeight + 4) { + readMore.style.display = 'block'; + readMore.addEventListener('click', () => { + if (messageElement.style.maxHeight === maxHeightPx) { + messageElement.style.maxHeight = 'none'; + readMore.className = 'read-less'; + readMore.innerHTML = '▲'; + } else { + messageElement.style.maxHeight = maxHeightPx; + readMore.className = 'read-more'; + readMore.textContent = 'Read more'; + } + }); + } + }, 0); + } + + const memberCount = entryMemberCount(entry); + if (typeof memberCount == 'number' && memberCount > 0) { + const memberCountElement = document.createElement('p'); + memberCountElement.innerText = `${memberCount} members`; + memberCountElement.classList = ['text-sm']; + textContainer.appendChild(memberCountElement); + } + + const imgElement = document.createElement('a'); + imgSource = + imageFile + ? directoryDataURL + imageFile + : "/img/group.svg"; + imgElement.innerHTML = `${displayName}`; + imgElement.href = platformSimplexUri(groupLink.connShortLink ?? groupLink.connFullLink); + if (!isCurrentSite(imgElement.href)) imgElement.target = "_blank"; + imgElement.title = `Join ${displayName}`; + entryDiv.appendChild(imgElement); + + entryDiv.appendChild(textContainer); + directory.appendChild(entryDiv); + } catch (e) { + console.log(e); + } + } + + for (let el of document.querySelectorAll('.secret')) { + el.addEventListener('click', () => el.classList.toggle('visible')); + } + + directory.style.height = ''; +} + +function goToPage(p) { + location.hash = p.toString(); +} + +function addPagination(entries) { + const entriesPerPage = 10; + const totalPages = Math.ceil(entries.length / entriesPerPage); + let currentPage = parseInt(location.hash.slice(1)) || 1; + if (currentPage < 1) currentPage = 1; + if (currentPage > totalPages) currentPage = totalPages; + + const startIndex = (currentPage - 1) * entriesPerPage; + const endIndex = Math.min(startIndex + entriesPerPage, entries.length); + const currentEntries = entries.slice(startIndex, endIndex); + + // addPaginationElements('top-pagination') + addPaginationElements('bottom-pagination') + return currentEntries; + + function addPaginationElements(paginationId) { + const pagination = document.getElementById(paginationId); + if (!pagination) { + return currentEntries; + } + pagination.innerHTML = ''; + + try { + let startPage, endPage; + const pageButtonCount = 8 + if (totalPages <= pageButtonCount) { + startPage = 1; + endPage = totalPages; + } else { + startPage = Math.max(1, currentPage - 4); + endPage = Math.min(totalPages, startPage + pageButtonCount - 1); + if (endPage - startPage + 1 < pageButtonCount) { + startPage = Math.max(1, endPage - pageButtonCount + 1); + } + } + + // if (currentPage > 1 && startPage > 1) { + // const firstBtn = document.createElement('button'); + // firstBtn.textContent = 'First'; + // firstBtn.classList.add('text-btn'); + // firstBtn.addEventListener('click', () => goToPage(1)); + // pagination.appendChild(firstBtn); + // } + + if (currentPage > 1) { + const prevBtn = document.createElement('button'); + prevBtn.textContent = 'Prev'; + prevBtn.classList.add('text-btn'); + prevBtn.addEventListener('click', () => goToPage(currentPage - 1)); + pagination.appendChild(prevBtn); + } + + for (let p = startPage; p <= endPage; p++) { + const pageBtn = document.createElement('button'); + pageBtn.textContent = p.toString(); + if (p === currentPage) { + pageBtn.classList.add('active'); + } else if (p === currentPage - 1 || p === currentPage + 1) { + pageBtn.classList.add('neighbor'); + } + pageBtn.addEventListener('click', () => goToPage(p)); + pagination.appendChild(pageBtn); + } + + if (currentPage < totalPages) { + const nextBtn = document.createElement('button'); + nextBtn.textContent = 'Next'; + nextBtn.classList.add('text-btn'); + nextBtn.addEventListener('click', () => goToPage(currentPage + 1)); + pagination.appendChild(nextBtn); + } + + // if (endPage < totalPages) { + // const lastBtn = document.createElement('button'); + // lastBtn.textContent = 'Last'; + // lastBtn.classList.add('text-btn'); + // lastBtn.addEventListener('click', () => goToPage(totalPages)); + // pagination.appendChild(lastBtn); + // } + + } catch (e) { + console.log(e); + } + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initDirectory); +} else { + initDirectory(); +} + +function escapeHtml(text) { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/\n/g, "
"); +} + +function getSimplexLinkDescr(linkType) { + switch (linkType) { + case 'contact': return 'SimpleX contact address'; + case 'invitation': return 'SimpleX one-time invitation'; + case 'group': return 'SimpleX group link'; + case 'channel': return 'SimpleX channel link'; + case 'relay': return 'SimpleX relay link'; + default: return 'SimpleX link'; + } +} + +function viaHost(smpHosts) { + const first = smpHosts[0] ?? '?'; + return `via ${first}`; +} + +function isCurrentSite(uri) { + return uri.startsWith("https://simplex.chat") || uri.startsWith("https://www.simplex.chat") +} + +function targetBlank(uri) { + return isCurrentSite(uri) ? '' : ' target="_blank"' +} + +function platformSimplexUri(uri) { + if (isMobile.any()) return uri; + if (uri.startsWith('simplex:/g#')) { + const prefixLength = 'simplex:/g#'.length; + const fragment = uri.substring(prefixLength); + const queryIndex = fragment.indexOf('?'); + if (queryIndex === -1) return uri; + const hashPart = fragment.substring(0, queryIndex); + const queryStr = fragment.substring(queryIndex + 1); + const params = new URLSearchParams(queryStr); + const host = params.get('h'); + if (!host) return uri; + params.delete('h'); + let newFragment = hashPart; + const remainingParams = params.toString(); + if (remainingParams) newFragment += '?' + remainingParams; + return `https://${host}:/g#${newFragment}`; + } else if(uri.startsWith('simplex:/')) { + const prefixLength = 'simplex:/'.length; + return 'https://simplex.chat/' + uri.substring(prefixLength); + } else { + return uri; + } +} + +function renderMarkdown(fts) { + let html = ''; + for (const ft of fts) { + const { format, text } = ft; + if (!format) { + html += escapeHtml(text); + continue; + } + try { + switch (format.type) { + case 'bold': + html += `${escapeHtml(text)}`; + break; + case 'italic': + html += `${escapeHtml(text)}`; + break; + case 'strikeThrough': + html += `${escapeHtml(text)}`; + break; + case 'snippet': + html += `${escapeHtml(text)}`; + break; + case 'secret': + html += `${escapeHtml(text)}`; + break; + case 'colored': + html += `${escapeHtml(text)}`; + break; + case 'uri': + let href = text.startsWith('http://') || text.startsWith('https://') || text.startsWith('simplex:/') ? text : 'https://' + text; + html += `${escapeHtml(text)}`; + break; + case 'hyperLink': { + const { showText, linkUri } = format; + html += `${escapeHtml(showText ?? linkUri)}`; + break; + } + case 'simplexLink': { + const { showText, linkType, simplexUri, smpHosts } = format; + const linkText = showText ? escapeHtml(showText) : getSimplexLinkDescr(linkType); + html += `${linkText} (${viaHost(smpHosts)})`; + break; + } + case 'command': + html += `${escapeHtml(text)}`; + break; + case 'mention': + html += `${escapeHtml(text)}`; + break; + case 'email': + html += `${escapeHtml(text)}`; + break; + case 'phone': + html += `${escapeHtml(text)}`; + break; + case 'unknown': + html += escapeHtml(text); + break; + default: + html += escapeHtml(text); + } + } catch { + html += escapeHtml(text); + } + } + return html; +} diff --git a/website/src/js/script.js b/website/src/js/script.js index 5f863f48ee..cf240dd375 100644 --- a/website/src/js/script.js +++ b/website/src/js/script.js @@ -26,7 +26,8 @@ const uniqueSwiper = new Swiper('.unique-swiper', { const isMobile = { Android: () => navigator.userAgent.match(/Android/i), - iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i) + iOS: () => navigator.userAgent.match(/iPhone|iPad|iPod/i), + any: () => navigator.userAgent.match(/Android|iPhone|iPad|iPod/i) }; const privateSwiper = new Swiper('.private-swiper', { From a1eee94a863d37df247bc05c60aaf2281b1c34bb Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 20 Sep 2025 20:11:34 +0100 Subject: [PATCH 047/137] website: fix search in directory --- website/src/js/directory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/js/directory.js b/website/src/js/directory.js index 2eb8db71a1..d25b5ae275 100644 --- a/website/src/js/directory.js +++ b/website/src/js/directory.js @@ -71,7 +71,7 @@ function includesQuery(field, query) { } function textIncludesQuery(text, query) { - text ? text.toLowerCase().includes(query) : false + return text ? text.toLowerCase().includes(query) : false } function uriIncludesQuery(uri, query) { From 5a951c56b2652657a2d51d9485741d5a1828d674 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 21 Sep 2025 09:43:02 +0100 Subject: [PATCH 048/137] android, desktop: update dependency --- apps/multiplatform/common/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 8b4dd0e4d7..2227766cfa 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -113,7 +113,7 @@ kotlin { } // For jSystemThemeDetector only implementation("net.java.dev.jna:jna-platform:5.14.0") - implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT") + implementation("com.sshtools:two-slices:0.9.1") implementation("org.slf4j:slf4j-simple:2.0.12") implementation("uk.co.caprica:vlcj:4.8.3") implementation("net.java.dev.jna:jna:5.14.0") From 83b655ea8318291f8a8017fb0afbf48de2aa74a6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 21 Sep 2025 10:40:06 +0100 Subject: [PATCH 049/137] website(directory): add bot address, open simplex links in new tab, improve error handling --- website/src/directory.html | 2 +- website/src/js/directory.js | 40 ++++++++++++++++++++++++------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/website/src/directory.html b/website/src/directory.html index 247ee6ebc2..333d7faed8 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -256,7 +256,7 @@ templateEngineOverride: njk

SimpleX Directory

Welcome to the selected users' communities that you can join via SimpleX Chat app.

-

SimpleX Directory is also available as a SimpleX chat bot.

+

SimpleX Directory is also available as a SimpleX chat bot.

Read about how to add your community.

diff --git a/website/src/js/directory.js b/website/src/js/directory.js index d25b5ae275..313bdeedec 100644 --- a/website/src/js/directory.js +++ b/website/src/js/directory.js @@ -1,3 +1,4 @@ +(function() { const directoryDataURL = 'https://directory.simplex.chat/data/'; // const directoryDataURL = 'http://localhost:8080/directory-data/'; @@ -83,10 +84,10 @@ function uriIncludesQuery(uri, query) { async function fetchJSON(url) { try { const response = await fetch(url) - if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`) + if (!response.ok) throw new Error(`HTTP status: ${response.status}`) return await response.json() - } catch (error) { - console.error('Error fetching JSON:', error) + } catch (e) { + console.error(e) } } @@ -196,7 +197,13 @@ function displayEntries(entries) { ? directoryDataURL + imageFile : "/img/group.svg"; imgElement.innerHTML = `${displayName}`; - imgElement.href = platformSimplexUri(groupLink.connShortLink ?? groupLink.connFullLink); + const groupLinkUri = groupLink.connShortLink ?? groupLink.connFullLink + try { + imgElement.href = platformSimplexUri(groupLinkUri); + } catch(e) { + console.log(e); + imgElement.href = groupLinkUri; + } if (!isCurrentSite(imgElement.href)) imgElement.target = "_blank"; imgElement.title = `Join ${displayName}`; entryDiv.appendChild(imgElement); @@ -345,11 +352,17 @@ function targetBlank(uri) { return isCurrentSite(uri) ? '' : ' target="_blank"' } +const simplexAddressRegexp = /^simplex:\/([a-z]+)#(.+)/i; + +const simplexShortLinkTypes = ["a", "c", "g", "i", "r"]; + function platformSimplexUri(uri) { if (isMobile.any()) return uri; - if (uri.startsWith('simplex:/g#')) { - const prefixLength = 'simplex:/g#'.length; - const fragment = uri.substring(prefixLength); + const res = uri.match(simplexAddressRegexp); + if (!res || !Array.isArray(res) || res.length < 3) return uri; + const linkType = res[1]; + const fragment = res[2]; + if (simplexShortLinkTypes.includes(linkType)) { const queryIndex = fragment.indexOf('?'); if (queryIndex === -1) return uri; const hashPart = fragment.substring(0, queryIndex); @@ -361,12 +374,9 @@ function platformSimplexUri(uri) { let newFragment = hashPart; const remainingParams = params.toString(); if (remainingParams) newFragment += '?' + remainingParams; - return `https://${host}:/g#${newFragment}`; - } else if(uri.startsWith('simplex:/')) { - const prefixLength = 'simplex:/'.length; - return 'https://simplex.chat/' + uri.substring(prefixLength); + return `https://${host}:/${linkType}#${newFragment}`; } else { - return uri; + return `https://simplex.chat/${fragment}`; } } @@ -410,7 +420,7 @@ function renderMarkdown(fts) { case 'simplexLink': { const { showText, linkType, simplexUri, smpHosts } = format; const linkText = showText ? escapeHtml(showText) : getSimplexLinkDescr(linkType); - html += `${linkText} (${viaHost(smpHosts)})`; + html += `${linkText} (${viaHost(smpHosts)})`; break; } case 'command': @@ -431,9 +441,11 @@ function renderMarkdown(fts) { default: html += escapeHtml(text); } - } catch { + } catch(e) { + console.log(e); html += escapeHtml(text); } } return html; } +})(); From c09665b9200fa5dfeafcd5fc60944372968355c1 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 21 Sep 2025 14:36:58 +0100 Subject: [PATCH 050/137] directory: show only recent groups in active/new tabs, page improvements (#6290) * directory: show only recent groups as active or new * round times * sorting order * fix links * improve --- .../src/Directory/Listing.hs | 28 ++++-- website/src/directory.html | 2 +- website/src/js/directory.js | 85 +++++++++++-------- 3 files changed, 72 insertions(+), 43 deletions(-) diff --git a/apps/simplex-directory-service/src/Directory/Listing.hs b/apps/simplex-directory-service/src/Directory/Listing.hs index b9b50cb87b..cef478c273 100644 --- a/apps/simplex-directory-service/src/Directory/Listing.hs +++ b/apps/simplex-directory-service/src/Directory/Listing.hs @@ -23,13 +23,15 @@ import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64.URL as B64URL import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as LB +import Data.Int (Int64) import Data.List (isPrefixOf) import Data.Maybe (catMaybes, fromMaybe) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) -import Data.Time.Clock (UTCTime, getCurrentTime) +import Data.Time.Clock +import Data.Time.Clock.System import Data.Time.Format.ISO8601 (iso8601Show) import Directory.Store import Simplex.Chat.Markdown @@ -66,8 +68,8 @@ data DirectoryEntry = DirectoryEntry shortDescr :: Maybe MarkdownList, welcomeMessage :: Maybe MarkdownList, imageFile :: Maybe String, - activeAt :: UTCTime, - createdAt :: UTCTime + activeAt :: Maybe UTCTime, + createdAt :: Maybe UTCTime } $(JQ.deriveJSON defaultJSON ''DirectoryEntry) @@ -78,8 +80,18 @@ $(JQ.deriveJSON defaultJSON ''DirectoryListing) type ImageFileData = ByteString -groupDirectoryEntry :: GroupInfoSummary -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData)) -groupDirectoryEntry (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLink_) = +newOrActive :: NominalDiffTime +newOrActive = 30 * nominalDay + +recentRoundedTime :: Int64 -> UTCTime -> UTCTime -> Maybe UTCTime +recentRoundedTime roundTo now t + | diffUTCTime now t > newOrActive = Nothing + | otherwise = + let secs = (systemSeconds (utcToSystemTime t) `div` roundTo) * roundTo + in Just $ systemToUTCTime $ MkSystemTime secs 0 + +groupDirectoryEntry :: UTCTime -> GroupInfoSummary -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData)) +groupDirectoryEntry now (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLink_) = let GroupProfile {displayName, shortDescr, description, image, memberAdmission} = groupProfile entryType = DETGroup memberAdmission summary entry groupLink = @@ -91,8 +103,8 @@ groupDirectoryEntry (GIS GroupInfo {groupProfile, chatTs, createdAt} summary gLi shortDescr = toFormattedText <$> shortDescr, welcomeMessage = toFormattedText <$> description, imageFile = fst <$> imgData, - activeAt = fromMaybe createdAt chatTs, - createdAt + activeAt = recentRoundedTime 900 now $ fromMaybe createdAt chatTs, + createdAt = recentRoundedTime 86400 now createdAt } imgData = imgFileData groupLink =<< image in (de, imgData) @@ -121,7 +133,7 @@ generateListing st dir gs = do createDirectoryIfMissing True (newDir listingImageFolder) gs'' <- fmap catMaybes $ forM gs' $ \g@(GIS GroupInfo {groupId} _ _) -> - forM (groupDirectoryEntry g) $ \(g', img) -> do + forM (groupDirectoryEntry ts g) $ \(g', img) -> do forM_ img $ \(imgFile, imgData) -> B.writeFile (newDir imgFile) imgData pure (groupId, g') saveListing newDir listingFileName gs'' diff --git a/website/src/directory.html b/website/src/directory.html index 333d7faed8..1d87ffdaa9 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -263,7 +263,7 @@ templateEngineOverride: njk
diff --git a/website/src/js/directory.js b/website/src/js/directory.js index 313bdeedec..72c93d35fb 100644 --- a/website/src/js/directory.js +++ b/website/src/js/directory.js @@ -5,12 +5,14 @@ const directoryDataURL = 'https://directory.simplex.chat/data/'; let allEntries = []; -let sortedEntries = []; - let filteredEntries = []; let currentSortMode = ''; +let currentSearch = ''; + +let currentPage = 1; + async function initDirectory() { const listing = await fetchJSON(directoryDataURL + 'listing.json') const liveBtn = document.querySelector('#top-pagination .live'); @@ -18,24 +20,29 @@ async function initDirectory() { const topBtn = document.querySelector('#top-pagination .top'); const searchInput = document.getElementById('search'); allEntries = listing.entries - renderSortedEntries('top', byMemberCountDesc, topBtn) - window.addEventListener('hashchange', renderDirectoryPage); - searchInput.addEventListener('input', (e) => renderFilteredEntries(e.target.value)); + renderEntries('top', bySortPriority, topBtn) + searchInput.addEventListener('input', (e) => renderEntries('top', bySortPriority, topBtn, e.target.value.trim())); + liveBtn.addEventListener('click', () => renderEntries('live', byActiveAtDesc, liveBtn, '')); + newBtn.addEventListener('click', () => renderEntries('new', byCreatedAtDesc, newBtn, '')); + topBtn.addEventListener('click', () => renderEntries('top', bySortPriority, topBtn, '')); - liveBtn.addEventListener('click', () => renderSortedEntries('live', byActiveAtDesc, liveBtn)); - newBtn.addEventListener('click', () => renderSortedEntries('new', byCreatedAtDesc, newBtn)); - topBtn.addEventListener('click', () => renderSortedEntries('top', byMemberCountDesc, topBtn)); - - function renderSortedEntries(mode, comparator, btn) { - if (currentSortMode === mode) return; + function renderEntries(mode, comparator, btn, search) { + if (currentSortMode === mode && search == currentSearch) return; currentSortMode = mode; if (location.hash) location.hash = ''; liveBtn.classList.remove('active'); newBtn.classList.remove('active'); topBtn.classList.remove('active'); - btn.classList.add('active'); - sortedEntries = allEntries.slice().sort(comparator); - renderFilteredEntries(searchInput.value); + if (search == '') { + currentSearch = ''; + currentPage = 1; + searchInput.value = ''; + btn.classList.add('active'); + } else { + currentSearch = search; + } + filteredEntries = filterEntries(mode, search ?? '').sort(comparator); + renderDirectoryPage(); } } @@ -44,18 +51,20 @@ function renderDirectoryPage() { displayEntries(currentEntries); } -function renderFilteredEntries(s) { - const query = s.toLowerCase().trim(); - if (query === '') { - filteredEntries = sortedEntries.slice(); - } else { - filteredEntries = sortedEntries.filter(entry => - (entry.displayName || '').toLowerCase().includes(query) - || includesQuery(entry.shortDescr, query) - || includesQuery(entry.welcomeMessage, query) - ); - } - renderDirectoryPage(); +function filterEntries(mode, s) { + if (s === '' && mode == 'top') return allEntries.slice(); + const query = s.toLowerCase(); + return allEntries.filter(entry => + ( mode === 'top' + || (mode === 'new' && entry.createdAt) + || (mode === 'live' && entry.activeAt) + ) && + ( query === '' + || (entry.displayName || '').toLowerCase().includes(query) + || includesQuery(entry.shortDescr, query) + || includesQuery(entry.welcomeMessage, query) + ) + ); } function includesQuery(field, query) { @@ -91,18 +100,18 @@ async function fetchJSON(url) { } } -function byMemberCountDesc(entry1, entry2) { - return entryMemberCount(entry2) - entryMemberCount(entry1); +function bySortPriority(entry1, entry2) { + return entrySortPriority(entry2) - entrySortPriority(entry1); } function byActiveAtDesc(entry1, entry2) { return (roundedTs(entry2.activeAt) - roundedTs(entry1.activeAt)) * 10 - + Math.sign(byMemberCountDesc(entry1, entry2)); + + Math.sign(bySortPriority(entry1, entry2)); } function byCreatedAtDesc(entry1, entry2) { return (roundedTs(entry2.createdAt) - roundedTs(entry1.createdAt)) * 10 - + Math.sign(byMemberCountDesc(entry1, entry2)); + + Math.sign(bySortPriority(entry1, entry2)); } function roundedTs(s) { @@ -114,6 +123,14 @@ function roundedTs(s) { } } +const simplexUsersGroup = 'SimpleX users group'; + +function entrySortPriority(entry) { + return entry.displayName === simplexUsersGroup + ? Number.MAX_VALUE + : entryMemberCount(entry) +} + function entryMemberCount(entry) { return entry.entryType.type == 'group' ? (entry.entryType.summary?.currentMembers ?? 0) @@ -204,7 +221,7 @@ function displayEntries(entries) { console.log(e); imgElement.href = groupLinkUri; } - if (!isCurrentSite(imgElement.href)) imgElement.target = "_blank"; + imgElement.target = "_blank"; imgElement.title = `Join ${displayName}`; entryDiv.appendChild(imgElement); @@ -223,13 +240,13 @@ function displayEntries(entries) { } function goToPage(p) { - location.hash = p.toString(); + currentPage = p; + renderDirectoryPage(); } function addPagination(entries) { const entriesPerPage = 10; const totalPages = Math.ceil(entries.length / entriesPerPage); - let currentPage = parseInt(location.hash.slice(1)) || 1; if (currentPage < 1) currentPage = 1; if (currentPage > totalPages) currentPage = totalPages; @@ -376,7 +393,7 @@ function platformSimplexUri(uri) { if (remainingParams) newFragment += '?' + remainingParams; return `https://${host}:/${linkType}#${newFragment}`; } else { - return `https://simplex.chat/${fragment}`; + return `https://simplex.chat/${linkType}#${fragment}`; } } From bfd9d794129c68f6bb7db5e43021baac32b7e6fe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 21 Sep 2025 19:15:10 +0100 Subject: [PATCH 051/137] directory: disable search autocomplete --- website/src/directory.html | 2 +- website/src/js/directory.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/website/src/directory.html b/website/src/directory.html index 1d87ffdaa9..a75d775bc5 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -259,7 +259,7 @@ templateEngineOverride: njk

SimpleX Directory is also available as a SimpleX chat bot.

Read about how to add your community.

- +