Compare commits

..

25 Commits

Author SHA1 Message Date
Alexander Bondarenko d49244b654 add /debug subs 2024-04-30 22:00:57 +03:00
Alexander Bondarenko d0f21b943c debug conn by chat connection id from contact info 2024-04-30 20:10:25 +03:00
IC Rainbow ef5a144b7b Merge remote-tracking branch 'origin/master' into ab/debug-subs 2024-04-29 21:14:10 +03:00
Alexander Bondarenko 8d0d8208ce fix undefined error 2024-04-29 20:38:54 +03:00
spaced4ndy 37e03a838c core: forward group message before ack (fixes forwarding message that deleted connection causing error in ackMsg) (#4108) 2024-04-29 19:49:04 +04:00
spaced4ndy aeb28400e9 core: take entity lock before processing file (#4105) 2024-04-29 19:32:53 +04:00
Evgeny Poberezkin 83ab8d6ef2 more tracking 2024-04-28 18:27:43 +01:00
Evgeny Poberezkin 7d4d0dd2c0 Merge branch 'master' into ab/debug-subs 2024-04-28 09:09:30 +01:00
sh d5a0c5e56a scripts/build-android: update nix and refactor (#4099) 2024-04-27 09:23:14 +01:00
Evgeny Poberezkin a28d965a65 website: fix privacy page 2024-04-26 23:24:54 +01:00
Evgeny Poberezkin 9148425eba blog: legally binding transparency, v5.7 announcement (#4097)
* blog: legally binding transparency, v5.7 announcement

* update

* update, images

* readme

* update
2024-04-26 23:06:40 +01:00
M. Sarmad Qadeer 406c35958e web: add privacy page to website (#4096)
* web: add privacy page to website

* web: fix a blog frontmatter

* web: fix privacy link from other docs

* update

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-04-26 23:00:49 +01:00
Alexander Bondarenko 982c4afd7b WIP: don't crash on missing corrId from acks 2024-04-26 22:52:43 +03:00
Alexander Bondarenko 24e718c7d5 one more /_stop 2024-04-26 22:05:51 +03:00
Alexander Bondarenko d3e6d638af fix late event in test 2024-04-26 21:17:47 +03:00
Alexander Bondarenko f6f23119cf don't omit nulls in debug objects 2024-04-26 18:57:11 +03:00
Alexander Bondarenko 33c36c8101 add Connection wholesale 2024-04-26 18:43:49 +03:00
Alexander Bondarenko 0622a52e00 fix withCompletedCommand 2024-04-26 18:25:03 +03:00
Evgeny Poberezkin 4634788ac0 Merge branch 'master' into ab/debug-subs 2024-04-26 15:47:07 +01:00
Alexander Bondarenko 40e717e7f4 read conn networkStatus 2024-04-26 17:39:19 +03:00
Alexander Bondarenko 97bdb9484b format 2024-04-26 17:25:33 +03:00
Alexander Bondarenko 3afc025a0d add connection debug 2024-04-26 17:10:38 +03:00
Alexander Bondarenko 19a3ab6230 fix DebugDelivery 2024-04-26 15:41:53 +03:00
Alexander Bondarenko ce5cb3137c collect some fields 2024-04-25 21:56:25 +03:00
Alexander Bondarenko 05d0554fef chat: delivery troubleshooting helper 2024-04-25 19:29:59 +03:00
30 changed files with 521 additions and 104 deletions
+1
View File
@@ -54,6 +54,7 @@ website/translations.json
website/src/img/images/ website/src/img/images/
website/src/images/ website/src/images/
website/src/js/lottie.min.js website/src/js/lottie.min.js
website/src/privacy.md
# Generated files # Generated files
website/package/generated* website/package/generated*
+9 -5
View File
@@ -1,16 +1,20 @@
---
layout: layouts/privacy.html
---
# SimpleX Chat Privacy Policy and Conditions of Use # SimpleX Chat Privacy Policy and Conditions of Use
SimpleX Chat is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability. SimpleX Chat is the first communication network based on a new protocol stack that builds on the same ideas of complete openness and decentralization as email and web, with the focus on providing security and privacy of communications, and without compromising on usability.
SimpleX Chat communication protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX Chat apps allow their users to send messages and files via relay server infrastructure. Relay server owners and providers do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not provide any user accounts. SimpleX Chat communication protocol is the first protocol that has no user profile IDs of any kind, not even random numbers, cryptographic keys or hashes that identify the users. SimpleX Chat apps allow their users to send messages and files via relay server infrastructure. Relay server owners and providers do not have any access to your messages, thanks to double-ratchet end-to-end encryption algorithm (also known as Signal algorithm - do not confuse with Signal protocols or platform) and additional encryption layers, and they also have no access to your profile and contacts - as they do not provide any user accounts.
Double ratchet algorithm has such important properties as [forward secrecy](./docs/GLOSSARY.md#forward-secrecy), sender [repudiation](./docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](./docs/GLOSSARY.md#post-compromise-security)). Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)).
If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
## Privacy Policy ## Privacy Policy
SimpleX Chat Ltd uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](./docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption cannot be compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](./docs/GLOSSARY.md#man-in-the-middle-attack), unlike most other communication platforms, services and networks. SimpleX Chat Ltd uses the best industry practices for security and encryption to provide client and server software for secure [end-to-end encrypted](/docs/GLOSSARY.md#end-to-end-encryption) messaging via private connections. This encryption cannot be compromised by the relays servers, even if they are modified or compromised, via [man-in-the-middle attack](/docs/GLOSSARY.md#man-in-the-middle-attack), unlike most other communication platforms, services and networks.
SimpleX Chat software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having any kind of addresses or other identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications. SimpleX Chat software is built on top of SimpleX messaging and application protocols, based on a new message routing protocol allowing to establish private connections without having any kind of addresses or other identifiers assigned to its users - it does not use emails, phone numbers, usernames, identity keys or any other user profile identifiers to pass messages between the user applications.
@@ -20,7 +24,7 @@ While SimpleX Chat Ltd is not a communication service provider, and provide publ
We see users and data sovereignty, and device and provider portability as critically important properties for any communication system. We see users and data sovereignty, and device and provider portability as critically important properties for any communication system.
SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). SimpleX Chat security assessment was done in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2 see [the announcement](/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
### Your information ### Your information
@@ -34,7 +38,7 @@ You can transfer the profile to another device by creating a backup of the app d
#### Messages and Files #### Messages and Files
SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 8mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](./docs/GLOSSARY.md#key-exchange) happens out-of-band. SimpleX relay servers cannot decrypt or otherwise access the content or even the size of your messages and files you send or receive. Each message is padded to a fixed size of 16kb. Each file is sent in chunks of 64kb, 256kb, 1mb or 8mb via all or some of the configured file relay servers. Both messages and files are sent end-to-end encrypted, and the servers do not have technical means to compromise this encryption, because part of the [key exchange](/docs/GLOSSARY.md#key-exchange) happens out-of-band.
Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline, messaging relay servers temporarily store end-to-end encrypted messages you can configure which relay servers are used to receive the messages from the new contacts, and you can manually change them for the existing contacts too. Your message history is stored only on your own device and the devices of your contacts. While the recipients' devices are offline, messaging relay servers temporarily store end-to-end encrypted messages you can configure which relay servers are used to receive the messages from the new contacts, and you can manually change them for the existing contacts too.
@@ -82,7 +86,7 @@ Additional technical information can be stored on our servers, including randoml
#### SimpleX Directory #### SimpleX Directory
[SimpleX Directory](./docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). [SimpleX Directory](/docs/DIRECTORY.md) stores: your search requests, the messages and the members profiles in the registered groups. You can connect to SimpleX Directory via [this address](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).
#### User Support #### User Support
+5 -3
View File
@@ -234,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates: Recent and important updates:
[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) [Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) [Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md)
@@ -412,13 +414,13 @@ We have never provided or have been requested access to our servers or any infor
We do not log IP addresses of the users and we do not perform any traffic correlation on our servers. If transport level security is critical you must use Tor or some other similar network to access messaging servers. We will be improving the client applications to reduce the opportunities for traffic correlation. We do not log IP addresses of the users and we do not perform any traffic correlation on our servers. If transport level security is critical you must use Tor or some other similar network to access messaging servers. We will be improving the client applications to reduce the opportunities for traffic correlation.
Please read more in [Terms & privacy policy](./PRIVACY.md). Please read more in [Privacy Policy](./PRIVACY.md).
## Security contact ## Security contact
To report a security vulnerability, please send us email to chat@simplex.chat. We will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues. Please see our [Security Policy](./docs/SECURITY.md) on how to report security vulnerabilities to us. We will coordinate the fix and disclosure.
Please treat any findings of possible traffic correlation attacks allowing to correlate two different conversations to the same user, other than covered in [the threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model), as security vulnerabilities, and follow this disclosure process. Please do NOT report security vulnerabilities via GitHub issues.
## License ## License
@@ -1,13 +1,115 @@
--- ---
layout: layouts/article.html layout: layouts/article.html
title: "SimpleX network: legally binding transparency, v5.7 released with better user experience for calls and messages" title: "SimpleX network: legally binding transparency, v5.7 released with better calls and messages"
date: 2024-04-26 date: 2024-04-26
preview: blog_previews/20240323.html previewBody: blog_previews/20240426.html
draft: true image: images/20240426-profile.png
imageBottom: true
permalink: "/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html" permalink: "/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html"
--- ---
# SimpleX network: legally binding transparency, v5.7 released with better user experience for calls and messages # SimpleX network: legally binding transparency, v5.7 released with better calls and messages
This is a permalink for the post What's new in v5.7:
- [quantum resistant end-to-end encryption](#quantum-resistant-end-to-end-encryption) with all contacts.
- [forward and save messages](#forward-and-save-messages) without revealing the source.
- [in-call sounds and switching sound sources](#in-call-sounds-and-switching-sound-sources).
- [better network connection management](#network-management).
- [customizable profile images](#customizable-shape-of-profile-images)
Also, we added Lithuanian interface language to the Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat).
## Legally binding transparency
We are committed to open-source, privacy and security. Here are the recent changes we made:
- We now have a [Transparency Reports](https://simplex.chat/transparency/) page.
- We updated our [Privacy Policy](https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files).
- We published a new page with [Frequently Asked Questions](https://simplex.chat/faq/), thanks to the guidance from users.
- We also have a new [Security Policy](https://simplex.chat/security/) we welcome your feedback on it.
What do we mean by “legally binding transparency?”. It includes these principles:
- Accountability: an empty promise or commitment to transparency that is not legally binding is just marketing, and can provide opportunities for the organizations to be misleading or not disclose important information that can affect their users privacy and security.
- Consistency: often, there's a disconnect between marketing claims and legally binding policies. Our approach is to ensure that promises made in marketing materials or any external communications align with our legally binding documents, so that users can rely on our promises, and know exactly what to expect from us.
For example:
- we use open-source code, and we made a legally binding commitment to use the published code in all released apps and deployed preset servers.
- we use precise and technical language in the Privacy Policy defining what data and metadata can be accessed via the preset relays.
If you see any inconsistency between technical parameters of SimpleX Network and what is promised in our Privacy Policy please raise it with us.
## What's new in v5.7
This release focus is improving the app usability, and preparing the foundation for v5.8 that will provide an in-built protection of user IP addresses when connecting to unknown file and messaging servers, reducing the need to use Tor (which would still remain supported via SOCKS proxy, for additional privacy).
### Quantum resistant end-to-end encryption
<img src="./images/20240426-pq.png" width="288" class="float-right">
We [wrote before](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) about how quantum resistant encryption was added to SimpleX Chat and also about other properties of end-to-end encryption, possible attacks on its security and known mitigations.
Quantum resistant encryption will now be enabled by default in all direct chats. For the new conversations it will be enabled from the beginning, and for the existing conversations it will be agreed after you exchange several messages with your contacts - you will see a notice in the conversation when it happens.
You can still safely downgrade the app to an earlier version if needed, as v5.6 already supports quantum resistant encryption.
With the users who have an earlier version, the app will work using the conventional encryption, which is still very secure. It's important that we augmented the conventional encryption with post-quantum cryptographic algorithm rather than replaced it, using a hybrid construction as recommended by the cryptography experts.
The groups currently do not support quantum resistant encryption yet - we plan to add it in the future to small groups.
### Forward and save messages
You can now save received messages to private notes and forward them to your contacts and groups. This is both more convenient than copy-pasting the messages, and also more private - you can forward files and preserve a disappearing file in your private notes without saving them outside of the app.
You can see and navigate to the original source of the message via the message information, but the recipient of the message can only see that it was forwarded, but not from which conversation - in this way you can show that the message was quoted from another source without revealing the source ([Chatham House Rule](https://en.wikipedia.org/wiki/Chatham_House_rule)).
<img src="./images/20240426-forward1.png" width="288"> <img src="./images/20240426-forward2.png" width="288"> <img src="./images/20240426-forward3.png" width="288">
### In-call sounds and switching sound sources
This was the most frequent request of the users who use SimpleX Chat for audio and video calls - to add sound indication to the connection progress, as happens in all other apps. This release added these sounds and also vibration when call connects and disconnects.
You can also switch between bluetooth headphones, speakerphone and earpiece via the new button in the call.
### Network management
To reduce traffic and battery usage, this release made connection timeouts dependent on which network your device uses - timeouts will be larger when you are on mobile connection and smaller when on WiFi. It also makes connection attempts very infrequent when the device is not connected to network.
### Customizable shape of profile images
To customizable the interface, you can now change the shapes of profile images to anything from squares with sharp corners to circles, as before, via the Appearance menu in the app Settings. The default shape of profile images is changed to squares with rounded corners.
<img src="./images/20240426-profile1.png" width="288"> <img src="./images/20240426-profile2.png" width="288"> <img src="./images/20240426-profile3.png" width="288">
## SimpleX network
Some links to answer the most common questions:
[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers).
[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users).
[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations).
[Frequently asked questions](../docs/FAQ.md).
Please also see our [website](https://simplex.chat).
## Help us with donations
Huge thank you to everybody who donates to SimpleX Chat!
We are planning a 3rd party security audit for the protocols and cryptography design in July 2024, and also the security audit for an implementation in December 2024/January 2025, and it would hugely help us if some part of this $50,000+ expense is covered with donations.
We are prioritizing users privacy and security - it would be impossible without your support.
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX network based on the same principles as email and web, but much more private and secure.
Your donations help us raise more funds any amount, even the price of the cup of coffee, makes a big difference for us.
See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate.
Thank you,
Evgeny
SimpleX Chat founder
+15
View File
@@ -1,5 +1,20 @@
# Blog # Blog
Apr 26, 2024 [SimpleX network: legally binding transparency, v5.7 released with better calls and messages](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy.
We are committed to have full consistency between marketing promises and legally binding documents.
What's new in v5.7:
- quantum resistant end-to-end encryption with all contacts.
- forward and save messages without revealing the source.
- in-call sounds and switching sound sources.
- better network connection management.
- customizable profile images
---
Apr 16. 2024 [The dangers of metadata in messengers](./20240416-dangers-of-metadata-in-messengers.md) Apr 16. 2024 [The dangers of metadata in messengers](./20240416-dangers-of-metadata-in-messengers.md)
_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ _By [Esra'a al Shafei](https://mastodon.social/@alshafei)_
Binary file not shown.

After

Width:  |  Height:  |  Size: 769 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+4 -4
View File
@@ -1,12 +1,12 @@
--- ---
title: Transparency Reports title: Transparency Reports
permalink: /transparency/index.html permalink: /transparency/index.html
revision: 09.04.2024 revision: 26.04.2024
--- ---
# Transparency Reports # Transparency Reports
**Updated**: Apr 9, 2024 **Updated**: Apr 26, 2024
SimpleX Chat Ltd. is a company registered in the UK it develops communication software enabling users to operate and communicate via SimpleX network, without user profile identifiers of any kind, and without having their data hosted by any network infrastructure operators. SimpleX Chat Ltd. is a company registered in the UK it develops communication software enabling users to operate and communicate via SimpleX network, without user profile identifiers of any kind, and without having their data hosted by any network infrastructure operators.
@@ -17,8 +17,8 @@ This page will include any and all reports on requests for user data.
Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances. Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances.
**Helpful resources**: **Helpful resources**:
- [Privacy policy](https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md) - [Privacy policy](../PRIVACY.md)
- [Privacy and security: technical details and limitations](https://github.com/simplex-chat/simplex-chat?tab=readme-ov-file#privacy-and-security-technical-details-and-limitations) - [Privacy and security: technical details and limitations](../README.md#privacy-and-security-technical-details-and-limitations)
- Whitepaper: - Whitepaper:
- [Trust in servers](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#trust-in-servers) - [Trust in servers](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#trust-in-servers)
- [Encryption Primitives Used](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#encryption-primitives-used) - [Encryption Primitives Used](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#encryption-primitives-used)
+19 -19
View File
@@ -8,9 +8,9 @@ u="$USER"
tmp="$(mktemp -d -t)" tmp="$(mktemp -d -t)"
folder="$tmp/simplex-chat" folder="$tmp/simplex-chat"
nix_ver="nix-2.19.2" nix_ver="nix-2.22.0"
nix_url="https://releases.nixos.org/nix/$nix_ver/install" nix_url="https://releases.nixos.org/nix/$nix_ver/install"
nix_hash="435f0d7e11f7c7dffeeab0ec9cc55723f6d3c03352379d785633cf4ddb5caf90" nix_hash="4fed7db867186c01ce2a2077da4a6950ed16232efbf78d0cd19700cff80559f9"
nix_config="sandbox = true nix_config="sandbox = true
max-jobs = auto max-jobs = auto
experimental-features = nix-command flakes" experimental-features = nix-command flakes"
@@ -43,12 +43,13 @@ nix_setup() {
} }
git_setup() { git_setup() {
[ "$folder" != "." ] && { if [ "$folder" != "." ]; then
git clone "$repo" "$folder" git clone "$repo" "$folder"
} fi
# Switch to nix-android branch if [ -z ${git_skip+x} ]; then
git -C "$folder" checkout "$commit" git -C "$folder" checkout "$commit"
fi
} }
checks() { checks() {
@@ -100,14 +101,10 @@ build() {
sed -i.bak 's/${extract_native_libs}/true/' "$folder/apps/multiplatform/android/src/main/AndroidManifest.xml" sed -i.bak 's/${extract_native_libs}/true/' "$folder/apps/multiplatform/android/src/main/AndroidManifest.xml"
sed -i.bak 's/jniLibs.useLegacyPackaging =.*/jniLibs.useLegacyPackaging = true/' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak 's/jniLibs.useLegacyPackaging =.*/jniLibs.useLegacyPackaging = true/' "$folder/apps/multiplatform/android/build.gradle.kts"
sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts"
sed -i.bak '/tasks/Q' "$folder/apps/multiplatform/android/build.gradle.kts"
for arch in $arches; do for arch in $arches; do
if [ "$arch" = "armv7a" ]; then
tag_full="$(git tag --points-at HEAD | head -n1)"
tag_version="${tag_full%%-*}"
if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then
git checkout "${tag_version}-armv7a"
android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux" android_simplex_lib="${folder}#hydraJobs.${arch}-android:lib:simplex-chat.x86_64-linux"
android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux" android_support_lib="${folder}#hydraJobs.${arch}-android:lib:support.x86_64-linux"
else else
@@ -150,10 +147,6 @@ build() {
zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final" zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final"
rm -rf "$libs_folder/$android_arch" rm -rf "$libs_folder/$android_arch"
if [ "$arch" = "armv7a" ] && [ -n "$tag_full" ] ; then
git checkout "${tag_full}"
fi
done done
} }
@@ -161,15 +154,22 @@ final() {
printf 'Simplex-chat was successfully compiled: %s/simplex-chat-*.apk\nDelete nix and gradle caches with "rm -rf /nix && rm $HOME/.nix* && $HOME/.gradle/caches" in case if no longer needed.\n' "$PWD" printf 'Simplex-chat was successfully compiled: %s/simplex-chat-*.apk\nDelete nix and gradle caches with "rm -rf /nix && rm $HOME/.nix* && $HOME/.gradle/caches" in case if no longer needed.\n' "$PWD"
} }
main() { pre() {
while getopts ":s" opt; do while getopts ":sg" opt; do
case $opt in case $opt in
s) folder="." ;; s) folder="." ;;
g) git_skip=1 ;;
*) printf "Flag '-%s' doesn't exist.\n" "$OPTARG"; exit 1 ;; *) printf "Flag '-%s' doesn't exist.\n" "$OPTARG"; exit 1 ;;
esac esac
done done
shift $(( $OPTIND - 1 )) shift $(( $OPTIND - 1 ))
commit="$1"; shift 1
commit="${1:-HEAD}"
}
main() {
pre "$@"
git_setup git_setup
checks checks
build build
+194 -51
View File
@@ -46,6 +46,7 @@ import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
import qualified Data.Set as S
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Text.Encoding (decodeLatin1, encodeUtf8)
@@ -91,14 +92,18 @@ import qualified Simplex.FileTransfer.Description as FD
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap) import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap)
import qualified Simplex.Messaging.Agent.Client as AC
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Lock (withLock)
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..))
import qualified Simplex.Messaging.Agent.Store as AS
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection)
import qualified Simplex.Messaging.Agent.Store.SQLite as ADB
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import qualified Simplex.Messaging.Agent.TRcvQueues as RQ
import Simplex.Messaging.Client (defaultNetworkConfig) import Simplex.Messaging.Client (defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
@@ -227,6 +232,8 @@ newChatController
inputQ <- newTBQueueIO tbqSize inputQ <- newTBQueueIO tbqSize
outputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize
connNetworkStatuses <- atomically TM.empty connNetworkStatuses <- atomically TM.empty
agentDeliveryStatuses <- atomically TM.empty
agentSubscriptions <- newTVarIO S.empty
subscriptionMode <- newTVarIO SMSubscribe subscriptionMode <- newTVarIO SMSubscribe
chatLock <- newEmptyTMVarIO chatLock <- newEmptyTMVarIO
entityLocks <- atomically TM.empty entityLocks <- atomically TM.empty
@@ -263,6 +270,8 @@ newChatController
inputQ, inputQ,
outputQ, outputQ,
connNetworkStatuses, connNetworkStatuses,
agentDeliveryStatuses,
agentSubscriptions,
subscriptionMode, subscriptionMode,
chatLock, chatLock,
entityLocks, entityLocks,
@@ -402,6 +411,7 @@ startChatController mainApp = do
subscribeUsers :: Bool -> [User] -> CM' () subscribeUsers :: Bool -> [User] -> CM' ()
subscribeUsers onlyNeeded users = do subscribeUsers onlyNeeded users = do
let (us, us') = partition activeUser users let (us, us') = partition activeUser users
asks agentSubscriptions >>= atomically . (`writeTVar` S.empty)
vr <- chatVersionRange' vr <- chatVersionRange'
subscribe vr us subscribe vr us
subscribe vr us' subscribe vr us'
@@ -2147,6 +2157,67 @@ processChatCommand' vr = \case
chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn) chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn)
agentMigrations <- withAgent getAgentMigrations agentMigrations <- withAgent getAgentMigrations
pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations}
DebugDelivery showAll -> do
ads <- mapM readTVarIO =<< readTVarIO =<< asks agentDeliveryStatuses
let collect (acId, ds) = if not showAll && agentDeliveryOk ds then Nothing else Just (decodeLatin1 $ strEncode acId, ds)
pure $ CRDebugDelivery . M.fromList . mapMaybe collect $ M.toList ads
DebugConnection (Left connId) -> withUser $ \user -> do
Connection {agentConnId} <- withStore $ \db -> getConnectionById db vr user connId
processChatCommand $ DebugConnection (Right agentConnId)
DebugConnection (Right acId@(AgentConnId acId')) -> do
user@User {userId} <- withStore' (`getUserByAConnId` acId) >>= maybe (throwError . ChatErrorStore $ SEUserNotFoundByAConnId acId) pure
AS.RcvQueue {server, rcvId = rcvId', status = rqStatus} <- withAgent $ \ac ->
-- dive into agent internals
liftIOEither . (`runReaderT` agentEnv ac) . runExceptT $ AC.withStore ac $ \adb ->
ADB.getPrimaryRcvQueue adb acId'
let tSess = (userId, server, Just acId')
SMP.ProtocolServer {host, port} = server
AgentClient {activeSubs, pendingSubs, smpClients, smpSubWorkers} <- withAgent pure
inActive <- any (\AS.RcvQueue {rcvId} -> rcvId == rcvId') <$> atomically (RQ.getSessQueues tSess activeSubs)
inPending <- any (\AS.RcvQueue {rcvId} -> rcvId == rcvId') <$> atomically (RQ.getSessQueues tSess pendingSubs)
smpClient <- atomically (TM.lookup (tSess $> Nothing) smpClients) >>= mapM (\AC.SessionVar {sessionVar} -> atomically (tryReadTMVar sessionVar))
smpClientIsolated <- atomically (TM.lookup tSess smpClients) >>= mapM (\AC.SessionVar {sessionVar} -> atomically (tryReadTMVar sessionVar))
let smpClientStatus = case smpClient <|> smpClientIsolated of
Nothing -> "missing"
Just Nothing -> "connecting"
Just (Just (Left err)) -> tshow err
Just (Just (Right _client)) -> "connected"
subWorker <- atomically (TM.lookup (tSess $> Nothing) smpSubWorkers) >>= mapM (\AC.SessionVar {sessionVar} -> atomically (tryReadTMVar sessionVar))
subWorkerIsolated <- atomically (TM.lookup tSess smpSubWorkers) >>= mapM (\AC.SessionVar {sessionVar} -> atomically (tryReadTMVar sessionVar))
let subWorkerStatus = case subWorker <|> subWorkerIsolated of
Nothing -> "idle"
Just Nothing -> "waiting"
Just (Just _async) -> "working"
connection <-
withStore (\db -> getConnectionEntity db vr user acId) >>= \case
RcvDirectMsgConnection {entityConnection} -> pure entityConnection
RcvGroupMsgConnection {entityConnection} -> pure entityConnection
SndFileConnection {entityConnection} -> pure entityConnection
RcvFileConnection {entityConnection} -> pure entityConnection
UserContactConnection {entityConnection} -> pure entityConnection
deliveryStatus <- mapM readTVarIO . M.lookup acId =<< readTVarIO =<< asks agentDeliveryStatuses
networkStatus <- M.lookup acId <$> chatReadVar connNetworkStatuses
pure $
CRDebugConnection
DebugConnectionStatus
{ deliveryStatus,
networkStatus,
inActive,
inPending,
server = (decodeLatin1 $ strEncode host, port),
smpClientStatus,
subWorkerStatus,
queueStatus = tshow rqStatus,
connection
}
DebugSubs -> do
ads <- mapM readTVarIO =<< readTVarIO =<< asks agentDeliveryStatuses
conns <- readTVarIO =<< asks agentSubscriptions
pure . CRDebugSubs . S.toList $ foldl' (\cs (acId, ds) -> if agentDeliveryOk ds then S.delete acId cs else cs) conns (M.toList ads)
DebugSubsDetails -> do
CRDebugSubs broken <- processChatCommand' vr DebugSubs
crs <- mapM (processChatCommand' vr . DebugConnection . Right) broken
pure $ CRDebugSubsDetails [dcs | CRDebugConnection dcs <- crs]
DebugLocks -> lift $ do DebugLocks -> lift $ do
chatLockName <- atomically . tryReadTMVar =<< asks chatLock chatLockName <- atomically . tryReadTMVar =<< asks chatLock
chatEntityLocks <- getLocks =<< asks entityLocks chatEntityLocks <- getLocks =<< asks entityLocks
@@ -2473,8 +2544,6 @@ processChatCommand' vr = \case
toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
forM_ (timed_ >>= timedDeleteAt') $ forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci)
drgRandomBytes :: Int -> CM ByteString
drgRandomBytes n = asks random >>= atomically . C.randomBytes n
privateGetUser :: UserId -> CM User privateGetUser :: UserId -> CM User
privateGetUser userId = privateGetUser userId =
tryChatError (withStore (`getUser` userId)) >>= \case tryChatError (withStore (`getUser` userId)) >>= \case
@@ -2932,7 +3001,7 @@ callTimed ct aciContent =
case aciContentCallStatus aciContent of case aciContentCallStatus aciContent of
Just callStatus Just callStatus
| callComplete callStatus -> do | callComplete callStatus -> do
contactCITimed ct contactCITimed ct
_ -> pure Nothing _ -> pure Nothing
where where
aciContentCallStatus :: ACIContent -> Maybe CICallStatus aciContentCallStatus :: ACIContent -> Maybe CICallStatus
@@ -3244,6 +3313,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns] let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns]
pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs) pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs)
-- subscribe using batched commands -- subscribe using batched commands
asks agentSubscriptions >>= \v -> atomically . modifyTVar' v $ \cs -> foldl' (\cs' acId -> S.insert (AgentConnId acId) cs') cs conns
rs <- withAgent $ \a -> agentBatchSubscribe a conns rs <- withAgent $ \a -> agentBatchSubscribe a conns
-- send connection events to view -- send connection events to view
contactSubsToView rs cts ce contactSubsToView rs cts ce
@@ -3533,13 +3603,29 @@ processAgentMessage _ connId (DEL_RCVQ srv qId err_) =
processAgentMessage _ connId DEL_CONN = processAgentMessage _ connId DEL_CONN =
toView $ CRAgentConnDeleted (AgentConnId connId) toView $ CRAgentConnDeleted (AgentConnId connId)
processAgentMessage corrId connId msg = do processAgentMessage corrId connId msg = do
lockEntity <- critical (withStore (`getChatLockEntity` AgentConnId connId)) let acId = AgentConnId connId
acTag = aCommandTag msg
when (acTag == MSG_ || acTag == RCVD_) . lift $ trackNewDelivery acId msg
lockEntity <- critical (withStore (`getChatLockEntity` acId))
withEntityLock "processAgentMessage" lockEntity $ do withEntityLock "processAgentMessage" lockEntity $ do
vr <- chatVersionRange vr <- chatVersionRange
-- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here
critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case critical (withStore' (`getUserByAConnId` acId)) >>= \case
Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user)) Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user))
_ -> throwChatError $ CENoConnectionUser (AgentConnId connId) _ -> throwChatError $ CENoConnectionUser acId
-- TODO: clean up deliveries
trackNewDelivery :: AgentConnId -> ACommand 'Agent 'AEConn -> CM' ()
trackNewDelivery acId msg = do
now <- liftIO getCurrentTime
asks agentDeliveryStatuses >>= atomically . TM.alterF (updateConn now) acId
where
(isMSG, msgBodyPfx) = case msg of
MSG _ _ msgBody -> (True, T.take 1000 $ safeDecodeUtf8 msgBody)
_ -> (False, "")
updateConn lastCmd = \case
Nothing -> Just <$> newTVar AgentDeliveryStatus {lastCmd, tracking = "create", isMSG, connId = Nothing, msgBodyPfx, eventTag = Nothing, ackSent = Nothing, pendingAcks = M.empty}
Just v -> Just v <$ modifyTVar' v (\AgentDeliveryStatus {pendingAcks} -> AgentDeliveryStatus {lastCmd, tracking = "create", isMSG, connId = Nothing, msgBodyPfx, eventTag = Nothing, ackSent = Nothing, pendingAcks = M.filter not pendingAcks})
-- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. -- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps.
-- SEDBBusyError will only be thrown on IO exceptions or SQLError during DB queries, -- SEDBBusyError will only be thrown on IO exceptions or SQLError during DB queries,
@@ -3577,14 +3663,19 @@ processAgentMessageNoConn = \case
processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM () processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM ()
processAgentMsgSndFile _corrId aFileId msg = do processAgentMsgSndFile _corrId aFileId msg = do
fileId <- withStore (`getXFTPSndFileDBId` AgentSndFileId aFileId) (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId)
withFileLock "processAgentMsgSndFile" fileId $ withEntityLock_ cRef_ $ withFileLock "processAgentMsgSndFile" fileId $
withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case
Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user))
_ -> do _ -> do
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId
where where
withEntityLock_ :: Maybe ChatRef -> CM a -> CM a
withEntityLock_ cRef_ = case cRef_ of
Just (ChatRef CTDirect contactId) -> withContactLock "processAgentMsgSndFile" contactId
Just (ChatRef CTGroup groupId) -> withGroupLock "processAgentMsgSndFile" groupId
_ -> id
process :: User -> FileTransferId -> CM () process :: User -> FileTransferId -> CM ()
process user fileId = do process user fileId = do
(ft@FileTransferMeta {xftpRedirectFor, cancelled}, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId (ft@FileTransferMeta {xftpRedirectFor, cancelled}, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId
@@ -3699,14 +3790,19 @@ splitFileDescr rfdText = do
processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM () processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM ()
processAgentMsgRcvFile _corrId aFileId msg = do processAgentMsgRcvFile _corrId aFileId msg = do
fileId <- withStore (`getXFTPRcvFileDBId` AgentRcvFileId aFileId) (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId)
withFileLock "processAgentMsgRcvFile" fileId $ withEntityLock_ cRef_ $ withFileLock "processAgentMsgRcvFile" fileId $
withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case
Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user))
_ -> do _ -> do
lift $ withAgent' (`xftpDeleteRcvFile` aFileId) lift $ withAgent' (`xftpDeleteRcvFile` aFileId)
throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId
where where
withEntityLock_ :: Maybe ChatRef -> CM a -> CM a
withEntityLock_ cRef_ = case cRef_ of
Just (ChatRef CTDirect contactId) -> withContactLock "processAgentMsgRcvFile" contactId
Just (ChatRef CTGroup groupId) -> withGroupLock "processAgentMsgRcvFile" groupId
_ -> id
process :: User -> FileTransferId -> CM () process :: User -> FileTransferId -> CM ()
process user fileId = do process user fileId = do
ft <- withStore $ \db -> getRcvFileTransfer db user fileId ft <- withStore $ \db -> getRcvFileTransfer db user fileId
@@ -3754,15 +3850,20 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> toView $ CRSubscriptionEnd user entity _ -> toView $ CRSubscriptionEnd user entity
MSGNTF smpMsgInfo -> toView $ CRNtfMessage user entity $ ntfMsgInfo smpMsgInfo MSGNTF smpMsgInfo -> toView $ CRNtfMessage user entity $ ntfMsgInfo smpMsgInfo
_ -> case entity of _ -> case entity of
RcvDirectMsgConnection conn contact_ -> RcvDirectMsgConnection conn contact_ -> do
storeDeliveryConn conn
processDirectMessage agentMessage entity conn contact_ processDirectMessage agentMessage entity conn contact_
RcvGroupMsgConnection conn gInfo m -> RcvGroupMsgConnection conn gInfo m -> do
storeDeliveryConn conn
processGroupMessage agentMessage entity conn gInfo m processGroupMessage agentMessage entity conn gInfo m
RcvFileConnection conn ft -> RcvFileConnection conn ft -> do
storeDeliveryConn conn
processRcvFileConn agentMessage entity conn ft processRcvFileConn agentMessage entity conn ft
SndFileConnection conn ft -> SndFileConnection conn ft -> do
storeDeliveryConn conn
processSndFileConn agentMessage entity conn ft processSndFileConn agentMessage entity conn ft
UserContactConnection conn uc -> UserContactConnection conn uc -> do
storeDeliveryConn conn
processUserContactRequest agentMessage entity conn uc processUserContactRequest agentMessage entity conn uc
where where
updateConnStatus :: ConnectionEntity -> CM ConnectionEntity updateConnStatus :: ConnectionEntity -> CM ConnectionEntity
@@ -3772,7 +3873,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
withStore' $ \db -> updateConnectionStatus db conn connStatus withStore' $ \db -> updateConnectionStatus db conn connStatus
pure $ updateEntityConnStatus acEntity connStatus pure $ updateEntityConnStatus acEntity connStatus
Nothing -> pure acEntity Nothing -> pure acEntity
storeDeliveryConn :: Connection -> CM ()
storeDeliveryConn Connection {connId} = lift $ agentDeliveryStatus (AgentConnId agentConnId) "storeDeliveryConn" $ \ad -> ad {connId = Just connId}
agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus
agentMsgConnStatus = \case agentMsgConnStatus = \case
CONF {} -> Just ConnRequested CONF {} -> Just ConnRequested
@@ -3851,6 +3953,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
(ct', conn') <- updateContactPQRcv user ct conn pqEncryption (ct', conn') <- updateContactPQRcv user ct conn pqEncryption
checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure () checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure ()
(conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody
lift $ agentDeliveryStatus (AgentConnId agentConnId) "eventTag" $ \ad -> ad {eventTag = Just $! tshow (toCMEventTag event)}
let ct'' = ct' {activeConn = Just conn''} :: Contact let ct'' = ct' {activeConn = Just conn''} :: Contact
assertDirectAllowed user MDRcv ct'' $ toCMEventTag event assertDirectAllowed user MDRcv ct'' $ toCMEventTag event
case event of case event of
@@ -4254,23 +4357,27 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected" _ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
MSG msgMeta _msgFlags msgBody -> do MSG msgMeta _msgFlags msgBody -> do
withAckMessage agentConnId msgMeta True $ do withAckMessage agentConnId msgMeta True $ do
lift $ agentDeliveryStatus (AgentConnId agentConnId) "group MSG" id
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
lift $ agentDeliveryStatus (AgentConnId agentConnId) "after checkIntegrityCreateItem" id
forM_ aChatMsgs $ \case forM_ aChatMsgs $ \case
Right (ACMsg _ chatMsg) -> Right (ACMsg _ chatMsg) -> do
lift $ agentDeliveryStatus (AgentConnId agentConnId) "has event" id
processEvent chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e processEvent chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e
Left e -> toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) Left e -> do
lift $ agentDeliveryStatus (AgentConnId agentConnId) "has error" id
toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e)
lift $ agentDeliveryStatus (AgentConnId agentConnId) "after forM_" id
forwardMsg_ `catchChatError` \_ -> pure ()
checkSendRcpt $ rights aChatMsgs checkSendRcpt $ rights aChatMsgs
-- currently only a single message is forwarded
let GroupMember {memberRole = membershipMemRole} = membership
when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ case aChatMsgs of
[Right (ACMsg _ chatMsg)] -> forwardMsg_ chatMsg
_ -> pure ()
where where
aChatMsgs = parseChatMessages msgBody aChatMsgs = parseChatMessages msgBody
brokerTs = metaBrokerTs msgMeta brokerTs = metaBrokerTs msgMeta
processEvent :: MsgEncodingI e => ChatMessage e -> CM () processEvent :: MsgEncodingI e => ChatMessage e -> CM ()
processEvent chatMsg = do processEvent chatMsg = do
lift $ agentDeliveryStatus (AgentConnId agentConnId) "processEvent" id
(m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg (m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg
lift $ agentDeliveryStatus (AgentConnId agentConnId) "eventTag in processEvent" $ \ad -> ad {eventTag = Just $! tshow (toCMEventTag event)}
case event of case event of
XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False
XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr
@@ -4312,22 +4419,27 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
where where
aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) =
hasDeliveryReceipt (toCMEventTag chatMsgEvent) hasDeliveryReceipt (toCMEventTag chatMsgEvent)
forwardMsg_ :: MsgEncodingI e => ChatMessage e -> CM () forwardMsg_ :: CM ()
forwardMsg_ chatMsg = forwardMsg_ = do
forM_ (forwardedGroupMsg chatMsg) $ \chatMsg' -> do let GroupMember {memberRole = membershipMemRole} = membership
ChatConfig {highlyAvailable} <- asks config when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ case aChatMsgs of
-- members introduced to this invited member -- currently only a single message is forwarded
introducedMembers <- [Right (ACMsg _ chatMsg)] ->
if memberCategory m == GCInviteeMember forM_ (forwardedGroupMsg chatMsg) $ \chatMsg' -> do
then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable ChatConfig {highlyAvailable} <- asks config
else pure [] -- members introduced to this invited member
-- invited members to which this member was introduced introducedMembers <-
invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable if memberCategory m == GCInviteeMember
let GroupMember {memberId} = m then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable
ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) chatMsg' else pure []
msg = XGrpMsgForward memberId chatMsg' brokerTs -- invited members to which this member was introduced
unless (null ms) . void $ invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable
sendGroupMessage' user gInfo ms msg let GroupMember {memberId} = m
ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) chatMsg'
msg = XGrpMsgForward memberId chatMsg' brokerTs
unless (null ms) . void $
sendGroupMessage' user gInfo ms msg
_ -> pure ()
RCVD msgMeta msgRcpt -> RCVD msgMeta msgRcpt ->
withAckMessage' agentConnId msgMeta $ withAckMessage' agentConnId msgMeta $
groupMsgReceived gInfo m conn msgMeta msgRcpt groupMsgReceived gInfo m conn msgMeta msgRcpt
@@ -4609,16 +4721,22 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM () withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM ()
withCompletedCommand Connection {connId} agentMsg action = do withCompletedCommand Connection {connId} agentMsg action = do
let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg
cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId pending_ <- mapM readTVarIO =<< atomically . TM.lookup acId =<< asks agentDeliveryStatuses
case cmdData_ of if agentMsgTag == APCT SAEConn OK_ && corrId /= "" && maybe False (M.member ackKey . pendingAcks) pending_
Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction} then lift $ agentDeliveryStatus acId "withCompletedCommand" $ \ad@AgentDeliveryStatus {pendingAcks} -> ad {pendingAcks = M.adjust (const True) ackKey pendingAcks}
| connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do else do
withStore' $ \db -> deleteCommand db user cmdId cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId
action cmdData case cmdData_ of
| otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction}
Just CommandData {cmdId, cmdConnId = Nothing} -> err cmdId $ "no command connection id, corrId = " <> show corrId | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do
Nothing -> throwChatError . CEAgentCommandError $ "command not found, corrId = " <> show corrId withStore' $ \db -> deleteCommand db user cmdId
action cmdData
| otherwise -> logWarn $ "not matching connection id or unexpected response, corrId = " <> tshow corrId
Just CommandData {cmdId, cmdConnId = Nothing} -> logWarn $ "no command connection id, corrId = " <> tshow corrId
Nothing -> logWarn $ "command not found, corrId = " <> tshow corrId
where where
acId = AgentConnId agentConnId
ackKey = decodeLatin1 $ strEncode corrId
err cmdId msg = do err cmdId msg = do
withStore' $ \db -> updateCommandStatus db user cmdId CSError withStore' $ \db -> updateCommandStatus db user cmdId CSError
throwChatError . CEAgentCommandError $ msg throwChatError . CEAgentCommandError $ msg
@@ -4639,11 +4757,20 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
Right withRcpt -> ackMsg msgMeta $ if withRcpt then Just "" else Nothing Right withRcpt -> 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 -- If showCritical is True, then these errors don't result in ACK and show user visible alert
-- This prevents losing the message that failed to be processed. -- This prevents losing the message that failed to be processed.
Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> do
lift $ agentDeliveryStatus (AgentConnId agentConnId) "SEDBBusyError" id
throwError $ ChatErrorAgent (CRITICAL True message) Nothing
Left e -> ackMsg msgMeta Nothing >> throwError e Left e -> ackMsg msgMeta Nothing >> throwError e
where where
ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM () ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM ()
ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt ackMsg MsgMeta {recipient = (msgId, _)} rcpt = do
ackCorrId <- drgRandomBytes 24
lift $ agentDeliveryStatus (AgentConnId agentConnId) "ackMsg" id
withAgent $ \a -> ackMessageAsync a ackCorrId cId msgId rcpt
now <- liftIO getCurrentTime
let ackKey = decodeLatin1 $ strEncode ackCorrId
lift . agentDeliveryStatus (AgentConnId agentConnId) "after ackMessageAsync" $ \ad@AgentDeliveryStatus {pendingAcks} ->
ad {ackSent = Just (now, ackKey), pendingAcks = M.insert ackKey False pendingAcks}
sentMsgDeliveryEvent :: Connection -> AgentMsgId -> CM () sentMsgDeliveryEvent :: Connection -> AgentMsgId -> CM ()
sentMsgDeliveryEvent Connection {connId} msgId = sentMsgDeliveryEvent Connection {connId} msgId =
@@ -5942,6 +6069,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
processForwardedMsg author chatMsg = do processForwardedMsg author chatMsg = do
let body = LB.toStrict $ J.encode msg let body = LB.toStrict $ J.encode msg
rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg
lift $ agentDeliveryStatus (AgentConnId agentConnId) "eventTag in processForwardedMsg" $ \ad -> ad {eventTag = Just $! tshow (toCMEventTag event)}
case event of case event of
XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True
XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr
@@ -6566,9 +6694,10 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn
-- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing) -- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing)
saveDirectRcvMSG :: Connection -> MsgMeta -> MsgBody -> CM (Connection, RcvMessage) saveDirectRcvMSG :: Connection -> MsgMeta -> MsgBody -> CM (Connection, RcvMessage)
saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody = saveDirectRcvMSG conn@Connection {connId, agentConnId} agentMsgMeta msgBody =
case parseChatMessages msgBody of case parseChatMessages msgBody of
[Right (ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent})] -> do [Right (ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent})] -> do
lift $ agentDeliveryStatus agentConnId "eventTag in saveDirectRcvMSG" $ \ad -> ad {eventTag = Just $! tshow (toCMEventTag chatMsgEvent)}
conn' <- updatePeerChatVRange conn chatVRange conn' <- updatePeerChatVRange conn chatVRange
let agentMsgId = fst $ recipient agentMsgMeta let agentMsgId = fst $ recipient agentMsgMeta
newMsg = NewRcvMessage {chatMsgEvent, msgBody} newMsg = NewRcvMessage {chatMsgEvent, msgBody}
@@ -6733,6 +6862,7 @@ deleteAgentConnectionAsync user acId = deleteAgentConnectionAsync' user acId Fal
deleteAgentConnectionAsync' :: User -> ConnId -> Bool -> CM () deleteAgentConnectionAsync' :: User -> ConnId -> Bool -> CM ()
deleteAgentConnectionAsync' user acId waitDelivery = do deleteAgentConnectionAsync' user acId waitDelivery = do
withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` (toView . CRChatError (Just user)) withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` (toView . CRChatError (Just user))
asks agentSubscriptions >>= \v -> atomically $ modifyTVar v $ S.delete (AgentConnId acId)
deleteAgentConnectionsAsync :: User -> [ConnId] -> CM () deleteAgentConnectionsAsync :: User -> [ConnId] -> CM ()
deleteAgentConnectionsAsync user acIds = deleteAgentConnectionsAsync' user acIds False deleteAgentConnectionsAsync user acIds = deleteAgentConnectionsAsync' user acIds False
@@ -6741,6 +6871,7 @@ deleteAgentConnectionsAsync' :: User -> [ConnId] -> Bool -> CM ()
deleteAgentConnectionsAsync' _ [] _ = pure () deleteAgentConnectionsAsync' _ [] _ = pure ()
deleteAgentConnectionsAsync' user acIds waitDelivery = do deleteAgentConnectionsAsync' user acIds waitDelivery = do
withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` (toView . CRChatError (Just user)) withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` (toView . CRChatError (Just user))
asks agentSubscriptions >>= \v -> atomically $ modifyTVar v $ \as -> foldl' (\as' acId -> S.delete (AgentConnId acId) as') as acIds
agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM () agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM ()
agentXFTPDeleteRcvFile aFileId fileId = do agentXFTPDeleteRcvFile aFileId fileId = do
@@ -7271,6 +7402,10 @@ chatCommandP =
"/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP), "/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP),
("/quit" <|> "/q" <|> "/exit") $> QuitChat, ("/quit" <|> "/q" <|> "/exit") $> QuitChat,
("/version" <|> "/v") $> ShowVersion, ("/version" <|> "/v") $> ShowVersion,
"/debug delivery" *> (DebugDelivery <$> (" all" $> True <|> pure False)),
"/debug conn " *> (DebugConnection <$> ((Left <$> A.decimal) <|> (Right . AgentConnId <$> base64P))),
"/debug subs" $> DebugSubs,
"/debug subs details" $> DebugSubsDetails,
"/debug locks" $> DebugLocks, "/debug locks" $> DebugLocks,
"/debug event " *> (DebugEvent <$> jsonP), "/debug event " *> (DebugEvent <$> jsonP),
"/get stats" $> GetAgentStats, "/get stats" $> GetAgentStats,
@@ -7432,6 +7567,9 @@ timeItToView s action = do
toView' $ CRTimedAction s diff toView' $ CRTimedAction s diff
pure a pure a
drgRandomBytes :: Int -> CM ByteString
drgRandomBytes n = asks random >>= atomically . C.randomBytes n
mkValidName :: String -> String mkValidName :: String -> String
mkValidName = reverse . dropWhile isSpace . fst3 . foldl' addChar ("", '\NUL', 0 :: Int) mkValidName = reverse . dropWhile isSpace . fst3 . foldl' addChar ("", '\NUL', 0 :: Int)
where where
@@ -7476,3 +7614,8 @@ xftpSndFileRedirect user ftId vfd = do
dummyFileDescr :: FileDescr dummyFileDescr :: FileDescr
dummyFileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} dummyFileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
agentDeliveryStatus :: AgentConnId -> Text -> (AgentDeliveryStatus -> AgentDeliveryStatus) -> CM' ()
agentDeliveryStatus acId tracking f = do
ads <- asks agentDeliveryStatuses
atomically $ TM.lookup acId ads >>= mapM_ (`modifyTVar'` \ds -> (f ds) {tracking})
+47
View File
@@ -39,6 +39,7 @@ import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Set (Set)
import Data.String import Data.String
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1) import Data.Text.Encoding (decodeLatin1)
@@ -207,6 +208,8 @@ data ChatController = ChatController
inputQ :: TBQueue String, inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse), outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse),
connNetworkStatuses :: TMap AgentConnId NetworkStatus, connNetworkStatuses :: TMap AgentConnId NetworkStatus,
agentDeliveryStatuses :: TMap AgentConnId (TVar AgentDeliveryStatus),
agentSubscriptions :: TVar (Set AgentConnId),
subscriptionMode :: TVar SubscriptionMode, subscriptionMode :: TVar SubscriptionMode,
chatLock :: Lock, chatLock :: Lock,
entityLocks :: TMap ChatLockEntity Lock, entityLocks :: TMap ChatLockEntity Lock,
@@ -233,6 +236,23 @@ data ChatController = ChatController
contactMergeEnabled :: TVar Bool contactMergeEnabled :: TVar Bool
} }
data AgentDeliveryStatus = AgentDeliveryStatus
{ lastCmd :: UTCTime,
tracking :: Text,
isMSG :: Bool, -- False for RCVD
connId :: Maybe Int64, -- chat connection ID
msgBodyPfx :: Text,
eventTag :: Maybe Text, -- tshow of ACMEventTag (for JSON instances)
ackSent :: Maybe (UTCTime, Text), -- strEncode of random CorrId
pendingAcks :: Map Text Bool
}
deriving (Show) -- for ChatResponse
agentDeliveryOk :: AgentDeliveryStatus -> Bool
agentDeliveryOk AgentDeliveryStatus {ackSent, pendingAcks} = case ackSent of
Nothing -> False
Just (_, corrId) -> M.lookup corrId pendingAcks == Just True && and pendingAcks
data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSRemote | HSSettings | HSDatabase data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSRemote | HSSettings | HSDatabase
deriving (Show) deriving (Show)
@@ -488,6 +508,10 @@ data ChatCommand
| APIStandaloneFileInfo FileDescriptionURI | APIStandaloneFileInfo FileDescriptionURI
| QuitChat | QuitChat
| ShowVersion | ShowVersion
| DebugDelivery Bool
| DebugConnection (Either Int64 AgentConnId)
| DebugSubs -- Check that all `subscribeUserConnections` are in place and healthy
| DebugSubsDetails -- Run DebugConnection on each unhealthy sub
| DebugLocks | DebugLocks
| DebugEvent ChatResponse | DebugEvent ChatResponse
| GetAgentStats | GetAgentStats
@@ -735,6 +759,10 @@ data ChatResponse
| CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption} | CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption}
| CRSQLResult {rows :: [Text]} | CRSQLResult {rows :: [Text]}
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
| CRDebugDelivery {debugDelivery :: Map Text AgentDeliveryStatus}
| CRDebugConnection {debugConnection :: DebugConnectionStatus}
| CRDebugSubs {brokenSubs :: [AgentConnId]}
| CRDebugSubsDetails {debugConnections :: [DebugConnectionStatus]}
| CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks}
| CRAgentStats {agentStats :: [[String]]} | CRAgentStats {agentStats :: [[String]]}
| CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails}
@@ -755,6 +783,21 @@ data ChatResponse
| CRCustomChatResponse {user_ :: Maybe User, response :: Text} | CRCustomChatResponse {user_ :: Maybe User, response :: Text}
deriving (Show) deriving (Show)
data DebugConnectionStatus = DebugConnectionStatus
{ deliveryStatus :: Maybe AgentDeliveryStatus,
networkStatus :: Maybe NetworkStatus,
-- from agent's TRecvQ via rcvId
inActive :: Bool, -- should the delivery work right now?
inPending :: Bool, -- is there a temporary error?
-- from receive queue
queueStatus :: Text,
server :: (Text, String), -- what's the server for this connection? -- XXX: reveals private servers and association
smpClientStatus :: Text, -- is there an active client for it?
subWorkerStatus :: Text, -- a session was recently restarted and tries to resubscribe
connection :: Connection
}
deriving (Show)
-- some of these can only be used as command responses -- some of these can only be used as command responses
allowRemoteEvent :: ChatResponse -> Bool allowRemoteEvent :: ChatResponse -> Bool
allowRemoteEvent = \case allowRemoteEvent = \case
@@ -1456,6 +1499,10 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCSR") ''RemoteCtrlStopReason)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHSR") ''RemoteHostStopReason) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHSR") ''RemoteHostStopReason)
$(JQ.deriveJSON J.defaultOptions ''AgentDeliveryStatus)
$(JQ.deriveJSON J.defaultOptions ''DebugConnectionStatus)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse)
$(JQ.deriveFromJSON defaultJSON ''ArchiveConfig) $(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
+33 -11
View File
@@ -29,8 +29,8 @@ module Simplex.Chat.Store.Files
createExtraSndFTDescrs, createExtraSndFTDescrs,
updateSndFTDeliveryXFTP, updateSndFTDeliveryXFTP,
setSndFTAgentDeleted, setSndFTAgentDeleted,
getXFTPSndFileDBId, getXFTPSndFileDBIds,
getXFTPRcvFileDBId, getXFTPRcvFileDBIds,
updateFileCancelled, updateFileCancelled,
updateCIFileStatus, updateCIFileStatus,
getSharedMsgIdByFileId, getSharedMsgIdByFileId,
@@ -109,7 +109,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (week) import Simplex.Chat.Util (week)
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId) import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
@@ -336,15 +336,37 @@ setSndFTAgentDeleted db User {userId} fileId = do
"UPDATE files SET agent_snd_file_deleted = 1, updated_at = ? WHERE user_id = ? AND file_id = ?" "UPDATE files SET agent_snd_file_deleted = 1, updated_at = ? WHERE user_id = ? AND file_id = ?"
(currentTs, userId, fileId) (currentTs, userId, fileId)
getXFTPSndFileDBId :: DB.Connection -> AgentSndFileId -> ExceptT StoreError IO FileTransferId getXFTPSndFileDBIds :: DB.Connection -> AgentSndFileId -> ExceptT StoreError IO (Maybe ChatRef, FileTransferId)
getXFTPSndFileDBId db aSndFileId = getXFTPSndFileDBIds db aSndFileId =
ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $ ExceptT . firstRow' toFileRef (SESndFileNotFoundXFTP aSndFileId) $
DB.query db "SELECT file_id FROM files WHERE agent_snd_file_id = ?" (Only aSndFileId) DB.query
db
[sql|
SELECT file_id, contact_id, group_id, note_folder_id
FROM files
WHERE agent_snd_file_id = ?
|]
(Only aSndFileId)
getXFTPRcvFileDBId :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO FileTransferId getXFTPRcvFileDBIds :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO (Maybe ChatRef, FileTransferId)
getXFTPRcvFileDBId db aRcvFileId = getXFTPRcvFileDBIds db aRcvFileId =
ExceptT . firstRow fromOnly (SERcvFileNotFoundXFTP aRcvFileId) $ ExceptT . firstRow' toFileRef (SERcvFileNotFoundXFTP aRcvFileId) $
DB.query db "SELECT file_id FROM rcv_files WHERE agent_rcv_file_id = ?" (Only aRcvFileId) DB.query
db
[sql|
SELECT rf.file_id, f.contact_id, f.group_id, f.note_folder_id
FROM rcv_files rf
JOIN files f ON f.file_id = rf.file_id
WHERE rf.agent_rcv_file_id = ?
|]
(Only aRcvFileId)
toFileRef :: (FileTransferId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (Maybe ChatRef, FileTransferId)
toFileRef = \case
(fileId, Just contactId, Nothing, Nothing) -> Right (Just $ ChatRef CTDirect contactId, fileId)
(fileId, Nothing, Just groupId, Nothing) -> Right (Just $ ChatRef CTGroup groupId, fileId)
(fileId, Nothing, Nothing, Just folderId) -> Right (Just $ ChatRef CTLocal folderId, fileId)
(fileId, _, _, _) -> Right (Nothing, fileId)
updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO () updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO ()
updateFileCancelled db User {userId} fileId ciFileStatus = do updateFileCancelled db User {userId} fileId ciFileStatus = do
-1
View File
@@ -146,7 +146,6 @@ import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (PQSupport)
import Simplex.Messaging.Util (eitherToMaybe) import Simplex.Messaging.Util (eitherToMaybe)
import UnliftIO.STM import UnliftIO.STM
+1
View File
@@ -59,6 +59,7 @@ data StoreError
= SEDuplicateName = SEDuplicateName
| SEUserNotFound {userId :: UserId} | SEUserNotFound {userId :: UserId}
| SEUserNotFoundByName {contactName :: ContactName} | SEUserNotFoundByName {contactName :: ContactName}
| SEUserNotFoundByAConnId {agentConnId :: AgentConnId}
| SEUserNotFoundByContactId {contactId :: ContactId} | SEUserNotFoundByContactId {contactId :: ContactId}
| SEUserNotFoundByGroupId {groupId :: GroupId} | SEUserNotFoundByGroupId {groupId :: GroupId}
| SEUserNotFoundByFileId {fileId :: FileTransferId} | SEUserNotFoundByFileId {fileId :: FileTransferId}
+4
View File
@@ -351,6 +351,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
<> (" :: avg: " <> sShow timeAvg <> " ms") <> (" :: avg: " <> sShow timeAvg <> " ms")
<> (" :: " <> plain (T.unwords $ T.lines query)) <> (" :: " <> plain (T.unwords $ T.lines query))
in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries)
CRDebugDelivery ads -> [plain $ LB.unpack (J.encode ads)]
CRDebugConnection cs -> [plain $ LB.unpack (J.encode cs)]
CRDebugSubs cs -> [plain $ LB.unpack (J.encode cs)]
CRDebugSubsDetails cs -> [plain $ LB.unpack (J.encode cs)]
CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} -> CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} ->
[ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName,
plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks), plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks),
+2
View File
@@ -1069,6 +1069,7 @@ testMaintenanceMode tmp = do
bob <# "alice> hi again" bob <# "alice> hi again"
bob #> "@alice hello" bob #> "@alice hello"
alice <# "bob> hello" alice <# "bob> hello"
threadDelay 100000
-- export / delete / import -- export / delete / import
alice ##> "/_stop" alice ##> "/_stop"
alice <## "chat stopped" alice <## "chat stopped"
@@ -1149,6 +1150,7 @@ testDatabaseEncryption tmp = do
alice <## "error: chat not stopped" alice <## "error: chat not stopped"
alice ##> "/db decrypt mykey" alice ##> "/db decrypt mykey"
alice <## "error: chat not stopped" alice <## "error: chat not stopped"
threadDelay 100000
alice ##> "/_stop" alice ##> "/_stop"
alice <## "chat stopped" alice <## "chat stopped"
alice ##> "/db decrypt mykey" alice ##> "/db decrypt mykey"
+13
View File
@@ -120,6 +120,7 @@ chatGroupTests = do
it "forward file (x.msg.file.descr)" testGroupMsgForwardFile it "forward file (x.msg.file.descr)" testGroupMsgForwardFile
it "forward role change (x.grp.mem.role)" testGroupMsgForwardChangeRole it "forward role change (x.grp.mem.role)" testGroupMsgForwardChangeRole
it "forward new member announcement (x.grp.mem.new)" testGroupMsgForwardNewMember it "forward new member announcement (x.grp.mem.new)" testGroupMsgForwardNewMember
it "forward member leaving (x.grp.leave)" testGroupMsgForwardLeave
describe "group history" $ do describe "group history" $ do
it "text messages" testGroupHistory it "text messages" testGroupHistory
it "history is sent when joining via group link" testGroupHistoryGroupLink it "history is sent when joining via group link" testGroupHistoryGroupLink
@@ -4437,6 +4438,18 @@ testGroupMsgForwardNewMember =
"dan (Daniel): member" "dan (Daniel): member"
] ]
testGroupMsgForwardLeave :: HasCallStack => FilePath -> IO ()
testGroupMsgForwardLeave =
testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
setupGroupForwarding3 "team" alice bob cath
bob ##> "/leave #team"
bob <## "#team: you left the group"
bob <## "use /d #team to delete the group"
alice <## "#team: bob left the group"
cath <## "#team: bob left the group"
testGroupHistory :: HasCallStack => FilePath -> IO () testGroupHistory :: HasCallStack => FilePath -> IO ()
testGroupHistory = testGroupHistory =
testChat3 aliceProfile bobProfile cathProfile $ testChat3 aliceProfile bobProfile cathProfile $
+3
View File
@@ -394,6 +394,9 @@ module.exports = function (ty) {
if (parsed.path.startsWith("../../blog")) { if (parsed.path.startsWith("../../blog")) {
parsed.path = parsed.path.replace("../../blog", "/blog") parsed.path = parsed.path.replace("../../blog", "/blog")
} }
if (parsed.path.startsWith("../PRIVACY.md")) {
parsed.path = parsed.path.replace("../PRIVACY.md", "/privacy")
}
parsed.path = parsed.path.replace(/\.md$/, ".html").toLowerCase() parsed.path = parsed.path.replace(/\.md$/, ".html").toLowerCase()
return uri.serialize(parsed) return uri.serialize(parsed)
} }
+1 -1
View File
@@ -24,7 +24,7 @@
"copyright-label": "© 2020-2023 SimpleX | Open-Source Project", "copyright-label": "© 2020-2023 SimpleX | Open-Source Project",
"simplex-chat-protocol": "SimpleX Chat protocol", "simplex-chat-protocol": "SimpleX Chat protocol",
"terminal-cli": "Terminal CLI", "terminal-cli": "Terminal CLI",
"terms-and-privacy-policy": "Terms & Privacy Policy", "terms-and-privacy-policy": "Privacy Policy",
"hero-header": "Privacy redefined", "hero-header": "Privacy redefined",
"hero-subheader": "The first messenger<br>without user IDs", "hero-subheader": "The first messenger<br>without user IDs",
"hero-p-1": "Other apps have user IDs: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> SimpleX does not, <strong>not even random numbers</strong>.<br> This radically improves your privacy.", "hero-p-1": "Other apps have user IDs: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> SimpleX does not, <strong>not even random numbers</strong>.<br> This radically improves your privacy.",
@@ -0,0 +1,16 @@
<p><strong>Legally binding transparency</strong></p>
<ul class="mb-[12px]">
<li>We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy.</li>
<li>We are committed to have full consistency between marketing promises and legally binding documents.</li>
</ul>
<p><strong>v5.7 is released:</strong></p>
<ul class="mb-[12px]">
<li>quantum resistant end-to-end encryption with all contacts.</li>
<li>forward and save messages without revealing the source.</li>
<li>in-call sounds and switching sound sources.</li>
<li>better network connection management.</li>
<li>customizable profile images.</li>
</ul>
+1 -3
View File
@@ -20,11 +20,9 @@
{{ "simplex-chat-protocol" | i18n({}, lang ) | safe }} {{ "simplex-chat-protocol" | i18n({}, lang ) | safe }}
<svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg> <svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg>
</a> </a>
<a href="https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md" <a href="/privacy"
target="_blank"
class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1">
{{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }} {{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }}
<svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg>
</a> </a>
<a href="https://github.com/simplex-chat/simplex-chat#help-us-with-donations" <a href="https://github.com/simplex-chat/simplex-chat#help-us-with-donations"
target="_blank" target="_blank"
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="{{ page.url | getlang }}"
{% for language in languages.languages %}
{% if language.label == page.url | getlang %}
dir="{{ "rtl" if language.rtl else "ltr" }}"
{% endif %}
{% endfor %}>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SimpleX Privacy Policy</title>
<meta name="Content-Type" content="text/html;charset=utf-8" />
<meta http-equiv="onion-location" content="{% cfg 'onionLocation' %}{{ permalink }}" />
<meta property="og:url" content="{% cfg 'siteLocation' %}{{ permalink }}" />
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ title }}" />
{% if image %}
<meta property="og:image" content="{% cfg 'siteLocation' %}/blog/{{ image }}" />
{% else %}
<meta property="og:image" content="{% cfg 'siteLocation' %}/img/share_simplex.png" />
{% endif %}
<meta name="twitter:card" content="summary" />
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon.ico" />
<link href="/css/tailwind.css" rel="stylesheet" />
<link rel="stylesheet" href="/css/blog.css" />
<link href="/css/style.css" rel="stylesheet" />
<script async defer src="https://buttons.github.io/buttons.js"></script>
</head>
<body class="bg-[#F3F6F7] dark:bg-[#0C0B13]">
{% include "navbar.html" %}
<section id="article" class="container mt-[25px] mb-[75px] bg-white dark:bg-[#17203D] px-5">
<div class="py-6 md:p-[60px]">{{ content | safe }}</div>
</section>
{% include "footer.html" %}
</body>
</html>
+2
View File
@@ -60,6 +60,8 @@
>Whitepaper >Whitepaper
<svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg> <svg class="float-right" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg>
</a></li> </a></li>
<li><a href="/privacy" class="lg:px-[20px] inline-block"
>{{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }}</a></li>
<li><a href="https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md" <li><a href="https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md"
target="_blank" class="lg:px-[20px] flex items-center gap-1" target="_blank" class="lg:px-[20px] flex items-center gap-1"
>{{ "smp-protocol" | i18n({}, lang ) | safe }} >{{ "smp-protocol" | i18n({}, lang ) | safe }}
+1
View File
@@ -8,6 +8,7 @@ rm website/src/docs/lang/*/README.md
cp -R blog website/src cp -R blog website/src
cp -R images website/src cp -R images website/src
rm website/src/blog/README.md rm website/src/blog/README.md
cp PRIVACY.md website/src/privacy.md
cd website cd website
langs=() langs=()