Compare commits

..

9 Commits

Author SHA1 Message Date
Evgeny Poberezkin
4758c21afa Merge branch 'master' into v5-dart 2021-11-09 19:30:19 +00:00
Evgeny Poberezkin
d79c9d7ef5 cryptographic primitives (#118)
* AES-GSM encryption with padding

* RSA-OAEP encryption and key generation

* SPKI encoding/decoding RSA public keys

* rename functions

* encode/decode RSA keys using asn1lib library

* remove poitycastle namespace

* remove unnecessary typecheck

* fix: ci

Co-authored-by: alex <alex@tekartik.com>
2021-10-16 14:02:06 +01:00
Evgeny Poberezkin
fd247a4e6b style: prefer single quotes (#117) 2021-10-09 13:12:12 +01:00
Evgeny Poberezkin
19ef1f65db abstract Transport class, SocketTransport class (#115)
* abstract Transport class, SocketTransport class

* build: import simplexmq

* feat: simple io transport unit test for read and write

* more efficient buffer extension

Co-authored-by: alex <alex@tekartik.com>
2021-10-09 12:42:41 +01:00
Alexandre Roux
09ace76b82 build: setup dart simple ci using github actions (#114) 2021-10-04 20:27:37 +01:00
Evgeny Poberezkin
e198424da8 move flutter app to packages (#112)
* remove flutter app from root

* add flutter app to packages

* ci: haskell build only on haskell changes

* update app identifiers
2021-10-03 19:59:08 +01:00
Evgeny Poberezkin
a39cd2990f SMP protocol commands encoding/decoding (#111)
* SMP protocol commands encoding/decoding

* change "var" to type

* Parser `word` method now returns null if the word is empty

* refactor Parser `word` method

* move Parser `end` getter

* add linter rules, move linter options to root

* remove omit_local_variable_types linter rule

* ci: only build haskell on changes
2021-10-03 19:10:11 +01:00
Evgeny Poberezkin
fec99d526a Dart / flutter app template (#109) 2021-10-02 11:06:10 +01:00
Evgeny Poberezkin
5cba18120b move haskell implementation to a folder (#108)
* move haskell implementation to a folder

* build v5 branch

* fixing CI
2021-10-02 10:10:35 +01:00
155 changed files with 4110 additions and 1969 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @epoberezkin @efim-poberezkin

1
.github/FUNDING.yml vendored
View File

@@ -1,2 +1 @@
github: simplex-chat
open_collective: simplex-chat

View File

@@ -4,10 +4,16 @@ on:
push:
branches:
- master
- v4
- v5
tags:
- "v*"
paths:
- haskell/**
- .github/workflows/build_haskell.yml
pull_request:
paths:
- haskell/**
- .github/workflows/build_haskell.yml
jobs:
prepare-release:
@@ -87,6 +93,7 @@ jobs:
- name: Build & test
id: build_test
working-directory: ./haskell
run: |
stack build ${{ matrix.stack_args }}
echo "::set-output name=LOCAL_INSTALL_ROOT::$(stack path --local-install-root)"

32
.github/workflows/ci_dart.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Run CI
on:
push:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0' # every sunday at midnight
jobs:
test:
name: Test on ${{ matrix.os }} / ${{ matrix.flutter }}
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: packages/repo_support
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
flutter: [stable, beta, dev]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: '12.x'
- uses: subosito/flutter-action@v1
with:
channel: ${{ matrix.flutter }}
- run: flutter --version
- run: flutter upgrade
- run: dart --version
- run: dart pub get
- run: dart run tool/run_ci.dart

View File

@@ -2,7 +2,7 @@ name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
pull_request:
types: [opened, closed, synchronize]
jobs:

52
.gitignore vendored
View File

@@ -40,11 +40,57 @@ cabal.project.local
cabal.project.local~
.HTF/
.ghc.environment.*
*.cabal
stack.yaml.lock
# Idris
*.ibc
# chat database
*.db
*.db.bak
# Misc
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ
*.iml
*.ipr
*.iws
.idea/
# VS Code
.vscode/
# Flutter/Dart/Pub
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
# Default behavior, only keep it for apps
pubspec.lock
# Web
lib/generated_plugin_registrant.dart
# Symbolication
app.*.symbols
# Obfuscation
app.*.map.json
# Android Studio build artifacts
/android/app/debug
/android/app/profile
/android/app/release

162
README.md
View File

@@ -1,46 +1,19 @@
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
<img align="right" src="images/logo.svg" alt="SimpleX logo" height="90">
# SimpleX Chat
# SimpleX chat
**The world's most private and secure chat** - open-source, decentralized, and without global identities of any kind.
## Private, secure, decentralized
[![GitHub build](https://github.com/simplex-chat/simplex-chat/workflows/build/badge.svg)](https://github.com/simplex-chat/simplex-chat/actions?query=workflow%3Abuild)
[![GitHub downloads](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases)
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases)
[![Follow on Twitter](https://img.shields.io/twitter/follow/SimpleXChat?style=social)](https://twitter.com/simplexchat)
[![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat)
SimpleX chat prototype is a thin terminal UI on top of [SimpleXMQ](https://github.com/simplex-chat/simplexmq) message broker that uses [SMP protocols](https://github.com/simplex-chat/simplexmq/blob/master/protocol). The motivation for SimpleX chat is [presented here](./simplex.md). See [simplex.chat](https://simplex.chat) website for chat demo and the explanations of the system and how SMP protocol works.
> **NEW in v0.4: [groups](#groups) and [sending files](#sending-files)!**
**NEW in v0.5.4: [messages persistence](#access-chat-history)**
The motivation for SimpleX chat is [presented here](./simplex.md).
**NEW in v0.5.0: [user contact addresses](#user-contact-addresses-alpha)**
SimpleX chat prototype is a thin terminal UI on top of [SimpleXMQ](https://github.com/simplex-chat/simplexmq) message broker that uses [SMP protocols](https://github.com/simplex-chat/simplexmq/blob/master/protocol).
**Please note**: v0.5.0 of SimpleX Chat works with the same database, but the connection links are not compatible with the prior versions - please ask all your contacts to upgrade!
### :zap: Quick installation
```sh
curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash
```
Once the chat client is installed, simply run `simplex-chat` from your terminal.
### :wave: Welcome
**We are building the world's most private and secure chat**. If you would like to support it, you can do so in the following ways:
- 🌟 **Star it on GitHub** - it helps us raise the visibility of the project.
- **Install the chat and try it out** - if you spot a bug, please [raise an issue](https://github.com/simplex-chat/simplex-chat/issues).
- :speech_balloon: **Spread the word** - terminal chat is an [early-stage product](#disclaimer) while we stabilize the protocol - you can invite your friends for some fun chat inside your terminal. We're using it right inside our IDEs as we are coding it 👨‍💻
- **Make a donation** via [opencollective](https://opencollective.com/simplex-chat) - every donation helps, however large or small!
- **Make a contribution to the project** - we're constantly moving the project forward and there are always lots of things to do!
We appreciate all the help from our contributors, thank you!
See [simplex.chat](https://simplex.chat) website for chat demo and the explanations of the system and how SMP protocol works.
![simplex-chat](./images/connection.gif)
@@ -49,7 +22,7 @@ We appreciate all the help from our contributors, thank you!
- [Disclaimer](#disclaimer)
- [Network topology](#network-topology)
- [Terminal chat features](#terminal-chat-features)
- [Installation](#🚀-installation)
- [Installation](#installation)
- [Download chat client](#download-chat-client)
- [Linux and MacOS](#linux-and-macos)
- [Troubleshooting on Unix](#troubleshooting-on-unix)
@@ -62,26 +35,25 @@ We appreciate all the help from our contributors, thank you!
- [How to use SimpleX chat](#how-to-use-simplex-chat)
- [Groups](#groups)
- [Sending files](#sending-files)
- [User contact addresses](#user-contact-addresses-alpha)
- [Access chat history](#access-chat-history)
- [Roadmap](#Roadmap)
- [Future roadmap](#future-roadmap)
- [License](#license)
## Disclaimer
This is WIP implementation of SimpleX Chat that implements a new network topology for asynchronous communication combining the advantages and avoiding the disadvantages of federated and P2P networks.
This is WIP implementation of SimpleX chat that implements a new network topology for asynchronous communication combining the advantages and avoiding the disadvantages of federated and P2P networks.
If you expect software to be reliable most of the time, then this is probably not ready for you yet. We use it ourselves for terminal chat and it seems to work most of the time - we would really appreciate if you try SimpleX Chat and give us your feedback!
If you expect a software being reliable most of the time and doing something useful, then this is probably not ready for you yet. We do use it for terminal chat though, and it seems to work most of the time - we would really appreciate if you try it and give us your feedback.
> :warning: **Please note:** The main differentiation of SimpleX network is the approach to internet message routing rather than encryption; for that reason no sufficient attention was paid to either TCP transport level encryption or to E2E encryption protocols - they are implemented in an ad hoc way based on RSA and AES algorithms. See [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a) on TCP transport encryption protocol (AEAD-GCM scheme, with an AES key negotiation based on RSA key hash known to the client in advance) and [this section](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption) on E2E encryption protocol (an ad hoc hybrid scheme a la PGP). These protocols will change in a consumer ready version to something more robust.
**Please note:** The main differentiation of SimpleX network is the approach to internet message routing rather than encryption; for that reason no sufficient attention was paid to either TCP transport level encryption or to E2E encryption protocols - they are implemented in an ad hoc way based on RSA and AES algorithms. See [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a) on TCP transport encryption protocol (AEAD-GCM scheme, with an AES key negotiation based on RSA key hash known to the client in advance) and [this section](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption) on E2E encryption protocol (an ad hoc hybrid scheme a la PGP). These protocols will change in a consumer ready version to something more robust.
## Network topology
SimpleX is a decentralized client-server network that uses redundant, disposable nodes to asynchronously pass messages via message queues, providing receiver and sender anonymity.
SimpleX is a decentralized client-server network that uses redundant, disposable nodes to asynchronously pass the messages via message queues, providing receiver and sender anonymity.
Unlike P2P networks, all messages are passed through one or several (for redundancy) servers, that do not even need to have persistence (in fact, the current [SMP server implementation](https://github.com/simplex-chat/simplexmq#smp-server) uses in-memory message storage, persisting only the queue records) - it provides better metadata protection than P2P designs, as no global participant ID is required, and avoids many [problems of P2P networks](https://github.com/simplex-chat/simplex-chat/blob/master/simplex.md#comparison-with-p2p-messaging-protocols).
Unlike federated networks, the participating server nodes **do not have records of the users**, **do not communicate with each other**, **do not store messages** after they are delivered to the recipients, and there is no way to discover the full list of participating servers. SimpleX network avoids the problem of metadata visibility that federated networks suffer from and better protects the network, as servers do not communicate with each other. Each server node provides unidirectional "dumb pipes" to the users, that do authorization without authentication, having no knowledge of the the users or their contacts. Each queue is assigned two RSA keys - one for receiver and one for sender - and each queue access is authorized with a signature created using a respective key's private counterpart.
Unlike federated networks, the participating server nodes do NOT have records of the users, do NOT communicate with each other, do NOT store messages after they are delivered to the recipients, and there is no way to discover the full list of participating servers - it avoids the problem of metadata visibility that federated networks suffer from and better protects the network, as servers do not communicate with each other. Each server node provides unidirectional "dumb pipes" to the users, that do authorization without authentication, having no knowledge of the the users or their contacts. Each queue is assigned two RSA keys - one for receiver and one for sender - and each queue access is authorized with a signature created using a respective key's private counterpart.
The routing of messages relies on the knowledge of client devices how user contacts and groups map at any given moment of time to these disposable queues on server nodes.
@@ -90,8 +62,6 @@ The routing of messages relies on the knowledge of client devices how user conta
- 1-to-1 chat with multiple people in the same terminal window.
- Group messaging.
- Sending files to contacts and groups.
- User contact addresses - establish connections via multiple-use contact links.
- Messages persisted in a local SQLite database.
- Auto-populated recipient name - just type your messages to reply to the sender once the connection is established.
- Demo SMP servers available and pre-configured in the app - or you can [deploy your own server](https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent).
- No global identity or any names visible to the server(s), ensuring full privacy of your contacts and conversations.
@@ -103,28 +73,14 @@ The routing of messages relies on the knowledge of client devices how user conta
RSA keys are not used as identity, they are randomly generated for each contact.
<a name="🚀-installation"></a>
## 🚀 Installation
## Installation
### Download chat client
Download the chat binary for your system from the [latest stable release](https://github.com/simplex-chat/simplex-chat/releases) and make it executable as shown below.
#### Linux and MacOS
To **install** or **update** `simplex-chat`, you should run the install script. To do that, use the following cURL or Wget command:
```sh
curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash
```
```sh
wget -qO- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash
```
Once the chat client downloads, you can run it with `simplex-chat` command in your terminal.
Alternatively, you can manually download the chat binary for your system from the [latest stable release](https://github.com/simplex-chat/simplex-chat/releases) and make it executable as shown below.
```sh
chmod +x <binary>
mv <binary> ~/.local/bin/simplex-chat
@@ -176,7 +132,7 @@ $ cd simplex-chat
$ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
```
> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.4-stretch` base image (change it in your local [Dockerfile](Dockerfile)).
> **Please note:** If you encounter ``version `GLIBC_2.28' not found`` error, rebuild it with `haskell:8.10.4-stretch` base image (change it in your local [Dockerfile](Dockerfile)).
#### Using Haskell stack
@@ -226,9 +182,9 @@ Run `simplex-chat -h` to see all available options.
### How to use SimpleX chat
Once you have started the chat, you will be prompted to specify your "display name" and an optional "full name" to create a local chat profile. Your display name is an alias for your contacts to refer to you by - it is not unique and does not serve as a global identity. If some of your contacts chose the same display name, the chat client adds a numeric suffix to their local display name.
Once you have started the chat, you will be prompted to specify your "display name" and an optional "full name" to create a local chat profile. Your display name is an alias for your contacts to refer to you by - it is not unique and does not serve as a global identity. In case different contacts chose the same display name, the chat client adds a numeric suffix to their local display names.
The diagram below shows how to connect and message a contact:
This diagram shows how to connect and message a contact:
<div align="center">
<img align="center" src="images/how-to-use-simplex.svg">
@@ -248,7 +204,7 @@ Use `/help` in chat to see the list of available commands.
### Groups
To create a group use `/g <group>`, then add contacts to it with `/a <group> <name>`. You can then send messages to the group by entering `#<group> <message>`. Use `/help groups` for other commands.
To create a group use `/g <group>`, then add contacts to it with `/a <group> <name>`and send messages with `#<group> <message>`. Use `/help groups` for other commands.
![simplex-chat](./images/groups.gif)
@@ -262,83 +218,39 @@ You can send a file to your contact with `/f @<contact> <file_path>` - the recip
You can send files to a group with `/f #<group> <file_path>`.
### User contact addresses (alpha)
As an alternative to one-time invitation links, you can create a long-term address with `/ad` (for `/address`). The created address can then be shared via any channel, and used by other users as a link to make a contact request with `/c <user_contact_address>`.
You can accept or reject incoming requests with `/ac <name>` and `/rc <name>` commands.
User address is "long-term" in a sense that it is a multiple-use connection link - it can be used until it is deleted by the user, in which case all established connections would still remain active (unlike how it works with email, when changing the address results in people not being able to message you).
Use `/help address` for other commands.
> :warning: **Please note:** This is an "alpha" feature - at the moment there is nothing to prevent someone who has obtained this address from spamming you with connection requests; countermeasures will be added soon! (In the short term, you can simply delete the long-term address you created if it starts getting abused.)
![simplex-chat](./images/user-addresses.gif)
### Access chat history
SimpleX chat stores all your contacts and conversations in a local SQLite database, making it private and portable by design, owned and controlled by user.
> 🚧 **Section currently out of date - will be updated soon** 🏗
You can view and search your chat history by querying your database:
SimpleX chat stores all your contacts and conversations in a local database file, making it private and portable by design, fully owned and controlled by you.
You can search your chat history via SQLite database file:
```
sqlite3 ~/.simplex/simplex.chat.db
sqlite3 ~/.simplex/smp-chat.db
```
Now you can run queries against `direct_messages`, `group_messages` and `all_messages` (or their simpler alternatives `direct_messages_plain`, `group_messages_plain` and `all_messages_plain`), for example:
Now you can query `messages` table, for example:
```sql
-- you can put these or your preferred settings into ~/.sqliterc to persist across sqlite3 client sessions
.mode column
.headers on
-- simple views into direct, group and all_messages with user's messages deduplicated for group and all_messages
-- only 'x.msg.new' ("new message") chat events - filters out service events
-- msg_sent is 0 for received, 1 for sent
select * from direct_messages_plain;
select * from group_messages_plain;
select * from all_messages_plain;
-- query other details of your chat history with regular SQL
select * from direct_messages where msg_sent = 1 and chat_msg_event = 'x.file'; -- files you offered for sending
select * from direct_messages where msg_sent = 0 and contact = 'catherine' and msg_body like '%cats%'; -- everything catherine sent related to cats
select * from group_messages where group_name = 'team' and contact = 'alice'; -- all correspondence with alice in #team
-- aggregate your chat data
select contact_or_group, num_messages from (
select contact as contact_or_group, count(1) as num_messages from direct_messages_plain group by contact
union
select group_name as contact_or_group, count(1) as num_messages from group_messages_plain group by group_name
) order by num_messages desc;
```
**Convenience queries**
Get all messages from today (`chat_dt` is in UTC):
```sql
select * from all_messages_plain where date(chat_dt) > date('now', '-1 day') order by chat_dt;
```
Get overnight messages in the morning:
```sql
select * from all_messages_plain where chat_dt > datetime('now', '-15 hours') order by chat_dt;
select * from messages
where conn_alias = cast('alice' as blob)
and body like '%cats%'
order by internal_id desc;
```
> **Please note:** SQLite foreign key constraints are disabled by default, and must be **[enabled separately for each database connection](https://sqlite.org/foreignkeys.html#fk_enable)**. The latter can be achieved by running `PRAGMA foreign_keys = ON;` command on an open database connection. By running data altering queries without enabling foreign keys prior to that, you may risk putting your database in an inconsistent state.
## Roadmap
## Future roadmap
1. Mobile and desktop apps (in progress).
2. SMP protocol improvements:
- SMP queue redundancy and rotation.
- Message delivery confirmation.
- Support multiple devices.
- SMP queue redundancy and rotation.
- Message delivery confirmation.
- Support multiple devices.
3. Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
- keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups.
- keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups.
4. Media server to optimize sending large files to groups.
5. Channels server for large groups and broadcast channels.

View File

@@ -1,27 +0,0 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Simplex.Chat
import Simplex.Chat.Controller (versionNumber)
import Simplex.Chat.Options
import System.Directory (getAppUserDataDirectory)
import System.Terminal (withTerminal)
main :: IO ()
main = do
opts <- welcomeGetOpts
t <- withTerminal pure
simplexChat defaultChatConfig opts t
welcomeGetOpts :: IO ChatOpts
welcomeGetOpts = do
appDir <- getAppUserDataDirectory "simplex"
opts@ChatOpts {dbFile} <- getChatOpts appDir
putStrLn $ "SimpleX Chat v" ++ versionNumber
putStrLn $ "db: " <> dbFile <> ".chat.db, " <> dbFile <> ".agent.db"
putStrLn "type \"/help\" or \"/h\" for usage info"
pure opts

232
haskell/README.md Normal file
View File

@@ -0,0 +1,232 @@
<img align="right" src="images/logo.svg" alt="SimpleX logo" height="90">
# SimpleX chat (legacy Haskell terminal prototype)
## Private, secure, decentralized
[![GitHub build](https://github.com/simplex-chat/simplex-chat/workflows/build/badge.svg)](https://github.com/simplex-chat/simplex-chat/actions?query=workflow%3Abuild)
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases)
> **NEW in v0.4: [groups](#groups) and [sending files](#sending-files)!**
The motivation for SimpleX chat is [presented here](./simplex.md).
SimpleX chat prototype is a thin terminal UI on top of [SimpleXMQ](https://github.com/simplex-chat/simplexmq) message broker that uses [SMP protocols](https://github.com/simplex-chat/simplexmq/blob/master/protocol).
See [simplex.chat](https://simplex.chat) website for chat demo and the explanations of the system and how SMP protocol works.
![simplex-chat](./images/connection.gif)
## Table of contents
- [Disclaimer](#disclaimer)
- [Network topology](#network-topology)
- [Terminal chat features](#terminal-chat-features)
- [Installation](#installation)
- [Download chat client](#download-chat-client)
- [Build from source](#build-from-source)
- [Using Docker](#using-docker)
- [Using Haskell stack](#using-haskell-stack)
- [Usage](#usage)
- [Running the chat client](#running-the-chat-client)
- [How to use SimpleX chat](#how-to-use-simplex-chat)
- [Groups](#groups)
- [Sending files](#sending-files)
- [Access chat history](#access-chat-history)
- [Future roadmap](#future-roadmap)
- [License](#license)
## Disclaimer
This is WIP implementation of SimpleX chat that implements a new network topology for asynchronous communication combining the advantages and avoiding the disadvantages of federated and P2P networks.
If you expect a software being reliable most of the time and doing something useful, then this is probably not ready for you yet. We do use it for terminal chat though, and it seems to work most of the time - we would really appreciate if you try it and give us your feedback.
**Please note:** The main differentiation of SimpleX network is the approach to internet message routing rather than encryption; for that reason no sufficient attention was paid to either TCP transport level encryption or to E2E encryption protocols - they are implemented in an ad hoc way based on RSA and AES algorithms. See [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a) on TCP transport encryption protocol (AEAD-GCM scheme, with an AES key negotiation based on RSA key hash known to the client in advance) and [this section](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2021-01-26-crypto.md#e2e-encryption) on E2E encryption protocol (an ad hoc hybrid scheme a la PGP). These protocols will change in a consumer ready version to something more robust.
## Network topology
SimpleX is a decentralized client-server network that uses redundant, disposable nodes to asynchronously pass the messages via message queues, providing receiver and sender anonymity.
Unlike P2P networks, all messages are passed through one or several (for redundancy) servers, that do not even need to have persistence (in fact, the current [SMP server implementation](https://github.com/simplex-chat/simplexmq#smp-server) uses in-memory message storage, persisting only the queue records) - it provides better metadata protection than P2P designs, as no global participant ID is required, and avoids many [problems of P2P networks](https://github.com/simplex-chat/simplex-chat/blob/master/simplex.md#comparison-with-p2p-messaging-protocols).
Unlike federated networks, the participating server nodes do NOT have records of the users, do NOT communicate with each other, do NOT store messages after they are delivered to the recipients, and there is no way to discover the full list of participating servers - it avoids the problem of metadata visibility that federated networks suffer from and better protects the network, as servers do not communicate with each other. Each server node provides unidirectional "dumb pipes" to the users, that do authorization without authentication, having no knowledge of the the users or their contacts. Each queue is assigned two RSA keys - one for receiver and one for sender - and each queue access is authorized with a signature created using a respective key's private counterpart.
The routing of messages relies on the knowledge of client devices how user contacts and groups map at any given moment of time to these disposable queues on server nodes.
## Terminal chat features
- 1-to-1 chat with multiple people in the same terminal window.
- Group messaging.
- Sending files to contacts and groups.
- Auto-populated recipient name - just type your messages to reply to the sender once the connection is established.
- Demo SMP servers available and pre-configured in the app - or you can [deploy your own server](https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent).
- No global identity or any names visible to the server(s), ensuring full privacy of your contacts and conversations.
- E2E encryption, with RSA public key that has to be passed out-of-band (see [How to use SimpleX chat](#how-to-use-simplex-chat)).
- Message signing and verification with automatically generated RSA keys.
- Message integrity validation (via including the digests of the previous messages).
- Authentication of each command/message by SMP servers with automatically generated RSA key pairs.
- TCP transport encryption using SMP transport protocol.
RSA keys are not used as identity, they are randomly generated for each contact.
## Installation
### Download chat client
Download the chat binary for your system from the [latest stable release](https://github.com/simplex-chat/simplex-chat/releases) and make it executable as shown below.
#### Linux and MacOS
```sh
chmod +x <binary>
mv <binary> ~/.local/bin/simplex-chat
```
(or any other preferred location on PATH).
On MacOS you also need to [allow Gatekeeper to run it](https://support.apple.com/en-us/HT202491).
#### Windows
```sh
move <binary> %APPDATA%/local/bin/simplex-chat.exe
```
### Build from source
#### Using Docker
On Linux, you can build the chat executable using [docker build with custom output](https://docs.docker.com/engine/reference/commandline/build/#custom-build-outputs):
```shell
$ git clone git@github.com:simplex-chat/simplex-chat.git
$ cd simplex-chat
$ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
```
> **Please note:** If you encounter ``version `GLIBC_2.28' not found`` error, rebuild it with `haskell:8.8.4-stretch` base image (change it in your local [Dockerfile](Dockerfile)).
#### Using Haskell stack
Install [Haskell stack](https://docs.haskellstack.org/en/stable/README/):
```shell
curl -sSL https://get.haskellstack.org/ | sh
```
and build the project:
```shell
$ git clone git@github.com:simplex-chat/simplex-chat.git
$ cd simplex-chat
$ stack install
```
## Usage
### Running the chat client
To start the chat client, run `simplex-chat` from the terminal.
By default, app data directory is created in the home directory (`~/.simplex`, or `%APPDATA%/simplex` on Windows), and two SQLite database files `simplex.chat.db` and `simplex.agent.db` are initialized in it.
To specify a different file path prefix for the database files use `-d` command line option:
```shell
$ simplex-chat -d alice
```
Running above, for example, would create `alice.chat.db` and `alice.agent.db` database files in current directory.
Default SMP servers are hosted on Linode (London, UK and Fremont, CA) - they are [pre-configured in the app](https://github.com/simplex-chat/simplex-chat/blob/master/src/Simplex/Chat/Options.hs#L40). Base-64 encoded string after server host is the transport key digest.
If you deployed your own SMP server(s) you can configure client via `-s` option:
```shell
$ simplex-chat -s smp.example.com:5223#KXNE1m2E1m0lm92WGKet9CL6+lO742Vy5G6nsrkvgs8=
```
The base-64 encoded string in server address is the digest of RSA transport handshake key that the server will generate on the first run and output its digest.
You can still talk to people using default or any other server - it only affects the location of the message queue when you initiate the connection (and the reply queue can be on another server, as set by the other party's client).
Run `simplex-chat -h` to see all available options.
### How to use SimpleX chat
Once you have started the chat, you will be prompted to specify your "display name" and an optional "full name" to create a local chat profile. Your display name is an alias for your contacts to refer to you by - it is not unique and does not serve as a global identity. In case different contacts chose the same display name, the chat client adds a numeric suffix to their local display names.
This diagram shows how to connect and message a contact:
<div align="center">
<img align="center" src="images/how-to-use-simplex.svg">
</div>
Once you've set up your local profile, enter `/c` (for `/connect`) to create a new connection and generate an invitation. Send this invitation to your contact via any other channel.
You are able to create multiple invitations by entering `/connect` multiple times and sending these invitations to the corresponding contacts you'd like to connect with.
The invitation has the format `smp::<server>::<queue_id>::<rsa_public_key_for_this_queue_only>`. The invitation can only be used once and even if this is intercepted, the attacker would not be able to use it to send you the messages via this queue once your contact confirms that the connection is established.
The contact who received the invitation should enter `/c <invitation>` to accept the connection. This establishes the connection, and both parties are notified.
They would then use `@<name> <message>` commands to send messages. You may also just start typing a message to send it to the contact that was the last.
Use `/help` in chat to see the list of available commands.
### Groups
To create a group use `/g <group>`, then add contacts to it with `/a <group> <name>`and send messages with `#<group> <message>`. Use `/help groups` for other commands.
![simplex-chat](./images/groups.gif)
> **Please note**: the groups are not stored on any server, they are maintained as a list of members in the app database to whom the messages will be sent.
### Sending files
You can send a file to your contact with `/f @<contact> <file_path>` - the recipient will have to accept it before it is sent. Use `/help files` for other commands.
![simplex-chat](./images/files.gif)
You can send files to a group with `/f #<group> <file_path>`.
### Access chat history
> 🚧 **Section currently out of date - will be updated soon** 🏗
SimpleX chat stores all your contacts and conversations in a local database file, making it private and portable by design, fully owned and controlled by you.
You can search your chat history via SQLite database file:
```
sqlite3 ~/.simplex/smp-chat.db
```
Now you can query `messages` table, for example:
```sql
select * from messages
where conn_alias = cast('alice' as blob)
and body like '%cats%'
order by internal_id desc;
```
> **Please note:** SQLite foreign key constraints are disabled by default, and must be **[enabled separately for each database connection](https://sqlite.org/foreignkeys.html#fk_enable)**. The latter can be achieved by running `PRAGMA foreign_keys = ON;` command on an open database connection. By running data altering queries without enabling foreign keys prior to that, you may risk putting your database in an inconsistent state.
## Future roadmap
1. Mobile and desktop apps (in progress).
2. SMP protocol improvements:
- SMP queue redundancy and rotation.
- Message delivery confirmation.
- Support multiple devices.
3. Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
- keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups.
4. Media server to optimize sending large files to groups.
5. Channels server for large groups and broadcast channels.
## License
[AGPL v3](./LICENSE)

View File

@@ -0,0 +1,119 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Demo where
import Simplex.Chat.Styled
import System.Console.ANSI.Types
import System.Terminal
someViewUpdate :: Monad m => m ()
someViewUpdate = pure ()
chatLayoutDemo :: MonadTerminal m => m ()
chatLayoutDemo =
mapM_
putStyledLn
[ " search " <> Styled gray "(ctrl-s) " <> lineV <> Styled toContact " @bob " <> "Bob Roberts " <> Styled greenColor "@john" <> "",
" " <> lineV <> Styled gray " 14:15 online profile (ctrl-p)",
lineH 20 <> crossover <> lineH 59,
"* " <> Styled [SetConsoleIntensity BoldIntensity] "all chats " <> " " <> lineV <> "",
Styled gray " (ctrl-a) " <> lineV <> "",
"*" <> Styled toContact " @alice " <> Styled darkGray "14:37 " <> lineV <> "",
Styled gray " Hello there! ... " <> lineV <> "",
Styled selected " " <> Styled (toContact <> selected) " @bob " <> Styled (selected <> gray) "12:35 " <> lineV <> "",
Styled selected " All good, John... " <> lineV <> "",
"*" <> Styled group " #team " <> Styled darkGray "10:55 " <> lineV <> "",
Styled gray " What's up ther... " <> lineV <> "",
" " <> Styled toContact " @tom " <> Styled darkGray "Wed " <> lineV <> "",
Styled gray " Have you seen ... " <> lineV <> "",
" " <> lineV,
" " <> lineV,
" " <> lineV,
" " <> lineV,
" " <> lineV,
" " <> lineV <> Styled greenColor " ✔︎" <> Styled darkGray " 12:30" <> Styled toContact " @bob" <> " hey bob - how is it going?",
" " <> lineV <> Styled greenColor " ✔︎" <> Styled darkGray " " <> Styled toContact " " <> " let's meet soon!",
" " <> lineV <> " *" <> Styled darkGray " 12:35" <> Styled contact " bob>" <> " All good, John! How are you?",
" " <> teeL <> lineH 59,
" " <> lineV <> " > " <> Styled toContact "@bob" <> " 😀 This is the message that will be sent to @bob"
]
>> putStyled (Styled ctrlKeys " help (ctrl-h) new contact (ctrl-n) choose chat (ctrl-↓↑) new group (ctrl-g) ")
contact :: [SGR]
contact = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Yellow]
toContact :: [SGR]
toContact = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Cyan]
group :: [SGR]
group = [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Cyan]
selected :: [SGR]
selected = [SetColor Background Vivid Black]
ctrlKeys :: [SGR]
ctrlKeys = [SetColor Background Dull White, SetColor Foreground Dull Black]
gray :: [SGR]
gray = [SetColor Foreground Dull White]
darkGray :: [SGR]
darkGray = [SetColor Foreground Vivid Black]
greenColor :: [SGR]
greenColor = [SetColor Foreground Vivid Green]
lineV :: StyledString
lineV = Styled selected " " -- "\x2502"
lineH :: Int -> StyledString
lineH n = Styled darkGray $ replicate n '\x2500'
teeL :: StyledString
teeL = Styled selected " " -- "\x251C"
crossover :: StyledString
crossover = Styled selected " " -- "\x253C"
putStyledLn :: MonadTerminal m => StyledString -> m ()
putStyledLn s = putStyled s >> putLn
putStyled :: MonadTerminal m => StyledString -> m ()
putStyled (s1 :<>: s2) = putStyled s1 >> putStyled s2
putStyled (Styled [] s) = putString s
putStyled (Styled sgr s) = setSGR sgr >> putString s >> resetAttributes
setSGR :: MonadTerminal m => [SGR] -> m ()
setSGR = mapM_ $ \case
Reset -> resetAttributes
SetConsoleIntensity BoldIntensity -> setAttribute bold
SetConsoleIntensity _ -> resetAttribute bold
SetItalicized True -> setAttribute italic
SetItalicized _ -> resetAttribute italic
SetUnderlining NoUnderline -> resetAttribute underlined
SetUnderlining _ -> setAttribute underlined
SetSwapForegroundBackground True -> setAttribute inverted
SetSwapForegroundBackground _ -> resetAttribute inverted
SetColor l i c -> setAttribute . layer l . intensity i $ color c
SetBlinkSpeed _ -> pure ()
SetVisible _ -> pure ()
SetRGBColor _ _ -> pure ()
SetPaletteColor _ _ -> pure ()
SetDefaultColor _ -> pure ()
where
layer = \case
Foreground -> foreground
Background -> background
intensity = \case
Dull -> id
Vivid -> bright
color = \case
Black -> black
Red -> red
Green -> green
Yellow -> yellow
Blue -> blue
Magenta -> magenta
Cyan -> cyan
White -> white

View File

@@ -0,0 +1,59 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Simplex.Chat
import Simplex.Chat.Controller (versionNumber)
import Simplex.Chat.Options
import System.Directory (getAppUserDataDirectory)
import System.Terminal (withTerminal)
main :: IO ()
main = do
opts <- welcomeGetOpts
t <- withTerminal pure
simplexChat defaultChatConfig opts t
welcomeGetOpts :: IO ChatOpts
welcomeGetOpts = do
appDir <- getAppUserDataDirectory "simplex"
opts@ChatOpts {dbFile} <- getChatOpts appDir
putStrLn $ "SimpleX Chat v" ++ versionNumber
putStrLn $ "db: " <> dbFile <> ".chat.db, " <> dbFile <> ".agent.db"
putStrLn "type \"/help\" or \"/h\" for usage info"
pure opts
-- defaultSettings :: C.Size -> C.VirtualTerminalSettings
-- defaultSettings size =
-- C.VirtualTerminalSettings
-- { C.virtualType = "xterm",
-- C.virtualWindowSize = pure size,
-- C.virtualEvent = retry,
-- C.virtualInterrupt = retry
-- }
-- main :: IO ()
-- main = do
-- void $ createStore "simplex-chat.db" 4
-- hFlush stdout
-- -- ChatTerminal {termSize} <- newChatTerminal
-- -- pos <- C.withVirtualTerminal (defaultSettings termSize) $
-- -- \t -> runTerminalT (C.setAlternateScreenBuffer True >> C.putString "a" >> C.flush >> C.getCursorPosition) t
-- -- print pos
-- -- race_ (printEvents t) (updateTerminal t)
-- void . withTerminal . runTerminalT $ chatLayoutDemo >> C.flush >> C.awaitEvent
-- printEvents :: C.VirtualTerminal -> IO ()
-- printEvents t = forever $ do
-- event <- withTerminal . runTerminalT $ C.flush >> C.awaitEvent
-- runTerminalT (putStringLn $ show event) t
-- updateTerminal :: C.VirtualTerminal -> IO ()
-- updateTerminal t = forever $ do
-- threadDelay 10000
-- win <- readTVarIO $ C.virtualWindow t
-- withTerminal . runTerminalT $ mapM_ C.putStringLn win >> C.flush

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
haskell/images/files.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 KiB

BIN
haskell/images/groups.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 332 KiB

12
haskell/images/logo.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg width="815" height="233" viewBox="0 0 815 233" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5683 66.9141C43.9954 66.9141 47.376 67.2323 50.7104 67.8687C54.0448 68.5051 57.2403 69.3234 60.2968 70.3235C63.3533 71.3236 66.2014 72.4601 68.8411 73.7329C71.4809 75.0058 73.9122 76.2786 76.1351 77.5515L66.132 96.0987L65.9952 95.9474C65.6001 95.5596 64.6732 94.9282 63.2144 94.0531C61.5472 93.053 59.4632 92.0074 56.9624 90.9164C54.4616 89.8254 51.7293 88.8708 48.7654 88.0525C45.8015 87.2342 42.8376 86.8251 39.8737 86.8251C31.723 86.8251 27.6476 89.5072 27.6476 94.8713C27.6476 96.5079 28.0876 97.8716 28.9675 98.9627C29.8474 100.054 31.1441 101.031 32.8576 101.895C34.5711 102.758 36.7245 103.554 39.318 104.281C41.9114 105.009 44.9216 105.827 48.3486 106.736C53.0723 108.009 57.3329 109.395 61.1304 110.896C64.9279 112.396 68.1465 114.26 70.7862 116.487C73.4259 118.715 75.4636 121.419 76.8993 124.602C78.3349 127.784 79.0527 131.602 79.0527 136.057C79.0527 141.512 78.0107 146.126 75.9267 149.899C73.8427 153.673 71.0872 156.718 67.6602 159.037C64.2332 161.355 60.2968 163.037 55.8509 164.083C51.4051 165.128 46.8203 165.651 42.0966 165.651C38.4844 165.651 34.7795 165.378 30.982 164.833C27.1845 164.287 23.4796 163.492 19.8674 162.446C16.2551 161.401 12.7587 160.15 9.37796 158.696C5.99726 157.241 2.87128 155.559 0 153.65L10.0032 134.148L10.1812 134.338C10.6877 134.825 11.8324 135.625 13.6154 136.739C15.6531 138.012 18.177 139.285 21.1872 140.558C24.1974 141.83 27.555 142.967 31.2599 143.967C34.9647 144.967 38.7159 145.467 42.5134 145.467C50.5715 145.467 54.6006 143.058 54.6006 138.239C54.6006 136.421 53.9985 134.921 52.7944 133.739C51.5903 132.557 49.9231 131.489 47.7928 130.534C45.6625 129.579 43.1386 128.693 40.221 127.875C37.3034 127.056 34.1311 126.147 30.7041 125.147C26.1657 123.783 22.2292 122.306 18.8948 120.715C15.5605 119.124 12.805 117.283 10.6284 115.192C8.45174 113.1 6.83086 110.691 5.76571 107.964C4.70056 105.236 4.16798 102.054 4.16798 98.4171C4.16798 93.3257 5.14051 88.8253 7.08557 84.9158C9.03063 81.0063 11.6703 77.7106 15.0047 75.0285C18.3391 72.3464 22.2061 70.3235 26.6056 68.9597C31.0051 67.5959 35.6594 66.9141 40.5683 66.9141ZM131.47 67.7323V164.56H108.685V67.7323H131.47ZM191.39 67.7323L216.954 118.328L242.657 67.7323H267.248V164.56H244.463V106.6L223.067 148.74H210.841L189.445 106.6V164.56H166.66V67.7323H191.39ZM419.925 67.7323V144.922H467.718V164.56H397.14V67.7323H419.925ZM560.072 67.7323V87.3706H514.78V106.191H553.681V124.329H514.78V144.922H561.323V164.56H491.995V67.7323H560.072ZM341.818 67.7323L343.378 67.7677C347.5 67.9563 351.31 68.8991 354.809 70.5962C358.745 72.5055 362.126 75.0058 364.951 78.097C367.776 81.1882 369.999 84.6885 371.62 88.598C373.24 92.5075 374.051 96.4624 374.051 100.463C374.051 104.736 373.287 108.827 371.758 112.737C370.23 116.646 368.1 120.147 365.368 123.238C362.635 126.329 359.324 128.784 355.434 130.602C351.976 132.218 348.189 133.116 344.072 133.296L342.513 133.33H322.507V164.56H299.722V67.7323H341.818ZM340.29 87.3706H322.507V113.828H341.124C343.81 113.828 346.125 112.691 348.07 110.418C350.016 108.145 350.988 104.827 350.988 100.463C350.988 98.1898 350.687 96.2351 350.085 94.5986C349.483 92.9621 348.672 91.5983 347.654 90.5073C346.635 89.4163 345.477 88.6207 344.18 88.1207C342.884 87.6206 341.587 87.3706 340.29 87.3706Z" fill="#062D56"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M642.628 136.08L680.309 173.782L699.513 154.567L699.506 154.561L737.917 116.134L700.236 78.4367L700.243 78.4334L681.404 59.5826L642.993 98.014L642.99 98.0104L681.401 59.5829L643.725 21.881L662.929 2.6652L700.605 40.3673L739.016 1.93511L757.855 20.7859L719.443 59.2176L757.121 96.918L795.533 58.4875L814.373 77.3382L775.959 115.768L813.643 153.471L794.439 172.687L756.756 134.984L718.348 173.415L756.031 211.119L736.827 230.335L699.144 192.63L660.74 231.065L641.901 212.214L680.306 173.78L642.625 136.083L642.628 136.08Z" fill="#07B4B9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M604.77 59.7651L642.446 97.4664L680.856 59.035L699.696 77.8858L661.285 116.317L698.966 154.019L679.762 173.235L642.081 135.532L603.675 173.965L584.836 155.114L623.243 116.682L585.566 78.9809L604.77 59.7651Z" fill="#062D56"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="815" height="233" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -194,8 +194,6 @@ CREATE TABLE connections ( -- all SMP agent connections
DEFERRABLE INITIALLY DEFERRED
);
-- PLEASE NOTE: all tables below were unused and are removed in the migration 20211227_messages.sql
CREATE TABLE events ( -- messages received by the agent, append only
event_id INTEGER PRIMARY KEY,
agent_msg_id INTEGER NOT NULL, -- internal message ID

View File

@@ -1,5 +1,5 @@
name: simplex-chat
version: 0.5.4
version: 0.4.2
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
@@ -29,7 +29,7 @@ dependencies:
- optparse-applicative == 0.15.*
- process == 1.6.*
- simple-logger == 0.1.*
- simplexmq == 0.5.*
- simplexmq == 0.4.*
- sqlite-simple == 0.4.*
- stm == 2.5.*
- terminal == 0.2.*

View File

@@ -24,7 +24,6 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (first)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isSpace)
import Data.Functor (($>))
import Data.Int (Int64)
import Data.List (find)
@@ -44,14 +43,13 @@ import Simplex.Chat.Store
import Simplex.Chat.Styled (plain)
import Simplex.Chat.Terminal
import Simplex.Chat.Types
import Simplex.Chat.Util (ifM, unlessM, whenM)
import Simplex.Chat.Util (ifM, unlessM)
import Simplex.Chat.View
import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), defaultAgentConfig)
import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (MsgBody)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Util (bshow, raceAny_, tryError)
import System.Exit (exitFailure, exitSuccess)
@@ -69,20 +67,10 @@ data ChatCommand
= ChatHelp
| FilesHelp
| GroupsHelp
| MyAddressHelp
| MarkdownHelp
| Welcome
| AddContact
| Connect (Maybe AConnectionRequest)
| ConnectAdmin
| SendAdminWelcome ContactName
| Connect SMPQueueInfo
| DeleteContact ContactName
| ListContacts
| CreateMyAddress
| DeleteMyAddress
| ShowMyAddress
| AcceptContact ContactName
| RejectContact ContactName
| SendMessage ContactName ByteString
| NewGroup GroupProfile
| AddMember GroupName ContactName GroupMemberRole
@@ -92,7 +80,6 @@ data ChatCommand
| LeaveGroup GroupName
| DeleteGroup GroupName
| ListMembers GroupName
| ListGroups
| SendGroupMessage GroupName ByteString
| SendFile ContactName FilePath
| SendGroupFile GroupName FilePath
@@ -133,9 +120,7 @@ simplexChat cfg opts t =
newChatController :: WithTerminal t => ChatConfig -> ChatOpts -> t -> (Notification -> IO ()) -> IO ChatController
newChatController config@ChatConfig {agentConfig = cfg, dbPoolSize, tbqSize} ChatOpts {dbFile, smpServers} t sendNotification = do
let f = chatStoreFile dbFile
firstTime <- not <$> doesFileExist f
chatStore <- createStore f dbPoolSize
chatStore <- createStore (dbFile <> ".chat.db") dbPoolSize
currentUser <- newTVarIO =<< getCreateActiveUser chatStore
chatTerminal <- newChatTerminal t
smpAgent <- getSMPAgentClient cfg {dbFile = dbFile <> ".agent.db", smpServers}
@@ -148,10 +133,7 @@ newChatController config@ChatConfig {agentConfig = cfg, dbPoolSize, tbqSize} Cha
pure ChatController {..}
runSimplexChat :: ChatController -> IO ()
runSimplexChat = runReaderT $ do
user <- readTVarIO =<< asks currentUser
whenM (asks firstTime) . printToView . chatWelcome user $ Onboarding 0 0 0 0 0
race_ runTerminalInput runChatController
runSimplexChat = runReaderT (race_ runTerminalInput runChatController)
runChatController :: (MonadUnliftIO m, MonadReader ChatController m) => m ()
runChatController =
@@ -176,7 +158,7 @@ inputSubscriber = do
atomically (readTBQueue q) >>= \case
InputControl _ -> pure ()
InputCommand s ->
case parseAll chatCommandP . B.dropWhileEnd isSpace . encodeUtf8 $ T.pack s of
case parseAll chatCommandP . encodeUtf8 $ T.pack s of
Left e -> printToView [plain s, "invalid input: " <> plain e]
Right cmd -> do
case cmd of
@@ -184,7 +166,6 @@ inputSubscriber = do
SendGroupMessage g msg -> showSentGroupMessage g msg
SendFile c f -> showSentFileInvitation c f
SendGroupFile g f -> showSentGroupFileInvitation g f
SendAdminWelcome c -> forM_ adminWelcomeMessages $ showSentMessage c
_ -> printToView [plain s]
user <- readTVarIO =<< asks currentUser
withAgentLock a . withLock l . void . runExceptT $
@@ -195,20 +176,14 @@ processChatCommand user@User {userId, profile} = \case
ChatHelp -> printToView chatHelpInfo
FilesHelp -> printToView filesHelpInfo
GroupsHelp -> printToView groupsHelpInfo
MyAddressHelp -> printToView myAddressHelpInfo
MarkdownHelp -> printToView markdownInfo
Welcome -> do
ob <- withStore (`getOnboarding` userId)
printToView $ chatWelcome user ob
AddContact -> do
(connId, cReq) <- withAgent (`createConnection` SCMInvitation)
(connId, qInfo) <- withAgent createConnection
withStore $ \st -> createDirectConnection st userId connId
showInvitation qInfo
Connect qInfo -> do
connId <- withAgent $ \a -> joinConnection a qInfo . directMessage $ XInfo profile
withStore $ \st -> createDirectConnection st userId connId
showInvitation cReq
Connect (Just (ACR SCMInvitation cReq)) -> connect cReq (XInfo profile) >> showSentConfirmation
Connect (Just (ACR SCMContact cReq)) -> connect cReq (XContact profile Nothing) >> showSentInvitation
Connect Nothing -> showInvalidConnReq
ConnectAdmin -> connect adminContactReq (XContact profile Nothing) >> showSentInvitation
SendAdminWelcome cName -> forM_ adminWelcomeMessages $ sendMessageCmd cName
DeleteContact cName ->
withStore (\st -> getContactGroupNames st userId cName) >>= \case
[] -> do
@@ -219,33 +194,11 @@ processChatCommand user@User {userId, profile} = \case
unsetActive $ ActiveC cName
showContactDeleted cName
gs -> showContactGroups cName gs
ListContacts -> withStore (`getUserContacts` user) >>= showContactsList
CreateMyAddress -> do
(connId, cReq) <- withAgent (`createConnection` SCMContact)
withStore $ \st -> createUserContactLink st userId connId cReq
showUserContactLinkCreated cReq
DeleteMyAddress -> do
conns <- withStore $ \st -> getUserContactLinkConnections st userId
withAgent $ \a -> forM_ conns $ \Connection {agentConnId} ->
deleteConnection a agentConnId `catchError` \(_ :: AgentErrorType) -> pure ()
withStore $ \st -> deleteUserContactLink st userId
showUserContactLinkDeleted
ShowMyAddress -> do
cReq <- withStore $ \st -> getUserContactLink st userId
showUserContactLink cReq
AcceptContact cName -> do
UserContactRequest {agentInvitationId, profileId} <- withStore $ \st ->
getContactRequest st userId cName
connId <- withAgent $ \a -> acceptContact a agentInvitationId . directMessage $ XInfo profile
withStore $ \st -> createAcceptedContact st userId connId cName profileId
showAcceptingContactRequest cName
RejectContact cName -> do
UserContactRequest {agentContactConnId, agentInvitationId} <- withStore $ \st ->
getContactRequest st userId cName
`E.finally` deleteContactRequest st userId cName
withAgent $ \a -> rejectContact a agentContactConnId agentInvitationId
showContactRequestRejected cName
SendMessage cName msg -> sendMessageCmd cName msg
SendMessage cName msg -> do
contact <- withStore $ \st -> getContact st userId cName
let msgEvent = XMsgNew $ MsgContent MTText [] [MsgContentBody {contentType = SimplexContentType XCText, contentData = msg}]
sendDirectMessage (contactConnId contact) msgEvent
setActive $ ActiveC cName
NewGroup gProfile -> do
gVar <- asks idsDrg
group <- withStore $ \st -> createNewGroup st gVar user gProfile
@@ -260,15 +213,15 @@ processChatCommand user@User {userId, profile} = \case
unless (memberActive membership) $ chatError CEGroupMemberNotActive
when (isJust $ contactMember contact members) $ chatError (CEGroupDuplicateMember cName)
gVar <- asks idsDrg
(agentConnId, cReq) <- withAgent (`createConnection` SCMInvitation)
(agentConnId, qInfo) <- withAgent createConnection
GroupMember {memberId} <- withStore $ \st -> createContactGroupMember st gVar user groupId contact memRole agentConnId
let msg = XGrpInv $ GroupInvitation (userMemberId, userRole) (memberId, memRole) cReq groupProfile
sendDirectMessage (contactConn contact) msg
let msg = XGrpInv $ GroupInvitation (userMemberId, userRole) (memberId, memRole) qInfo groupProfile
sendDirectMessage (contactConnId contact) msg
showSentGroupInvitation gName cName
setActive $ ActiveG gName
JoinGroup gName -> do
ReceivedGroupInvitation {fromMember, userMember, connRequest} <- withStore $ \st -> getGroupInvitation st user gName
agentConnId <- withAgent $ \a -> joinConnection a connRequest . directMessage . XGrpAcpt $ memberId userMember
ReceivedGroupInvitation {fromMember, userMember, queueInfo} <- withStore $ \st -> getGroupInvitation st user gName
agentConnId <- withAgent $ \a -> joinConnection a queueInfo . directMessage . XGrpAcpt $ memberId userMember
withStore $ \st -> do
createMemberConnection st userId fromMember agentConnId
updateGroupMemberStatus st userId fromMember GSMemAccepted
@@ -281,7 +234,7 @@ processChatCommand user@User {userId, profile} = \case
Just member -> do
let userRole = memberRole membership
when (userRole < GRAdmin || userRole < memberRole member) $ chatError CEGroupUserRole
when (memberStatus member /= GSMemInvited) . sendGroupMessage members $ XGrpMemDel (memberId member)
sendGroupMessage members . XGrpMemDel $ memberId member
deleteMemberConnection member
withStore $ \st -> updateGroupMemberStatus st userId member GSMemRemoved
showDeletedMember gName Nothing (Just member)
@@ -305,8 +258,8 @@ processChatCommand user@User {userId, profile} = \case
ListMembers gName -> do
group <- withStore $ \st -> getGroup st user gName
showGroupMembers group
ListGroups -> withStore (`getUserGroupNames` userId) >>= showGroupsList
SendGroupMessage gName msg -> do
-- TODO save sent messages
-- TODO save pending message delivery for members without connections
Group {members, membership} <- withStore $ \st -> getGroup st user gName
unless (memberActive membership) $ chatError CEGroupMemberUserRemoved
@@ -316,11 +269,11 @@ processChatCommand user@User {userId, profile} = \case
SendFile cName f -> do
(fileSize, chSize) <- checkSndFile f
contact <- withStore $ \st -> getContact st userId cName
(agentConnId, fileConnReq) <- withAgent (`createConnection` SCMInvitation)
let fileInv = FileInvitation {fileName = takeFileName f, fileSize, fileConnReq}
(agentConnId, fileQInfo) <- withAgent createConnection
let fileInv = FileInvitation {fileName = takeFileName f, fileSize, fileQInfo}
SndFileTransfer {fileId} <- withStore $ \st ->
createSndFileTransfer st userId contact f fileInv agentConnId chSize
sendDirectMessage (contactConn contact) $ XFile fileInv
sendDirectMessage (contactConnId contact) $ XFile fileInv
showSentFileInfo fileId
setActive $ ActiveC cName
SendGroupFile gName f -> do
@@ -329,18 +282,17 @@ processChatCommand user@User {userId, profile} = \case
unless (memberActive membership) $ chatError CEGroupMemberUserRemoved
let fileName = takeFileName f
ms <- forM (filter memberActive members) $ \m -> do
(connId, fileConnReq) <- withAgent (`createConnection` SCMInvitation)
pure (m, connId, FileInvitation {fileName, fileSize, fileConnReq})
(connId, fileQInfo) <- withAgent createConnection
pure (m, connId, FileInvitation {fileName, fileSize, fileQInfo})
fileId <- withStore $ \st -> createSndGroupFileTransfer st userId group ms f fileSize chSize
-- TODO sendGroupMessage - same file invitation to all
forM_ ms $ \(m, _, fileInv) ->
traverse (`sendDirectMessage` XFile fileInv) $ memberConn m
traverse (`sendDirectMessage` XFile fileInv) $ memberConnId m
showSentFileInfo fileId
setActive $ ActiveG gName
ReceiveFile fileId filePath_ -> do
ft@RcvFileTransfer {fileInvitation = FileInvitation {fileName, fileConnReq}, fileStatus} <- withStore $ \st -> getRcvFileTransfer st userId fileId
ft@RcvFileTransfer {fileInvitation = FileInvitation {fileName, fileQInfo}, fileStatus} <- withStore $ \st -> getRcvFileTransfer st userId fileId
unless (fileStatus == RFSNew) . chatError $ CEFileAlreadyReceiving fileName
tryError (withAgent $ \a -> joinConnection a fileConnReq . directMessage $ XFileAcpt fileName) >>= \case
tryError (withAgent $ \a -> joinConnection a fileQInfo . directMessage $ XFileAcpt fileName) >>= \case
Right agentConnId -> do
filePath <- getRcvFilePath fileId filePath_ fileName
withStore $ \st -> acceptRcvFileTransfer st userId fileId agentConnId filePath
@@ -362,22 +314,12 @@ processChatCommand user@User {userId, profile} = \case
user' <- withStore $ \st -> updateUserProfile st user p
asks currentUser >>= atomically . (`writeTVar` user')
contacts <- withStore (`getUserContacts` user)
forM_ contacts $ \ct -> sendDirectMessage (contactConn ct) $ XInfo p
forM_ contacts $ \ct -> sendDirectMessage (contactConnId ct) $ XInfo p
showUserProfileUpdated user user'
ShowProfile -> showUserProfile profile
QuitChat -> liftIO exitSuccess
ShowVersion -> printToView clientVersionInfo
where
connect :: ConnectionRequest c -> ChatMsgEvent -> m ()
connect cReq msg = do
connId <- withAgent $ \a -> joinConnection a cReq $ directMessage msg
withStore $ \st -> createDirectConnection st userId connId
sendMessageCmd :: ContactName -> ByteString -> m ()
sendMessageCmd cName msg = do
contact <- withStore $ \st -> getContact st userId cName
let msgEvent = XMsgNew $ MsgContent MTText [] [MsgContentBody {contentType = SimplexContentType XCText, contentData = msg}]
sendDirectMessage (contactConn contact) msgEvent
setActive $ ActiveC cName
contactMember :: Contact -> [GroupMember] -> Maybe GroupMember
contactMember Contact {contactId} =
find $ \GroupMember {memberContactId = cId, memberStatus = s} ->
@@ -436,7 +378,6 @@ subscribeUserConnections = void . runExceptT $ do
subscribeGroups user
subscribeFiles user
subscribePendingConnections user
subscribeUserContactLink user
where
subscribeContacts user = do
contacts <- withStore (`getUserContacts` user)
@@ -477,17 +418,10 @@ subscribeUserConnections = void . runExceptT $ do
resume RcvFileInfo {agentConnId} =
subscribe agentConnId `catchError` showRcvFileSubError ft
subscribePendingConnections user = do
cs <- withStore (`getPendingConnections` user)
subscribeConns cs `catchError` \_ -> pure ()
subscribeUserContactLink User {userId} = do
cs <- withStore (`getUserContactLinkConnections` userId)
(subscribeConns cs >> showUserContactLinkSubscribed)
`catchError` showUserContactLinkSubError
connections <- withStore (`getPendingConnections` user)
forM_ connections $ \Connection {agentConnId} ->
subscribe agentConnId `catchError` \_ -> pure ()
subscribe cId = withAgent (`subscribeConnection` cId)
subscribeConns conns =
withAgent $ \a ->
forM_ conns $ \Connection {agentConnId} ->
subscribeConnection a agentConnId
processAgentMessage :: forall m. ChatMonad m => User -> ConnId -> ACommand 'Agent -> m ()
processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
@@ -503,8 +437,6 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
processRcvFileConn agentMessage conn ft
SndFileConnection conn ft ->
processSndFileConn agentMessage conn ft
UserContactConnection conn uc ->
processUserContactRequest agentMessage conn uc
where
isMember :: MemberId -> Group -> Bool
isMember memId Group {membership, members} =
@@ -518,7 +450,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
agentMsgConnStatus :: ACommand 'Agent -> Maybe ConnStatus
agentMsgConnStatus = \case
CONF {} -> Just ConnRequested
REQ _ _ -> Just ConnRequested
INFO _ -> Just ConnSndReady
CON -> Just ConnReady
_ -> Nothing
@@ -526,41 +458,35 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m ()
processDirectMessage agentMsg conn = \case
Nothing -> case agentMsg of
CONF confId connInfo -> do
REQ confId connInfo -> do
saveConnInfo conn connInfo
allowAgentConnection conn confId $ XInfo profile
acceptAgentConnection conn confId $ XInfo profile
INFO connInfo ->
saveConnInfo conn connInfo
MSG meta msgBody -> do
_ <- saveRcvMSG conn meta msgBody
MSG meta _ ->
withAckMessage agentConnId meta $ pure ()
ackMsgDeliveryEvent conn meta
SENT msgId ->
sentMsgDeliveryEvent conn msgId
_ -> pure ()
Just ct@Contact {localDisplayName = c} -> case agentMsg of
MSG meta msgBody -> do
chatMsgEvent <- saveRcvMSG conn meta msgBody
withAckMessage agentConnId meta $
case chatMsgEvent of
XMsgNew (MsgContent MTText [] body) -> newTextMessage c meta $ find (isSimplexContentType XCText) body
XFile fInv -> processFileInvitation ct meta fInv
XInfo p -> xInfo ct p
XGrpInv gInv -> processGroupInvitation ct gInv
XInfoProbe probe -> xInfoProbe ct probe
XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash
XInfoProbeOk probe -> xInfoProbeOk ct probe
_ -> pure ()
ackMsgDeliveryEvent conn meta
CONF confId connInfo -> do
MSG meta msgBody -> withAckMessage agentConnId meta $ do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody
case chatMsgEvent of
XMsgNew (MsgContent MTText [] body) -> newTextMessage c meta $ find (isSimplexContentType XCText) body
XFile fInv -> processFileInvitation ct meta fInv
XInfo p -> xInfo ct p
XGrpInv gInv -> processGroupInvitation ct gInv
XInfoProbe probe -> xInfoProbe ct probe
XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash
XInfoProbeOk probe -> xInfoProbeOk ct probe
_ -> pure ()
REQ confId connInfo -> do
-- confirming direct connection with a member
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XGrpMemInfo _memId _memProfile -> do
-- TODO check member ID
-- TODO update member profile
allowAgentConnection conn confId XOk
_ -> messageError "CONF from member must have x.grp.mem.info"
acceptAgentConnection conn confId XOk
_ -> messageError "REQ from member must have x.grp.mem.info"
INFO connInfo -> do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
@@ -568,11 +494,8 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
-- TODO check member ID
-- TODO update member profile
pure ()
XInfo _profile -> do
-- TODO update contact profile
pure ()
XOk -> pure ()
_ -> messageError "INFO for existing contact must have x.grp.mem.info, x.info or x.ok"
_ -> messageError "INFO from member must have x.grp.mem.info or x.ok"
CON ->
withStore (\st -> getViaGroupMember st user ct) >>= \case
Nothing -> do
@@ -583,8 +506,6 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
when (memberIsReady m) $ do
notifyMemberConnected gName m
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
SENT msgId ->
sentMsgDeliveryEvent conn msgId
END -> do
showContactAnotherClient c
showToast (c <> "> ") "connected to another client"
@@ -600,7 +521,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
processGroupMessage :: ACommand 'Agent -> Connection -> GroupName -> GroupMember -> m ()
processGroupMessage agentMsg conn gName m = case agentMsg of
CONF confId connInfo -> do
REQ confId connInfo -> do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
case memberCategory m of
GCInviteeMember ->
@@ -608,18 +529,18 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
XGrpAcpt memId
| memId == memberId m -> do
withStore $ \st -> updateGroupMemberStatus st userId m GSMemAccepted
allowAgentConnection conn confId XOk
acceptAgentConnection conn confId XOk
| otherwise -> messageError "x.grp.acpt: memberId is different from expected"
_ -> messageError "CONF from invited member must have x.grp.acpt"
_ -> messageError "REQ from invited member must have x.grp.acpt"
_ ->
case chatMsgEvent of
XGrpMemInfo memId _memProfile
| memId == memberId m -> do
-- TODO update member profile
Group {membership} <- withStore $ \st -> getGroup st user gName
allowAgentConnection conn confId $ XGrpMemInfo (memberId membership) profile
acceptAgentConnection conn confId $ XGrpMemInfo (memberId membership) profile
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
_ -> messageError "CONF from member must have x.grp.mem.info"
_ -> messageError "REQ from member must have x.grp.mem.info"
INFO connInfo -> do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
@@ -650,7 +571,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
intros <- withStore $ \st -> createIntroductions st group m
sendGroupMessage members . XGrpMemNew $ memberInfo m
forM_ intros $ \intro -> do
sendDirectMessage conn . XGrpMemIntro . memberInfo $ reMember intro
sendDirectMessage agentConnId . XGrpMemIntro . memberInfo $ reMember intro
withStore $ \st -> updateIntroStatus st intro GMIntroSent
_ -> do
-- TODO send probe and decide whether to use existing contact connection or the new contact connection
@@ -663,39 +584,34 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
when (contactIsReady ct) $ do
notifyMemberConnected gName m
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
MSG meta msgBody -> do
chatMsgEvent <- saveRcvMSG conn meta msgBody
withAckMessage agentConnId meta $
case chatMsgEvent of
XMsgNew (MsgContent MTText [] body) ->
newGroupTextMessage gName m meta $ find (isSimplexContentType XCText) body
XFile fInv -> processGroupFileInvitation gName m meta fInv
XGrpMemNew memInfo -> xGrpMemNew gName m memInfo
XGrpMemIntro memInfo -> xGrpMemIntro conn gName m memInfo
XGrpMemInv memId introInv -> xGrpMemInv gName m memId introInv
XGrpMemFwd memInfo introInv -> xGrpMemFwd gName m memInfo introInv
XGrpMemDel memId -> xGrpMemDel gName m memId
XGrpLeave -> xGrpLeave gName m
XGrpDel -> xGrpDel gName m
_ -> messageError $ "unsupported message: " <> T.pack (show chatMsgEvent)
ackMsgDeliveryEvent conn meta
SENT msgId ->
sentMsgDeliveryEvent conn msgId
MSG meta msgBody -> withAckMessage agentConnId meta $ do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody
case chatMsgEvent of
XMsgNew (MsgContent MTText [] body) ->
newGroupTextMessage gName m meta $ find (isSimplexContentType XCText) body
XFile fInv -> processGroupFileInvitation gName m meta fInv
XGrpMemNew memInfo -> xGrpMemNew gName m memInfo
XGrpMemIntro memInfo -> xGrpMemIntro gName m memInfo
XGrpMemInv memId introInv -> xGrpMemInv gName m memId introInv
XGrpMemFwd memInfo introInv -> xGrpMemFwd gName m memInfo introInv
XGrpMemDel memId -> xGrpMemDel gName m memId
XGrpLeave -> xGrpLeave gName m
XGrpDel -> xGrpDel gName m
_ -> messageError $ "unsupported message: " <> T.pack (show chatMsgEvent)
_ -> pure ()
processSndFileConn :: ACommand 'Agent -> Connection -> SndFileTransfer -> m ()
processSndFileConn agentMsg conn ft@SndFileTransfer {fileId, fileName, fileStatus} =
case agentMsg of
CONF confId connInfo -> do
REQ confId connInfo -> do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
-- TODO save XFileAcpt message
XFileAcpt name
| name == fileName -> do
withStore $ \st -> updateSndFileStatus st ft FSAccepted
allowAgentConnection conn confId XOk
acceptAgentConnection conn confId XOk
| otherwise -> messageError "x.file.acpt: fileName is different from expected"
_ -> messageError "CONF from file connection must have x.file.acpt"
_ -> messageError "REQ from file connection must have x.file.acpt"
CON -> do
withStore $ \st -> updateSndFileStatus st ft FSConnected
showSndFileStart ft
@@ -749,34 +665,10 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
_ -> pure ()
processUserContactRequest :: ACommand 'Agent -> Connection -> UserContact -> m ()
processUserContactRequest agentMsg _conn UserContact {userContactLinkId} = case agentMsg of
REQ invId connInfo -> do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
case chatMsgEvent of
XContact p _ -> profileContactRequest invId p
XInfo p -> profileContactRequest invId p
-- TODO show/log error, other events in contact request
_ -> pure ()
_ -> pure ()
where
profileContactRequest :: InvitationId -> Profile -> m ()
profileContactRequest invId p = do
cName <- withStore $ \st -> createContactRequest st userId userContactLinkId invId p
showReceivedContactRequest cName p
withAckMessage :: ConnId -> MsgMeta -> m () -> m ()
withAckMessage cId MsgMeta {recipient = (msgId, _)} action =
action `E.finally` withAgent (\a -> ackMessage a cId msgId `catchError` \_ -> pure ())
ackMsgDeliveryEvent :: Connection -> MsgMeta -> m ()
ackMsgDeliveryEvent Connection {connId} MsgMeta {recipient = (msgId, _)} =
withStore $ \st -> createRcvMsgDeliveryEvent st connId msgId MDSRcvAcknowledged
sentMsgDeliveryEvent :: Connection -> AgentMsgId -> m ()
sentMsgDeliveryEvent Connection {connId} msgId =
withStore $ \st -> createSndMsgDeliveryEvent st connId msgId MDSSndSent
badRcvFileChunk :: RcvFileTransfer -> String -> m ()
badRcvFileChunk ft@RcvFileTransfer {fileStatus} err =
case fileStatus of
@@ -795,13 +687,13 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
probeMatchingContacts ct = do
gVar <- asks idsDrg
(probe, probeId) <- withStore $ \st -> createSentProbe st gVar userId ct
sendDirectMessage (contactConn ct) $ XInfoProbe probe
sendDirectMessage (contactConnId ct) $ XInfoProbe probe
cs <- withStore (\st -> getMatchingContacts st userId ct)
let probeHash = C.sha256Hash probe
forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchError` const (pure ())
where
sendProbeHash c probeHash probeId = do
sendDirectMessage (contactConn c) $ XInfoProbeCheck probeHash
sendDirectMessage (contactConnId c) $ XInfoProbeCheck probeHash
withStore $ \st -> createSentProbeHash st userId probeId c
messageWarning :: Text -> m ()
@@ -814,7 +706,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
newTextMessage c meta = \case
Just MsgContentBody {contentData = bs} -> do
let text = safeDecodeUtf8 bs
showReceivedMessage c (snd $ broker meta) (msgPlain text) (integrity (meta :: MsgMeta))
showReceivedMessage c (snd $ broker meta) (msgPlain text) (integrity meta)
showToast (c <> "> ") text
setActive $ ActiveC c
_ -> messageError "x.msg.new: no expected message body"
@@ -823,7 +715,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
newGroupTextMessage gName GroupMember {localDisplayName = c} meta = \case
Just MsgContentBody {contentData = bs} -> do
let text = safeDecodeUtf8 bs
showReceivedGroupMessage gName c (snd $ broker meta) (msgPlain text) (integrity (meta :: MsgMeta))
showReceivedGroupMessage gName c (snd $ broker meta) (msgPlain text) (integrity meta)
showToast ("#" <> gName <> " " <> c <> "> ") text
setActive $ ActiveG gName
_ -> messageError "x.msg.new: no expected message body"
@@ -833,14 +725,14 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
-- TODO chunk size has to be sent as part of invitation
chSize <- asks $ fileChunkSize . config
ft <- withStore $ \st -> createRcvFileTransfer st userId contact fInv chSize
showReceivedMessage c (snd $ broker meta) (receivedFileInvitation ft) (integrity (meta :: MsgMeta))
showReceivedMessage c (snd $ broker meta) (receivedFileInvitation ft) (integrity meta)
setActive $ ActiveC c
processGroupFileInvitation :: GroupName -> GroupMember -> MsgMeta -> FileInvitation -> m ()
processGroupFileInvitation gName m@GroupMember {localDisplayName = c} meta fInv = do
chSize <- asks $ fileChunkSize . config
ft <- withStore $ \st -> createRcvGroupFileTransfer st userId m fInv chSize
showReceivedGroupMessage gName c (snd $ broker meta) (receivedFileInvitation ft) (integrity (meta :: MsgMeta))
showReceivedGroupMessage gName c (snd $ broker meta) (receivedFileInvitation ft) (integrity meta)
setActive $ ActiveG gName
processGroupInvitation :: Contact -> GroupInvitation -> m ()
@@ -868,7 +760,7 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
probeMatch :: Contact -> Contact -> ByteString -> m ()
probeMatch c1@Contact {profile = p1} c2@Contact {profile = p2} probe =
when (p1 == p2) $ do
sendDirectMessage (contactConn c1) $ XInfoProbeOk probe
sendDirectMessage (contactConnId c1) $ XInfoProbeOk probe
mergeContacts c1 c2
xInfoProbeOk :: Contact -> ByteString -> m ()
@@ -881,6 +773,9 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
withStore $ \st -> mergeContactRecords st userId to from
showContactsMerged to from
parseChatMessage :: ByteString -> Either ChatError ChatMessage
parseChatMessage msgBody = first ChatErrorMessage (parseAll rawChatMessageP msgBody >>= toChatMessage)
saveConnInfo :: Connection -> ConnInfo -> m ()
saveConnInfo activeConn connInfo = do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage connInfo
@@ -900,19 +795,19 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
newMember <- withStore $ \st -> createNewGroupMember st user group memInfo GCPostMember GSMemAnnounced
showJoinedGroupMemberConnecting gName m newMember
xGrpMemIntro :: Connection -> GroupName -> GroupMember -> MemberInfo -> m ()
xGrpMemIntro conn gName m memInfo@(MemberInfo memId _ _) =
xGrpMemIntro :: GroupName -> GroupMember -> MemberInfo -> m ()
xGrpMemIntro gName m memInfo@(MemberInfo memId _ _) =
case memberCategory m of
GCHostMember -> do
group <- withStore $ \st -> getGroup st user gName
if isMember memId group
then messageWarning "x.grp.mem.intro ignored: member already exists"
else do
(groupConnId, groupConnReq) <- withAgent (`createConnection` SCMInvitation)
(directConnId, directConnReq) <- withAgent (`createConnection` SCMInvitation)
(groupConnId, groupQInfo) <- withAgent createConnection
(directConnId, directQInfo) <- withAgent createConnection
newMember <- withStore $ \st -> createIntroReMember st user group m memInfo groupConnId directConnId
let msg = XGrpMemInv memId IntroInvitation {groupConnReq, directConnReq}
sendDirectMessage conn msg
let msg = XGrpMemInv memId IntroInvitation {groupQInfo, directQInfo}
sendDirectMessage agentConnId msg
withStore $ \st -> updateGroupMemberStatus st userId newMember GSMemIntroInvited
_ -> messageError "x.grp.mem.intro can be only sent by host member"
@@ -927,13 +822,13 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
intro <- withStore $ \st -> saveIntroInvitation st reMember m introInv
case activeConn (reMember :: GroupMember) of
Nothing -> pure () -- this is not an error, introduction will be forwarded once the member is connected
Just reConn -> do
sendDirectMessage reConn $ XGrpMemFwd (memberInfo m) introInv
Just Connection {agentConnId = reAgentConnId} -> do
sendDirectMessage reAgentConnId $ XGrpMemFwd (memberInfo m) introInv
withStore $ \st -> updateIntroStatus st intro GMIntroInvForwarded
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
xGrpMemFwd :: GroupName -> GroupMember -> MemberInfo -> IntroInvitation -> m ()
xGrpMemFwd gName m memInfo@(MemberInfo memId _ _) introInv@IntroInvitation {groupConnReq, directConnReq} = do
xGrpMemFwd gName m memInfo@(MemberInfo memId _ _) introInv@IntroInvitation {groupQInfo, directQInfo} = do
group@Group {membership} <- withStore $ \st -> getGroup st user gName
toMember <- case find ((== memId) . memberId) $ members group of
-- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent
@@ -944,8 +839,8 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
Just m' -> pure m'
withStore $ \st -> saveMemberInvitation st toMember introInv
let msg = XGrpMemInfo (memberId membership) profile
groupConnId <- withAgent $ \a -> joinConnection a groupConnReq $ directMessage msg
directConnId <- withAgent $ \a -> joinConnection a directConnReq $ directMessage msg
groupConnId <- withAgent $ \a -> joinConnection a groupQInfo $ directMessage msg
directConnId <- withAgent $ \a -> joinConnection a directQInfo $ directMessage msg
withStore $ \st -> createIntroToMemberContact st userId m toMember groupConnId directConnId
xGrpMemDel :: GroupName -> GroupMember -> MemberId -> m ()
@@ -983,9 +878,6 @@ processAgentMessage user@User {userId, profile} agentConnId agentMessage = do
mapM_ deleteMemberConnection ms
showGroupDeleted gName m
parseChatMessage :: ByteString -> Either ChatError ChatMessage
parseChatMessage msgBody = first ChatErrorMessage (parseAll rawChatMessageP msgBody >>= toChatMessage)
sendFileChunk :: ChatMonad m => SndFileTransfer -> m ()
sendFileChunk ft@SndFileTransfer {fileId, fileStatus, agentConnId} =
unless (fileStatus == FSComplete || fileStatus == FSCancelled) $
@@ -1091,46 +983,26 @@ deleteMemberConnection m@GroupMember {activeConn} = do
-- withStore $ \st -> deleteGroupMemberConnection st userId m
forM_ activeConn $ \conn -> withStore $ \st -> updateConnectionStatus st conn ConnDeleted
sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> m ()
sendDirectMessage conn chatMsgEvent = do
let msgBody = directMessage chatMsgEvent
newMsg = NewMessage {direction = MDSnd, chatMsgEventType = toChatEventType chatMsgEvent, msgBody}
-- can be done in transaction after sendMessage, probably shouldn't
msgId <- withStore $ \st -> createNewMessage st newMsg
deliverMessage conn msgBody msgId
sendDirectMessage :: ChatMonad m => ConnId -> ChatMsgEvent -> m ()
sendDirectMessage agentConnId chatMsgEvent =
void . withAgent $ \a -> sendMessage a agentConnId $ directMessage chatMsgEvent
directMessage :: ChatMsgEvent -> ByteString
directMessage chatMsgEvent =
serializeRawChatMessage $
rawChatMessage ChatMessage {chatMsgId = Nothing, chatMsgEvent, chatDAG = Nothing}
deliverMessage :: ChatMonad m => Connection -> MsgBody -> MessageId -> m ()
deliverMessage Connection {connId, agentConnId} msgBody msgId = do
agentMsgId <- withAgent $ \a -> sendMessage a agentConnId msgBody
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
withStore $ \st -> createSndMsgDelivery st sndMsgDelivery msgId
sendGroupMessage :: ChatMonad m => [GroupMember] -> ChatMsgEvent -> m ()
sendGroupMessage members chatMsgEvent = do
let msgBody = directMessage chatMsgEvent
newMsg = NewMessage {direction = MDSnd, chatMsgEventType = toChatEventType chatMsgEvent, msgBody}
msgId <- withStore $ \st -> createNewMessage st newMsg
let msg = directMessage chatMsgEvent
-- TODO once scheduled delivery is implemented memberActive should be changed to memberCurrent
forM_ (map memberConn $ filter memberActive members) $
traverse (\conn -> deliverMessage conn msgBody msgId)
withAgent $ \a ->
forM_ (filter memberActive members) $
traverse (\connId -> sendMessage a connId msg) . memberConnId
saveRcvMSG :: ChatMonad m => Connection -> MsgMeta -> MsgBody -> m ChatMsgEvent
saveRcvMSG Connection {connId} agentMsgMeta msgBody = do
ChatMessage {chatMsgEvent} <- liftEither $ parseChatMessage msgBody
let newMsg = NewMessage {direction = MDRcv, chatMsgEventType = toChatEventType chatMsgEvent, msgBody}
agentMsgId = fst $ recipient agentMsgMeta
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta}
withStore $ \st -> createNewMessageAndRcvMsgDelivery st newMsg rcvMsgDelivery
pure chatMsgEvent
allowAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m ()
allowAgentConnection conn@Connection {agentConnId} confId msg = do
withAgent $ \a -> allowConnection a agentConnId confId $ directMessage msg
acceptAgentConnection :: ChatMonad m => Connection -> ConfirmationId -> ChatMsgEvent -> m ()
acceptAgentConnection conn@Connection {agentConnId} confId msg = do
withAgent $ \a -> acceptConnection a agentConnId confId $ directMessage msg
withStore $ \st -> updateConnectionStatus st conn ConnAccepted
getCreateActiveUser :: SQLiteStore -> IO User
@@ -1218,7 +1090,6 @@ chatCommandP :: Parser ChatCommand
chatCommandP =
("/help files" <|> "/help file" <|> "/hf") $> FilesHelp
<|> ("/help groups" <|> "/help group" <|> "/hg") $> GroupsHelp
<|> ("/help address" <|> "/ha") $> MyAddressHelp
<|> ("/help" <|> "/h") $> ChatHelp
<|> ("/group #" <|> "/group " <|> "/g #" <|> "/g ") *> (NewGroup <$> groupProfile)
<|> ("/add #" <|> "/add " <|> "/a #" <|> "/a ") *> (AddMember <$> displayName <* A.space <*> displayName <*> memberRole)
@@ -1227,10 +1098,8 @@ chatCommandP =
<|> ("/leave #" <|> "/leave " <|> "/l #" <|> "/l ") *> (LeaveGroup <$> displayName)
<|> ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName)
<|> ("/members #" <|> "/members " <|> "/ms #" <|> "/ms ") *> (ListMembers <$> displayName)
<|> ("/groups" <|> "/gs") $> ListGroups
<|> A.char '#' *> (SendGroupMessage <$> displayName <* A.space <*> A.takeByteString)
<|> ("/contacts" <|> "/cs") $> ListContacts
<|> ("/connect " <|> "/c ") *> (Connect <$> ((Just <$> connReqP) <|> A.takeByteString $> Nothing))
<|> ("/connect " <|> "/c ") *> (Connect <$> smpQueueInfoP)
<|> ("/connect" <|> "/c") $> AddContact
<|> ("/delete @" <|> "/delete " <|> "/d @" <|> "/d ") *> (DeleteContact <$> displayName)
<|> A.char '@' *> (SendMessage <$> displayName <*> (A.space *> A.takeByteString))
@@ -1239,18 +1108,10 @@ chatCommandP =
<|> ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (A.space *> filePath))
<|> ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal)
<|> ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal)
<|> "/admin_welcome " *> (SendAdminWelcome <$> displayName)
<|> "/admin" $> ConnectAdmin
<|> ("/address" <|> "/ad") $> CreateMyAddress
<|> ("/delete_address" <|> "/da") $> DeleteMyAddress
<|> ("/show_address" <|> "/sa") $> ShowMyAddress
<|> ("/accept @" <|> "/accept " <|> "/ac @" <|> "/ac ") *> (AcceptContact <$> displayName)
<|> ("/reject @" <|> "/reject " <|> "/rc @" <|> "/rc ") *> (RejectContact <$> displayName)
<|> ("/markdown" <|> "/m") $> MarkdownHelp
<|> ("/welcome" <|> "/w") $> Welcome
<|> ("/profile " <|> "/p ") *> (UpdateProfile <$> userProfile)
<|> ("/profile" <|> "/p") $> ShowProfile
<|> ("/quit" <|> "/q" <|> "/exit") $> QuitChat
<|> ("/quit" <|> "/q") $> QuitChat
<|> ("/version" <|> "/v") $> ShowVersion
where
displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
@@ -1272,7 +1133,3 @@ chatCommandP =
<|> (" admin" $> GRAdmin)
<|> (" member" $> GRMember)
<|> pure GRAdmin
adminContactReq :: ConnectionRequest 'CMContact
adminContactReq =
either error id $ parseAll connReqP' "https://simplex.chat/contact#/?smp=smp%3A%2F%2Fnxc7HnrnM8dOKgkMp008ub_9o9LXJlxlMrMpR-mfMQw%3D%40smp3.simplex.im%2F-TXnePw5eH5-4L7B%23&e2e=rsa%3AMIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAQEA6vpcsZggnYL38Qa2G5YU0W5uqnV8WAq_S3flIFU2kx4qW-aokVT8fo0CLJXv9aagdHObFfhc9SXcZPcm4T2NLnafKTgQa_HYFfj764l6cHkbSI-4JBE1gyhtaapsvrDGIdoiGDLgsF3AJVjqs8gavkuTsmw035aWMH-pkpc4qGlEWpNWp1Nn-7O4sdIIQ7yN48jsdCfeIY-BIk3kFR6s4oQOgiOcnir8e3x5tTuRMX1KWSiuzuqLHqgmcI1IqcPJPrBoTQLbXXEMGG1RsvIudxR03jejXXbQvlxXlNNrxwkniEe-P0rApGuCyv2NRMb4n0Wd3ZwewH7X-xtr16XNbQKBgDouGUHD1C55jB-w8W8VJRhFZS2xIYka9gJH1jjCFxHFzgjo69A_sObIamND1pF_JOzj_XCoA1fDICF95XbfS0rq9iS6xvX6M8Muq8QiJsfD5bRt5nh-Y3GK5rAFXS0ZtyOeh07iMLAMJ_EFxBQuKKDRu9_9KAvLL_plU0PuaMH3"

View File

@@ -26,7 +26,7 @@ import System.IO (Handle)
import UnliftIO.STM
versionNumber :: String
versionNumber = "0.5.4"
versionNumber = "0.4.2"
data ChatConfig = ChatConfig
{ agentConfig :: AgentConfig,
@@ -37,7 +37,6 @@ data ChatConfig = ChatConfig
data ChatController = ChatController
{ currentUser :: TVar User,
firstTime :: Bool,
smpAgent :: AgentClient,
chatTerminal :: ChatTerminal,
chatStore :: SQLiteStore,

View File

@@ -0,0 +1,110 @@
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Chat.Help
( chatHelpInfo,
filesHelpInfo,
groupsHelpInfo,
markdownInfo,
)
where
import Data.List (intersperse)
import Data.Text (Text)
import Simplex.Chat.Markdown
import Simplex.Chat.Styled
import System.Console.ANSI.Types
highlight :: Text -> Markdown
highlight = Markdown (Colored Cyan)
green :: Text -> Markdown
green = Markdown (Colored Green)
indent :: Markdown
indent = " "
listHighlight :: [Text] -> Markdown
listHighlight = mconcat . intersperse ", " . map highlight
chatHelpInfo :: [StyledString]
chatHelpInfo =
map
styleMarkdown
[ highlight "Using SimpleX Chat",
"Follow these steps to set up a connection:",
"",
green "Step 1: " <> highlight "/connect" <> " - Alice adds a contact.",
indent <> "Alice should send the invitation printed by the /add command",
indent <> "to her contact, Bob, out-of-band, via any trusted channel.",
"",
green "Step 2: " <> highlight "/connect <invitation>" <> " - Bob accepts the invitation.",
indent <> "Bob should use the invitation he received out-of-band.",
"",
green "Step 3: " <> "Bob and Alice are notified that the connection is set up,",
indent <> "both can now send messages:",
indent <> highlight "@bob Hello, Bob!" <> " - Alice messages Bob (assuming Bob has display name 'bob').",
indent <> highlight "@alice Hey, Alice!" <> " - Bob replies to Alice.",
"",
green "To send file:",
indent <> highlight "/file bob ./photo.jpg" <> " - Alice sends file to Bob",
indent <> "File commands: " <> highlight "/help files",
"",
green "To create group:",
indent <> highlight "/group team" <> " - create group #team",
indent <> "Group commands: " <> highlight "/help groups",
"",
green "Other commands:",
indent <> highlight "/profile " <> " - show user profile",
indent <> highlight "/profile <name> [<full_name>]" <> " - update user profile",
indent <> highlight "/delete <contact>" <> " - delete contact and all messages with them",
indent <> highlight "/markdown " <> " - show supported markdown syntax",
indent <> highlight "/version " <> " - show SimpleX Chat version",
indent <> highlight "/quit " <> " - quit chat",
"",
"The commands may be abbreviated to a single letter: " <> listHighlight ["/c", "/f", "/g", "/p", "/h"] <> ", etc."
]
filesHelpInfo :: [StyledString]
filesHelpInfo =
map
styleMarkdown
[ green "File transfer commands:",
indent <> highlight "/file @<contact> <file_path> " <> " - send file to contact",
indent <> highlight "/file #<group> <file_path> " <> " - send file to group",
indent <> highlight "/freceive <file_id> [<file_path>]" <> " - accept to receive file",
indent <> highlight "/fcancel <file_id> " <> " - cancel sending / receiving file",
indent <> highlight "/fstatus <file_id> " <> " - show file transfer status",
"",
"The commands may be abbreviated: " <> listHighlight ["/f", "/fr", "/fc", "/fs"]
]
groupsHelpInfo :: [StyledString]
groupsHelpInfo =
map
styleMarkdown
[ green "Group management commands:",
indent <> highlight "/group <group> [<full_name>] " <> " - create group",
indent <> highlight "/add <group> <contact> [<role>]" <> " - add contact to group, roles: " <> highlight "owner" <> ", " <> highlight "admin" <> " (default), " <> highlight "member",
indent <> highlight "/join <group> " <> " - accept group invitation",
indent <> highlight "/remove <group> <member> " <> " - remove member from group",
indent <> highlight "/leave <group> " <> " - leave group",
indent <> highlight "/delete <group> " <> " - delete group",
indent <> highlight "/members <group> " <> " - list group members",
indent <> highlight "#<group> <message> " <> " - send message to group",
"",
"The commands may be abbreviated: " <> listHighlight ["/g", "/a", "/j", "/rm", "/l", "/d", "/ms"]
]
markdownInfo :: [StyledString]
markdownInfo =
map
styleMarkdown
[ green "Markdown:",
indent <> highlight "*bold* " <> " - " <> Markdown Bold "bold text",
indent <> highlight "_italic_ " <> " - " <> Markdown Italic "italic text" <> " (shown as underlined)",
indent <> highlight "+underlined+ " <> " - " <> Markdown Underline "underlined text",
indent <> highlight "~strikethrough~" <> " - " <> Markdown StrikeThrough "strikethrough text" <> " (shown as inverse)",
indent <> highlight "`code snippet` " <> " - " <> Markdown Snippet "a + b // no *markdown* here",
indent <> highlight "!1 text! " <> " - " <> Markdown (Colored Red) "red text" <> " (1-6: red, green, blue, yellow, cyan, magenta)",
indent <> highlight "#secret# " <> " - " <> Markdown Secret "secret text" <> " (can be copy-pasted)"
]

View File

@@ -10,10 +10,10 @@ import Control.Monad (void)
import Data.List (isInfixOf)
import Data.Map (Map, fromList)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, isJust)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import System.Directory (createDirectoryIfMissing, doesFileExist, findExecutable, getAppUserDataDirectory)
import System.Directory (createDirectoryIfMissing, doesFileExist, getAppUserDataDirectory)
import System.FilePath (combine)
import System.Info (os)
import System.Process (readCreateProcess, shell)
@@ -27,25 +27,17 @@ initializeNotifications =
"mingw32" -> initWinNotify
"linux" ->
doesFileExist "/proc/sys/kernel/osrelease" >>= \case
False -> initLinuxNotify
False -> pure $ notify linuxScript
True -> do
v <- readFile "/proc/sys/kernel/osrelease"
if "Microsoft" `isInfixOf` v || "WSL" `isInfixOf` v
then initWslNotify
else initLinuxNotify
_ -> pure noNotifications
noNotifications :: Notification -> IO ()
noNotifications _ = pure ()
else pure $ notify linuxScript
_ -> pure . const $ pure ()
hideException :: (a -> IO ()) -> (a -> IO ())
hideException f a = f a `catch` \(_ :: SomeException) -> pure ()
initLinuxNotify :: IO (Notification -> IO ())
initLinuxNotify = do
found <- isJust <$> findExecutable "notify-send"
pure $ if found then notify linuxScript else noNotifications
notify :: (Notification -> Text) -> Notification -> IO ()
notify script notification =
void $ readCreateProcess (shell . T.unpack $ script notification) ""

View File

@@ -11,7 +11,7 @@
module Simplex.Chat.Protocol where
import Control.Applicative (optional)
import Control.Monad ((<=<), (>=>))
import Control.Monad ((<=<))
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import Data.Attoparsec.ByteString.Char8 (Parser)
@@ -21,7 +21,7 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Int (Int64)
import Data.List (find, findIndex)
import Data.List (find)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
@@ -38,7 +38,6 @@ data ChatDirection (p :: AParty) where
SentGroupMessage :: GroupName -> ChatDirection 'Client
SndFileConnection :: Connection -> SndFileTransfer -> ChatDirection 'Agent
RcvFileConnection :: Connection -> RcvFileTransfer -> ChatDirection 'Agent
UserContactConnection :: Connection -> UserContact -> ChatDirection 'Agent
deriving instance Eq (ChatDirection p)
@@ -50,14 +49,12 @@ fromConnection = \case
ReceivedGroupMessage conn _ _ -> conn
SndFileConnection conn _ -> conn
RcvFileConnection conn _ -> conn
UserContactConnection conn _ -> conn
data ChatMsgEvent
= XMsgNew MsgContent
| XFile FileInvitation
| XFileAcpt String
| XInfo Profile
| XContact Profile (Maybe MsgContent)
| XGrpInv GroupInvitation
| XGrpAcpt MemberId
| XGrpMemNew MemberInfo
@@ -103,30 +100,6 @@ data ChatMessage = ChatMessage
}
deriving (Eq, Show)
toChatEventType :: ChatMsgEvent -> Text
toChatEventType = \case
XMsgNew _ -> "x.msg.new"
XFile _ -> "x.file"
XFileAcpt _ -> "x.file.acpt"
XInfo _ -> "x.info"
XContact _ _ -> "x.con"
XGrpInv _ -> "x.grp.inv"
XGrpAcpt _ -> "x.grp.acpt"
XGrpMemNew _ -> "x.grp.mem.new"
XGrpMemIntro _ -> "x.grp.mem.intro"
XGrpMemInv _ _ -> "x.grp.mem.inv"
XGrpMemFwd _ _ -> "x.grp.mem.fwd"
XGrpMemInfo _ _ -> "x.grp.mem.info"
XGrpMemCon _ -> "x.grp.mem.con"
XGrpMemConAll _ -> "x.grp.mem.con.all"
XGrpMemDel _ -> "x.grp.mem.del"
XGrpLeave -> "x.grp.leave"
XGrpDel -> "x.grp.del"
XInfoProbe _ -> "x.info.probe"
XInfoProbeCheck _ -> "x.info.probe.check"
XInfoProbeOk _ -> "x.info.probe.ok"
XOk -> "x.ok"
toChatMessage :: RawChatMessage -> Either String ChatMessage
toChatMessage RawChatMessage {chatMsgId, chatMsgEvent, chatMsgParams, chatMsgBody} = do
(chatDAG, body) <- getDAG <$> mapM toMsgBodyContent chatMsgBody
@@ -134,42 +107,34 @@ toChatMessage RawChatMessage {chatMsgId, chatMsgEvent, chatMsgParams, chatMsgBod
case (chatMsgEvent, chatMsgParams) of
("x.msg.new", mt : rawFiles) -> do
t <- toMsgType mt
files <- toFiles rawFiles
files <- mapM (toContentInfo <=< parseAll contentInfoP) rawFiles
chatMsg . XMsgNew $ MsgContent {messageType = t, files, content = body}
("x.file", [name, size, cReq]) -> do
("x.file", [name, size, qInfo]) -> do
let fileName = T.unpack $ safeDecodeUtf8 name
fileSize <- parseAll A.decimal size
fileConnReq <- parseAll connReqP' cReq
chatMsg . XFile $ FileInvitation {fileName, fileSize, fileConnReq}
fileQInfo <- parseAll smpQueueInfoP qInfo
chatMsg . XFile $ FileInvitation {fileName, fileSize, fileQInfo}
("x.file.acpt", [name]) ->
chatMsg . XFileAcpt . T.unpack $ safeDecodeUtf8 name
("x.info", []) -> do
profile <- getJSON body
chatMsg $ XInfo profile
("x.con", []) -> do
profile <- getJSON body
chatMsg $ XContact profile Nothing
("x.con", mt : rawFiles) -> do
(profile, body') <- extractJSON body
t <- toMsgType mt
files <- toFiles rawFiles
chatMsg . XContact profile $ Just MsgContent {messageType = t, files, content = body'}
("x.grp.inv", [fromMemId, fromRole, memId, role, cReq]) -> do
("x.grp.inv", [fromMemId, fromRole, memId, role, qInfo]) -> do
fromMem <- (,) <$> B64.decode fromMemId <*> toMemberRole fromRole
invitedMem <- (,) <$> B64.decode memId <*> toMemberRole role
groupConnReq <- parseAll connReqP' cReq
groupQInfo <- parseAll smpQueueInfoP qInfo
profile <- getJSON body
chatMsg . XGrpInv $ GroupInvitation fromMem invitedMem groupConnReq profile
chatMsg . XGrpInv $ GroupInvitation fromMem invitedMem groupQInfo profile
("x.grp.acpt", [memId]) ->
chatMsg . XGrpAcpt =<< B64.decode memId
("x.grp.mem.new", [memId, role]) -> do
chatMsg . XGrpMemNew =<< toMemberInfo memId role body
("x.grp.mem.intro", [memId, role]) ->
chatMsg . XGrpMemIntro =<< toMemberInfo memId role body
("x.grp.mem.inv", [memId, groupConnReq, directConnReq]) ->
chatMsg =<< (XGrpMemInv <$> B64.decode memId <*> toIntroInv groupConnReq directConnReq)
("x.grp.mem.fwd", [memId, role, groupConnReq, directConnReq]) -> do
chatMsg =<< (XGrpMemFwd <$> toMemberInfo memId role body <*> toIntroInv groupConnReq directConnReq)
("x.grp.mem.inv", [memId, groupQInfo, directQInfo]) ->
chatMsg =<< (XGrpMemInv <$> B64.decode memId <*> toIntroInv groupQInfo directQInfo)
("x.grp.mem.fwd", [memId, role, groupQInfo, directQInfo]) -> do
chatMsg =<< (XGrpMemFwd <$> toMemberInfo memId role body <*> toIntroInv groupQInfo directQInfo)
("x.grp.mem.info", [memId]) ->
chatMsg =<< (XGrpMemInfo <$> B64.decode memId <*> getJSON body)
("x.grp.mem.con", [memId]) ->
@@ -185,9 +150,9 @@ toChatMessage RawChatMessage {chatMsgId, chatMsgEvent, chatMsgParams, chatMsgBod
("x.info.probe", [probe]) -> do
chatMsg . XInfoProbe =<< B64.decode probe
("x.info.probe.check", [probeHash]) -> do
chatMsg . XInfoProbeCheck =<< B64.decode probeHash
chatMsg =<< (XInfoProbeCheck <$> B64.decode probeHash)
("x.info.probe.ok", [probe]) -> do
chatMsg . XInfoProbeOk =<< B64.decode probe
chatMsg =<< (XInfoProbeOk <$> B64.decode probe)
("x.ok", []) ->
chatMsg XOk
_ -> Left $ "bad syntax or unsupported event " <> B.unpack chatMsgEvent
@@ -199,18 +164,11 @@ toChatMessage RawChatMessage {chatMsgId, chatMsgEvent, chatMsgParams, chatMsgBod
toMemberInfo :: ByteString -> ByteString -> [MsgContentBody] -> Either String MemberInfo
toMemberInfo memId role body = MemberInfo <$> B64.decode memId <*> toMemberRole role <*> getJSON body
toIntroInv :: ByteString -> ByteString -> Either String IntroInvitation
toIntroInv groupConnReq directConnReq = IntroInvitation <$> parseAll connReqP' groupConnReq <*> parseAll connReqP' directConnReq
toIntroInv groupQInfo directQInfo = IntroInvitation <$> parseAll smpQueueInfoP groupQInfo <*> parseAll smpQueueInfoP directQInfo
toContentInfo :: (RawContentType, Int) -> Either String (ContentType, Int)
toContentInfo (rawType, size) = (,size) <$> toContentType rawType
toFiles :: [ByteString] -> Either String [(ContentType, Int)]
toFiles = mapM $ toContentInfo <=< parseAll contentInfoP
getJSON :: FromJSON a => [MsgContentBody] -> Either String a
getJSON = J.eitherDecodeStrict' <=< getSimplexContentType XCJson
extractJSON :: FromJSON a => [MsgContentBody] -> Either String (a, [MsgContentBody])
extractJSON =
extractSimplexContentType XCJson >=> \(a, bs) -> do
j <- J.eitherDecodeStrict' a
pure (j, bs)
isContentType :: ContentType -> MsgContentBody -> Bool
isContentType t MsgContentBody {contentType = t'} = t == t'
@@ -223,85 +181,71 @@ getContentType t body = case find (isContentType t) body of
Just MsgContentBody {contentData} -> Right contentData
Nothing -> Left "no required content type"
extractContentType :: ContentType -> [MsgContentBody] -> Either String (ByteString, [MsgContentBody])
extractContentType t body = case findIndex (isContentType t) body of
Just i -> case splitAt i body of
(b, el : a) -> Right (contentData (el :: MsgContentBody), b ++ a)
(_, []) -> Left "no required content type" -- this can only happen if findIndex returns incorrect result
Nothing -> Left "no required content type"
getSimplexContentType :: XContentType -> [MsgContentBody] -> Either String ByteString
getSimplexContentType = getContentType . SimplexContentType
extractSimplexContentType :: XContentType -> [MsgContentBody] -> Either String (ByteString, [MsgContentBody])
extractSimplexContentType = extractContentType . SimplexContentType
rawChatMessage :: ChatMessage -> RawChatMessage
rawChatMessage ChatMessage {chatMsgId, chatMsgEvent, chatDAG} =
case chatMsgEvent of
XMsgNew MsgContent {messageType = t, files, content} ->
rawMsg (rawMsgType t : toRawFiles files) content
XFile FileInvitation {fileName, fileSize, fileConnReq} ->
rawMsg [encodeUtf8 $ T.pack fileName, bshow fileSize, serializeConnReq' fileConnReq] []
let rawFiles = map (serializeContentInfo . rawContentInfo) files
in rawMsg "x.msg.new" (rawMsgType t : rawFiles) content
XFile FileInvitation {fileName, fileSize, fileQInfo} ->
rawMsg "x.file" [encodeUtf8 $ T.pack fileName, bshow fileSize, serializeSmpQueueInfo fileQInfo] []
XFileAcpt fileName ->
rawMsg [encodeUtf8 $ T.pack fileName] []
rawMsg "x.file.acpt" [encodeUtf8 $ T.pack fileName] []
XInfo profile ->
rawMsg [] [jsonBody profile]
XContact profile Nothing ->
rawMsg [] [jsonBody profile]
XContact profile (Just MsgContent {messageType = t, files, content}) ->
rawMsg (rawMsgType t : toRawFiles files) (jsonBody profile : content)
XGrpInv (GroupInvitation (fromMemId, fromRole) (memId, role) cReq groupProfile) ->
rawMsg "x.info" [] [jsonBody profile]
XGrpInv (GroupInvitation (fromMemId, fromRole) (memId, role) qInfo groupProfile) ->
let params =
[ B64.encode fromMemId,
serializeMemberRole fromRole,
B64.encode memId,
serializeMemberRole role,
serializeConnReq' cReq
serializeSmpQueueInfo qInfo
]
in rawMsg params [jsonBody groupProfile]
in rawMsg "x.grp.inv" params [jsonBody groupProfile]
XGrpAcpt memId ->
rawMsg [B64.encode memId] []
rawMsg "x.grp.acpt" [B64.encode memId] []
XGrpMemNew (MemberInfo memId role profile) ->
let params = [B64.encode memId, serializeMemberRole role]
in rawMsg params [jsonBody profile]
in rawMsg "x.grp.mem.new" params [jsonBody profile]
XGrpMemIntro (MemberInfo memId role profile) ->
rawMsg [B64.encode memId, serializeMemberRole role] [jsonBody profile]
XGrpMemInv memId IntroInvitation {groupConnReq, directConnReq} ->
let params = [B64.encode memId, serializeConnReq' groupConnReq, serializeConnReq' directConnReq]
in rawMsg params []
XGrpMemFwd (MemberInfo memId role profile) IntroInvitation {groupConnReq, directConnReq} ->
rawMsg "x.grp.mem.intro" [B64.encode memId, serializeMemberRole role] [jsonBody profile]
XGrpMemInv memId IntroInvitation {groupQInfo, directQInfo} ->
let params = [B64.encode memId, serializeSmpQueueInfo groupQInfo, serializeSmpQueueInfo directQInfo]
in rawMsg "x.grp.mem.inv" params []
XGrpMemFwd (MemberInfo memId role profile) IntroInvitation {groupQInfo, directQInfo} ->
let params =
[ B64.encode memId,
serializeMemberRole role,
serializeConnReq' groupConnReq,
serializeConnReq' directConnReq
serializeSmpQueueInfo groupQInfo,
serializeSmpQueueInfo directQInfo
]
in rawMsg params [jsonBody profile]
in rawMsg "x.grp.mem.fwd" params [jsonBody profile]
XGrpMemInfo memId profile ->
rawMsg [B64.encode memId] [jsonBody profile]
rawMsg "x.grp.mem.info" [B64.encode memId] [jsonBody profile]
XGrpMemCon memId ->
rawMsg [B64.encode memId] []
rawMsg "x.grp.mem.con" [B64.encode memId] []
XGrpMemConAll memId ->
rawMsg [B64.encode memId] []
rawMsg "x.grp.mem.con.all" [B64.encode memId] []
XGrpMemDel memId ->
rawMsg [B64.encode memId] []
rawMsg "x.grp.mem.del" [B64.encode memId] []
XGrpLeave ->
rawMsg [] []
rawMsg "x.grp.leave" [] []
XGrpDel ->
rawMsg [] []
rawMsg "x.grp.del" [] []
XInfoProbe probe ->
rawMsg [B64.encode probe] []
rawMsg "x.info.probe" [B64.encode probe] []
XInfoProbeCheck probeHash ->
rawMsg [B64.encode probeHash] []
rawMsg "x.info.probe.check" [B64.encode probeHash] []
XInfoProbeOk probe ->
rawMsg [B64.encode probe] []
rawMsg "x.info.probe.ok" [B64.encode probe] []
XOk ->
rawMsg [] []
rawMsg "x.ok" [] []
where
rawMsg :: [ByteString] -> [MsgContentBody] -> RawChatMessage
rawMsg chatMsgParams body = do
let event = encodeUtf8 $ toChatEventType chatMsgEvent
rawMsg :: ByteString -> [ByteString] -> [MsgContentBody] -> RawChatMessage
rawMsg event chatMsgParams body =
RawChatMessage {chatMsgId, chatMsgEvent = event, chatMsgParams, chatMsgBody = rawWithDAG body}
rawContentInfo :: (ContentType, Int) -> (RawContentType, Int)
rawContentInfo (t, size) = (rawContentType t, size)
@@ -313,8 +257,6 @@ rawChatMessage ChatMessage {chatMsgId, chatMsgEvent, chatDAG} =
rawWithDAG body = map rawMsgBodyContent $ case chatDAG of
Nothing -> body
Just dag -> MsgContentBody {contentType = SimplexDAG, contentData = dag} : body
toRawFiles :: [(ContentType, Int)] -> [ByteString]
toRawFiles = map $ serializeContentInfo . rawContentInfo
toMsgBodyContent :: RawMsgBodyContent -> Either String MsgContentBody
toMsgBodyContent RawMsgBodyContent {contentType, contentData} = do

View File

@@ -17,7 +17,6 @@ module Simplex.Chat.Store
( SQLiteStore,
StoreError (..),
createStore,
chatStoreFile,
createUser,
getUsers,
setActiveUser,
@@ -29,14 +28,6 @@ module Simplex.Chat.Store
updateUserProfile,
updateContactProfile,
getUserContacts,
createUserContactLink,
getUserContactLinkConnections,
deleteUserContactLink,
getUserContactLink,
createContactRequest,
getContactRequest,
deleteContactRequest,
createAcceptedContact,
getLiveSndFileTransfers,
getLiveRcvFileTransfers,
getPendingSndChunks,
@@ -49,7 +40,6 @@ module Simplex.Chat.Store
getGroup,
deleteGroup,
getUserGroups,
getUserGroupNames,
getGroupInvitation,
createContactGroupMember,
createMemberConnection,
@@ -89,12 +79,6 @@ module Simplex.Chat.Store
deleteRcvFileChunks,
getFileTransfer,
getFileTransferProgress,
getOnboarding,
createNewMessage,
createSndMsgDelivery,
createNewMessageAndRcvMsgDelivery,
createSndMsgDeliveryEvent,
createRcvMsgDeliveryEvent,
)
where
@@ -123,7 +107,7 @@ import qualified Database.SQLite.Simple as DB
import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (AParty (..), AgentMsgId, ConnId, InvitationId, MsgMeta (..))
import Simplex.Messaging.Agent.Protocol (AParty (..), AgentMsgId, ConnId, SMPQueueInfo)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
import qualified Simplex.Messaging.Crypto as C
@@ -143,9 +127,6 @@ migrations =
createStore :: FilePath -> Int -> IO SQLiteStore
createStore dbFilePath poolSize = createSQLiteStore dbFilePath poolSize migrations
chatStoreFile :: FilePath -> FilePath
chatStoreFile = (<> ".chat.db")
checkConstraint :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a)
checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err)
@@ -199,45 +180,20 @@ setActiveUser st userId = do
createDirectConnection :: MonadUnliftIO m => SQLiteStore -> UserId -> ConnId -> m ()
createDirectConnection st userId agentConnId =
liftIO . withTransaction st $ \db ->
void $ createContactConnection_ db userId agentConnId Nothing 0
void $ createConnection_ db userId agentConnId Nothing 0
createContactConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> IO Connection
createContactConnection_ db userId = createConnection_ db userId ConnContact Nothing
-- field types coincidentally match, but the first element here is user ID and not connection ID as in ConnectionRow
type InsertedConnectionRow = ConnectionRow
createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe Int64 -> Int -> IO Connection
createConnection_ db userId connType entityId agentConnId viaContact connLevel = do
createConnection_ :: DB.Connection -> UserId -> ConnId -> Maybe Int64 -> Int -> IO Connection
createConnection_ db userId agentConnId viaContact connLevel = do
createdAt <- getCurrentTime
DB.execute
db
[sql|
INSERT INTO connections (
user_id, agent_conn_id, conn_level, via_contact, conn_status, conn_type,
contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?);
INSERT INTO connections
(user_id, agent_conn_id, conn_status, conn_type, via_contact, conn_level, created_at) VALUES (?,?,?,?,?,?,?);
|]
(insertConnParams createdAt)
(userId, agentConnId, ConnNew, ConnContact, viaContact, connLevel, createdAt)
connId <- insertedRowId db
pure Connection {connId, agentConnId, connType, entityId, viaContact, connLevel, connStatus = ConnNew, createdAt}
where
insertConnParams :: UTCTime -> InsertedConnectionRow
insertConnParams createdAt =
( userId,
agentConnId,
connLevel,
viaContact,
ConnNew,
connType,
ent ConnContact,
ent ConnMember,
ent ConnSndFile,
ent ConnRcvFile,
ent ConnUserContact,
createdAt
)
ent ct = if connType == ct then entityId else Nothing
pure Connection {connId, agentConnId, connType = ConnContact, entityId = Nothing, viaContact, connLevel, connStatus = ConnNew, createdAt}
createDirectContact :: StoreMonad m => SQLiteStore -> UserId -> Connection -> Profile -> m ()
createDirectContact st userId Connection {connId} profile =
@@ -381,7 +337,7 @@ getContact_ db userId localDisplayName = do
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at
FROM connections c
WHERE c.user_id = :user_id AND c.contact_id == :contact_id
ORDER BY c.connection_id DESC
@@ -403,142 +359,6 @@ getUserContacts st User {userId} =
contactNames <- map fromOnly <$> DB.query db "SELECT local_display_name FROM contacts WHERE user_id = ?" (Only userId)
rights <$> mapM (runExceptT . getContact_ db userId) contactNames
createUserContactLink :: StoreMonad m => SQLiteStore -> UserId -> ConnId -> ConnReqContact -> m ()
createUserContactLink st userId agentConnId cReq =
liftIOEither . checkConstraint SEDuplicateContactLink . withTransaction st $ \db -> do
DB.execute db "INSERT INTO user_contact_links (user_id, conn_req_contact) VALUES (?, ?)" (userId, cReq)
userContactLinkId <- insertedRowId db
Right () <$ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing 0
getUserContactLinkConnections :: StoreMonad m => SQLiteStore -> UserId -> m [Connection]
getUserContactLinkConnections st userId =
liftIOEither . withTransaction st $ \db ->
connections
<$> DB.queryNamed
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id == uc.user_contact_link_id
WHERE c.user_id = :user_id
AND uc.user_id = :user_id
AND uc.local_display_name = ''
|]
[":user_id" := userId]
where
connections [] = Left SEUserContactLinkNotFound
connections rows = Right $ map toConnection rows
deleteUserContactLink :: MonadUnliftIO m => SQLiteStore -> UserId -> m ()
deleteUserContactLink st userId =
liftIO . withTransaction st $ \db -> do
DB.execute
db
[sql|
DELETE FROM connections WHERE connection_id IN (
SELECT connection_id
FROM connections c
JOIN user_contact_links uc USING (user_contact_link_id)
WHERE uc.user_id = ? AND uc.local_display_name = ''
)
|]
(Only userId)
DB.executeNamed
db
[sql|
DELETE FROM display_names
WHERE user_id = :user_id
AND local_display_name in (
SELECT cr.local_display_name
FROM contact_requests cr
JOIN user_contact_links uc USING (user_contact_link_id)
WHERE uc.user_id = :user_id
AND uc.local_display_name = ''
)
|]
[":user_id" := userId]
DB.executeNamed
db
[sql|
DELETE FROM contact_profiles
WHERE contact_profile_id in (
SELECT cr.contact_profile_id
FROM contact_requests cr
JOIN user_contact_links uc USING (user_contact_link_id)
WHERE uc.user_id = :user_id
AND uc.local_display_name = ''
)
|]
[":user_id" := userId]
DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = ''" (Only userId)
getUserContactLink :: StoreMonad m => SQLiteStore -> UserId -> m ConnReqContact
getUserContactLink st userId =
liftIOEither . withTransaction st $ \db ->
connReq
<$> DB.query
db
[sql|
SELECT conn_req_contact
FROM user_contact_links
WHERE user_id = ?
AND local_display_name = ''
|]
(Only userId)
where
connReq [Only cReq] = Right cReq
connReq _ = Left SEUserContactLinkNotFound
createContactRequest :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> InvitationId -> Profile -> m ContactName
createContactRequest st userId userContactId invId Profile {displayName, fullName} =
liftIOEither . withTransaction st $ \db ->
withLocalDisplayName db userId displayName $ \ldn -> do
DB.execute db "INSERT INTO contact_profiles (display_name, full_name) VALUES (?, ?)" (displayName, fullName)
profileId <- insertedRowId db
DB.execute
db
[sql|
INSERT INTO contact_requests
(user_contact_link_id, agent_invitation_id, contact_profile_id, local_display_name, user_id) VALUES (?,?,?,?,?)
|]
(userContactId, invId, profileId, ldn, userId)
pure ldn
getContactRequest :: StoreMonad m => SQLiteStore -> UserId -> ContactName -> m UserContactRequest
getContactRequest st userId localDisplayName =
liftIOEither . withTransaction st $ \db ->
contactReq
<$> DB.query
db
[sql|
SELECT cr.contact_request_id, cr.agent_invitation_id, cr.user_contact_link_id,
c.agent_conn_id, cr.contact_profile_id
FROM contact_requests cr
JOIN connections c USING (user_contact_link_id)
WHERE cr.user_id = ?
AND cr.local_display_name = ?
|]
(userId, localDisplayName)
where
contactReq [(contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, profileId)] =
Right UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, profileId, localDisplayName}
contactReq _ = Left $ SEContactRequestNotFound localDisplayName
deleteContactRequest :: MonadUnliftIO m => SQLiteStore -> UserId -> ContactName -> m ()
deleteContactRequest st userId localDisplayName =
liftIO . withTransaction st $ \db -> do
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
createAcceptedContact :: MonadUnliftIO m => SQLiteStore -> UserId -> ConnId -> ContactName -> Int64 -> m ()
createAcceptedContact st userId agentConnId localDisplayName profileId =
liftIO . withTransaction st $ \db -> do
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
DB.execute db "INSERT INTO contacts (user_id, local_display_name, contact_profile_id) VALUES (?,?,?)" (userId, localDisplayName, profileId)
contactId <- insertedRowId db
void $ createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing 0
getLiveSndFileTransfers :: MonadUnliftIO m => SQLiteStore -> User -> m [SndFileTransfer]
getLiveSndFileTransfers st User {userId} =
liftIO . withTransaction st $ \db -> do
@@ -596,7 +416,7 @@ getPendingConnections st User {userId} =
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact,
conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at
conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, created_at
FROM connections
WHERE user_id = :user_id
AND conn_type = :conn_type
@@ -612,7 +432,7 @@ getContactConnections st userId displayName =
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at
FROM connections c
JOIN contacts cs ON c.contact_id == cs.contact_id
WHERE c.user_id = :user_id
@@ -624,12 +444,12 @@ getContactConnections st userId displayName =
connections [] = Left $ SEContactNotFound displayName
connections rows = Right $ map toConnection rows
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, UTCTime)
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, ConnStatus, ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, UTCTime)
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime)
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime)
toConnection :: ConnectionRow -> Connection
toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt) =
toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, createdAt) =
let entityId = entityId_ connType
in Connection {connId, agentConnId, connLevel, viaContact, connStatus, connType, entityId, createdAt}
where
@@ -638,11 +458,10 @@ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType,
entityId_ ConnMember = groupMemberId
entityId_ ConnRcvFile = rcvFileId
entityId_ ConnSndFile = sndFileId
entityId_ ConnUserContact = userContactLinkId
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
toMaybeConnection (Just connId, Just agentConnId, Just connLevel, viaContact, Just connStatus, Just connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, Just createdAt) =
Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId, createdAt)
toMaybeConnection (Just connId, Just agentConnId, Just connLevel, viaContact, Just connStatus, Just connType, contactId, groupMemberId, sndFileId, rcvFileId, Just createdAt) =
Just $ toConnection (connId, agentConnId, connLevel, viaContact, connStatus, connType, contactId, groupMemberId, sndFileId, rcvFileId, createdAt)
toMaybeConnection _ = Nothing
getMatchingContacts :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> m [Contact]
@@ -747,6 +566,7 @@ mergeContactRecords st userId Contact {contactId = toContactId} Contact {contact
DB.execute db "UPDATE connections SET contact_id = ? WHERE contact_id = ? AND user_id = ?" (toContactId, fromContactId, userId)
DB.execute db "UPDATE connections SET via_contact = ? WHERE via_contact = ? AND user_id = ?" (toContactId, fromContactId, userId)
DB.execute db "UPDATE group_members SET invited_by = ? WHERE invited_by = ? AND user_id = ?" (toContactId, fromContactId, userId)
DB.execute db "UPDATE messages SET contact_id = ? WHERE contact_id = ?" (toContactId, fromContactId)
DB.executeNamed
db
[sql|
@@ -779,7 +599,6 @@ getConnectionChatDirection st User {userId, userContactId} agentConnId =
ConnContact -> ReceivedDirectMessage c . Just <$> getContactRec_ db entId c
ConnSndFile -> SndFileConnection c <$> getConnSndFileTransfer_ db entId c
ConnRcvFile -> RcvFileConnection c <$> ExceptT (getRcvFileTransfer_ db userId entId)
ConnUserContact -> UserContactConnection c <$> getUserContact_ db entId
where
getConnection_ :: DB.Connection -> ExceptT StoreError IO Connection
getConnection_ db = ExceptT $ do
@@ -788,7 +607,7 @@ getConnectionChatDirection st User {userId, userContactId} agentConnId =
db
[sql|
SELECT connection_id, agent_conn_id, conn_level, via_contact,
conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at
conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, created_at
FROM connections
WHERE user_id = ? AND agent_conn_id = ?
|]
@@ -855,21 +674,6 @@ getConnectionChatDirection st User {userId, userContactId} agentConnId =
Just recipientDisplayName -> Right SndFileTransfer {..}
Nothing -> Left $ SESndFileInvalid fileId
sndFileTransfer_ fileId _ _ = Left $ SESndFileNotFound fileId
getUserContact_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UserContact
getUserContact_ db userContactLinkId = ExceptT $ do
userContact_
<$> DB.query
db
[sql|
SELECT conn_req_contact
FROM user_contact_links
WHERE user_id = ? AND user_contact_link_id = ?
|]
(userId, userContactLinkId)
where
userContact_ :: [Only ConnReqContact] -> Either StoreError UserContact
userContact_ [Only cReq] = Right UserContact {userContactLinkId, connReqContact = cReq}
userContact_ _ = Left SEUserContactLinkNotFound
updateConnectionStatus :: MonadUnliftIO m => SQLiteStore -> Connection -> ConnStatus -> m ()
updateConnectionStatus st Connection {connId} connStatus =
@@ -894,14 +698,14 @@ createNewGroup st gVar user groupProfile =
-- | creates a new group record for the group the current user was invited to
createGroupInvitation ::
StoreMonad m => SQLiteStore -> User -> Contact -> GroupInvitation -> m Group
createGroupInvitation st user contact GroupInvitation {fromMember, invitedMember, connRequest, groupProfile} =
createGroupInvitation st user contact GroupInvitation {fromMember, invitedMember, queueInfo, groupProfile} =
liftIOEither . withTransaction st $ \db -> do
let GroupProfile {displayName, fullName} = groupProfile
uId = userId user
withLocalDisplayName db uId displayName $ \localDisplayName -> do
DB.execute db "INSERT INTO group_profiles (display_name, full_name) VALUES (?, ?)" (displayName, fullName)
profileId <- insertedRowId db
DB.execute db "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id) VALUES (?, ?, ?, ?)" (profileId, localDisplayName, connRequest, uId)
DB.execute db "INSERT INTO groups (group_profile_id, local_display_name, inv_queue_info, user_id) VALUES (?, ?, ?, ?)" (profileId, localDisplayName, queueInfo, uId)
groupId <- insertedRowId db
member <- createContactMember_ db user groupId contact fromMember GCHostMember GSMemInvited IBUnknown
membership <- createContactMember_ db user groupId user invitedMember GCUserMember GSMemInvited (IBContact $ contactId contact)
@@ -913,14 +717,14 @@ getGroup :: StoreMonad m => SQLiteStore -> User -> GroupName -> m Group
getGroup st user localDisplayName =
liftIOEither . withTransaction st $ \db -> runExceptT $ fst <$> getGroup_ db user localDisplayName
getGroup_ :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO (Group, Maybe ConnReqInvitation)
getGroup_ :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO (Group, Maybe SMPQueueInfo)
getGroup_ db User {userId, userContactId} localDisplayName = do
(g@Group {groupId}, cReq) <- getGroupRec_
(g@Group {groupId}, qInfo) <- getGroupRec_
allMembers <- getMembers_ groupId
(members, membership) <- liftEither $ splitUserMember_ allMembers
pure (g {members, membership}, cReq)
pure (g {members, membership}, qInfo)
where
getGroupRec_ :: ExceptT StoreError IO (Group, Maybe ConnReqInvitation)
getGroupRec_ :: ExceptT StoreError IO (Group, Maybe SMPQueueInfo)
getGroupRec_ = ExceptT $ do
toGroup
<$> DB.query
@@ -932,10 +736,10 @@ getGroup_ db User {userId, userContactId} localDisplayName = do
WHERE g.local_display_name = ? AND g.user_id = ?
|]
(localDisplayName, userId)
toGroup :: [(Int64, GroupName, Text, Maybe ConnReqInvitation)] -> Either StoreError (Group, Maybe ConnReqInvitation)
toGroup [(groupId, displayName, fullName, cReq)] =
toGroup :: [(Int64, GroupName, Text, Maybe SMPQueueInfo)] -> Either StoreError (Group, Maybe SMPQueueInfo)
toGroup [(groupId, displayName, fullName, qInfo)] =
let groupProfile = GroupProfile {displayName, fullName}
in Right (Group {groupId, localDisplayName, groupProfile, members = undefined, membership = undefined}, cReq)
in Right (Group {groupId, localDisplayName, groupProfile, members = undefined, membership = undefined}, qInfo)
toGroup _ = Left $ SEGroupNotFound localDisplayName
getMembers_ :: Int64 -> ExceptT StoreError IO [GroupMember]
getMembers_ groupId = ExceptT $ do
@@ -947,7 +751,7 @@ getGroup_ db User {userId, userContactId} localDisplayName = do
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id
LEFT JOIN connections c ON c.connection_id = (
@@ -977,32 +781,19 @@ deleteGroup st User {userId} Group {groupId, members, localDisplayName} =
DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName)
getUserGroups :: MonadUnliftIO m => SQLiteStore -> User -> m [Group]
getUserGroups st user@User {userId} =
getUserGroups st user =
liftIO . withTransaction st $ \db -> do
groupNames <- map fromOnly <$> DB.query db "SELECT local_display_name FROM groups WHERE user_id = ?" (Only userId)
groupNames <- liftIO $ map fromOnly <$> DB.query db "SELECT local_display_name FROM groups WHERE user_id = ?" (Only $ userId user)
map fst . rights <$> mapM (runExceptT . getGroup_ db user) groupNames
getUserGroupNames :: MonadUnliftIO m => SQLiteStore -> UserId -> m [(GroupName, Text)]
getUserGroupNames st userId =
liftIO . withTransaction st $ \db ->
DB.query
db
[sql|
SELECT g.local_display_name, p.full_name
FROM groups g
JOIN group_profiles p USING (group_profile_id)
WHERE g.user_id = ?
|]
(Only userId)
getGroupInvitation :: StoreMonad m => SQLiteStore -> User -> GroupName -> m ReceivedGroupInvitation
getGroupInvitation st user localDisplayName =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
(Group {membership, members, groupProfile}, cReq) <- getGroup_ db user localDisplayName
(Group {membership, members, groupProfile}, qInfo) <- getGroup_ db user localDisplayName
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
case (cReq, findFromContact (invitedBy membership) members) of
(Just connRequest, Just fromMember) ->
pure ReceivedGroupInvitation {fromMember, userMember = membership, connRequest, groupProfile}
case (qInfo, findFromContact (invitedBy membership) members) of
(Just queueInfo, Just fromMember) ->
pure ReceivedGroupInvitation {fromMember, userMember = membership, queueInfo, groupProfile}
_ -> throwError SEGroupInvitationNotFound
where
findFromContact :: InvitedBy -> [GroupMember] -> Maybe GroupMember
@@ -1148,14 +939,14 @@ saveIntroInvitation st reMember toMember introInv = do
WHERE group_member_intro_id = :intro_id
|]
[ ":intro_status" := GMIntroInvReceived,
":group_queue_info" := groupConnReq introInv,
":direct_queue_info" := directConnReq introInv,
":group_queue_info" := groupQInfo introInv,
":direct_queue_info" := directQInfo introInv,
":intro_id" := introId intro
]
pure intro {introInvitation = Just introInv, introStatus = GMIntroInvReceived}
saveMemberInvitation :: StoreMonad m => SQLiteStore -> GroupMember -> IntroInvitation -> m ()
saveMemberInvitation st GroupMember {groupMemberId} IntroInvitation {groupConnReq, directConnReq} =
saveMemberInvitation st GroupMember {groupMemberId} IntroInvitation {groupQInfo, directQInfo} =
liftIO . withTransaction st $ \db ->
DB.executeNamed
db
@@ -1167,8 +958,8 @@ saveMemberInvitation st GroupMember {groupMemberId} IntroInvitation {groupConnRe
WHERE group_member_id = :group_member_id
|]
[ ":member_status" := GSMemIntroInvited,
":group_queue_info" := groupConnReq,
":direct_queue_info" := directConnReq,
":group_queue_info" := groupQInfo,
":direct_queue_info" := directQInfo,
":group_member_id" := groupMemberId
]
@@ -1184,9 +975,9 @@ getIntroduction_ db reMember toMember = ExceptT $ do
|]
(groupMemberId reMember, groupMemberId toMember)
where
toIntro :: [(Int64, Maybe ConnReqInvitation, Maybe ConnReqInvitation, GroupMemberIntroStatus)] -> Either StoreError GroupMemberIntro
toIntro [(introId, groupConnReq, directConnReq, introStatus)] =
let introInvitation = IntroInvitation <$> groupConnReq <*> directConnReq
toIntro :: [(Int64, Maybe SMPQueueInfo, Maybe SMPQueueInfo, GroupMemberIntroStatus)] -> Either StoreError GroupMemberIntro
toIntro [(introId, groupQInfo, directQInfo, introStatus)] =
let introInvitation = IntroInvitation <$> groupQInfo <*> directQInfo
in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation}
toIntro _ = Left SEIntroNotFound
@@ -1194,7 +985,7 @@ createIntroReMember :: StoreMonad m => SQLiteStore -> User -> Group -> GroupMemb
createIntroReMember st user@User {userId} group@Group {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) groupAgentConnId directAgentConnId =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
Connection {connId = directConnId} <- liftIO $ createContactConnection_ db userId directAgentConnId memberContactId cLevel
Connection {connId = directConnId} <- liftIO $ createConnection_ db userId directAgentConnId memberContactId cLevel
(localDisplayName, contactId, memProfileId) <- ExceptT $ createContact_ db userId directConnId memberProfile (Just groupId)
liftIO $ do
let newMember =
@@ -1216,7 +1007,7 @@ createIntroToMemberContact st userId GroupMember {memberContactId = viaContactId
liftIO . withTransaction st $ \db -> do
let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn
void $ createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel
Connection {connId = directConnId} <- createContactConnection_ db userId directAgentConnId viaContactId cLevel
Connection {connId = directConnId} <- createConnection_ db userId directAgentConnId viaContactId cLevel
contactId <- createMemberContact_ db directConnId
updateMember_ db contactId
where
@@ -1249,7 +1040,17 @@ createIntroToMemberContact st userId GroupMember {memberContactId = viaContactId
[":contact_id" := contactId, ":group_member_id" := groupMemberId]
createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> IO Connection
createMemberConnection_ db userId groupMemberId = createConnection_ db userId ConnMember (Just groupMemberId)
createMemberConnection_ db userId groupMemberId agentConnId viaContact connLevel = do
createdAt <- getCurrentTime
DB.execute
db
[sql|
INSERT INTO connections
(user_id, agent_conn_id, conn_status, conn_type, group_member_id, via_contact, conn_level, created_at) VALUES (?,?,?,?,?,?,?,?);
|]
(userId, agentConnId, ConnNew, ConnMember, groupMemberId, viaContact, connLevel, createdAt)
connId <- insertedRowId db
pure Connection {connId, agentConnId, connType = ConnMember, entityId = Just groupMemberId, viaContact, connLevel, connStatus = ConnNew, createdAt}
createContactMember_ :: IsContact a => DB.Connection -> User -> Int64 -> a -> (MemberId, GroupMemberRole) -> GroupMemberCategory -> GroupMemberStatus -> InvitedBy -> IO GroupMember
createContactMember_ db User {userId, userContactId} groupId userOrContact (memberId, memberRole) memberCategory memberStatus invitedBy = do
@@ -1297,7 +1098,7 @@ getViaGroupMember st User {userId, userContactId} Contact {contactId} =
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
m.invited_by, m.local_display_name, m.contact_id, p.display_name, p.full_name,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at
FROM group_members m
JOIN contacts ct ON ct.contact_id = m.contact_id
JOIN contact_profiles p ON p.contact_profile_id = m.contact_profile_id
@@ -1327,7 +1128,7 @@ getViaGroupContact st User {userId} GroupMember {groupMemberId} =
SELECT
ct.contact_id, ct.local_display_name, p.display_name, p.full_name, ct.via_group,
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact,
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at
c.conn_status, c.conn_type, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.created_at
FROM contacts ct
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
JOIN connections c ON c.connection_id = (
@@ -1370,8 +1171,19 @@ createSndGroupFileTransfer st userId Group {groupId} ms filePath fileSize chunkS
pure fileId
createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection
createSndFileConnection_ db userId fileId agentConnId =
createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing 0
createSndFileConnection_ db userId fileId agentConnId = do
createdAt <- getCurrentTime
let connType = ConnSndFile
connStatus = ConnNew
DB.execute
db
[sql|
INSERT INTO connections
(user_id, snd_file_id, agent_conn_id, conn_status, conn_type, created_at) VALUES (?,?,?,?,?,?)
|]
(userId, fileId, agentConnId, connStatus, connType, createdAt)
connId <- insertedRowId db
pure Connection {connId, agentConnId, connType, entityId = Just fileId, viaContact = Nothing, connLevel = 0, connStatus, createdAt}
updateSndFileStatus :: MonadUnliftIO m => SQLiteStore -> SndFileTransfer -> FileStatus -> m ()
updateSndFileStatus st SndFileTransfer {fileId, connId} status =
@@ -1424,19 +1236,19 @@ deleteSndFileChunks st SndFileTransfer {fileId, connId} =
DB.execute db "DELETE FROM snd_file_chunks WHERE file_id = ? AND connection_id = ?" (fileId, connId)
createRcvFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> Contact -> FileInvitation -> Integer -> m RcvFileTransfer
createRcvFileTransfer st userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq} chunkSize =
createRcvFileTransfer st userId Contact {contactId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileQInfo} chunkSize =
liftIO . withTransaction st $ \db -> do
DB.execute db "INSERT INTO files (user_id, contact_id, file_name, file_size, chunk_size) VALUES (?, ?, ?, ?, ?)" (userId, contactId, fileName, fileSize, chunkSize)
fileId <- insertedRowId db
DB.execute db "INSERT INTO rcv_files (file_id, file_status, file_queue_info) VALUES (?, ?, ?)" (fileId, FSNew, fileConnReq)
DB.execute db "INSERT INTO rcv_files (file_id, file_status, file_queue_info) VALUES (?, ?, ?)" (fileId, FSNew, fileQInfo)
pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, senderDisplayName = c, chunkSize}
createRcvGroupFileTransfer :: MonadUnliftIO m => SQLiteStore -> UserId -> GroupMember -> FileInvitation -> Integer -> m RcvFileTransfer
createRcvGroupFileTransfer st userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq} chunkSize =
createRcvGroupFileTransfer st userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileQInfo} chunkSize =
liftIO . withTransaction st $ \db -> do
DB.execute db "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size) VALUES (?, ?, ?, ?, ?)" (userId, groupId, fileName, fileSize, chunkSize)
fileId <- insertedRowId db
DB.execute db "INSERT INTO rcv_files (file_id, file_status, file_queue_info, group_member_id) VALUES (?, ?, ?, ?)" (fileId, FSNew, fileConnReq, groupMemberId)
DB.execute db "INSERT INTO rcv_files (file_id, file_status, file_queue_info, group_member_id) VALUES (?, ?, ?, ?)" (fileId, FSNew, fileQInfo, groupMemberId)
pure RcvFileTransfer {fileId, fileInvitation = f, fileStatus = RFSNew, senderDisplayName = c, chunkSize}
getRcvFileTransfer :: StoreMonad m => SQLiteStore -> UserId -> Int64 -> m RcvFileTransfer
@@ -1463,10 +1275,10 @@ getRcvFileTransfer_ db userId fileId =
(userId, fileId)
where
rcvFileTransfer ::
[(FileStatus, ConnReqInvitation, String, Integer, Integer, Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe Int64, Maybe ConnId)] ->
[(FileStatus, SMPQueueInfo, String, Integer, Integer, Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe Int64, Maybe ConnId)] ->
Either StoreError RcvFileTransfer
rcvFileTransfer [(fileStatus', fileConnReq, fileName, fileSize, chunkSize, contactName_, memberName_, filePath_, connId_, agentConnId_)] =
let fileInv = FileInvitation {fileName, fileSize, fileConnReq}
rcvFileTransfer [(fileStatus', fileQInfo, fileName, fileSize, chunkSize, contactName_, memberName_, filePath_, connId_, agentConnId_)] =
let fileInv = FileInvitation {fileName, fileSize, fileQInfo}
fileInfo = (filePath_, connId_, agentConnId_)
in case contactName_ <|> memberName_ of
Nothing -> Left $ SERcvFileInvalid fileId
@@ -1602,116 +1414,6 @@ getSndFileTransfers_ db userId fileId =
Just recipientDisplayName -> Right SndFileTransfer {..}
Nothing -> Left $ SESndFileInvalid fileId
getOnboarding :: MonadUnliftIO m => SQLiteStore -> UserId -> m Onboarding
getOnboarding st userId =
liftIO . withTransaction st $ \db -> do
contactsCount <- intQuery db "SELECT COUNT(contact_id) FROM contacts WHERE user_id = ? AND is_user = 0"
createdGroups <- headOrZero <$> DB.query db "SELECT COUNT(g.group_id) FROM groups g JOIN group_members m WHERE g.user_id = ? AND m.member_status = ?" (userId, GSMemCreator)
membersCount <- headOrZero <$> DB.query db "SELECT COUNT(group_member_id) FROM group_members WHERE user_id = ? AND (member_status = ? OR member_status = ?)" (userId, GSMemConnected, GSMemComplete)
filesSentCount <- intQuery db "SELECT COUNT(s.file_id) FROM snd_files s JOIN files f USING (file_id) WHERE f.user_id = ?"
addressCount <- intQuery db "SELECT COUNT(user_contact_link_id) FROM user_contact_links WHERE user_id = ?"
pure $ Onboarding {..}
where
intQuery :: DB.Connection -> DB.Query -> IO Int
intQuery db q = headOrZero <$> DB.query db q (Only userId)
headOrZero [] = 0
headOrZero (n : _) = fromOnly n
createNewMessage :: MonadUnliftIO m => SQLiteStore -> NewMessage -> m MessageId
createNewMessage st newMsg =
liftIO . withTransaction st $ \db ->
createNewMessage_ db newMsg
createSndMsgDelivery :: MonadUnliftIO m => SQLiteStore -> SndMsgDelivery -> MessageId -> m ()
createSndMsgDelivery st sndMsgDelivery messageId =
liftIO . withTransaction st $ \db -> do
msgDeliveryId <- createSndMsgDelivery_ db sndMsgDelivery messageId
createMsgDeliveryEvent_ db msgDeliveryId MDSSndAgent
createNewMessageAndRcvMsgDelivery :: MonadUnliftIO m => SQLiteStore -> NewMessage -> RcvMsgDelivery -> m ()
createNewMessageAndRcvMsgDelivery st newMsg rcvMsgDelivery =
liftIO . withTransaction st $ \db -> do
messageId <- createNewMessage_ db newMsg
msgDeliveryId <- createRcvMsgDelivery_ db rcvMsgDelivery messageId
createMsgDeliveryEvent_ db msgDeliveryId MDSRcvAgent
createSndMsgDeliveryEvent :: StoreMonad m => SQLiteStore -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDSnd -> m ()
createSndMsgDeliveryEvent st connId agentMsgId sndMsgDeliveryStatus =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
msgDeliveryId <- ExceptT $ getMsgDeliveryId_ db connId agentMsgId
liftIO $ createMsgDeliveryEvent_ db msgDeliveryId sndMsgDeliveryStatus
createRcvMsgDeliveryEvent :: StoreMonad m => SQLiteStore -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDRcv -> m ()
createRcvMsgDeliveryEvent st connId agentMsgId rcvMsgDeliveryStatus =
liftIOEither . withTransaction st $ \db -> runExceptT $ do
msgDeliveryId <- ExceptT $ getMsgDeliveryId_ db connId agentMsgId
liftIO $ createMsgDeliveryEvent_ db msgDeliveryId rcvMsgDeliveryStatus
createNewMessage_ :: DB.Connection -> NewMessage -> IO MessageId
createNewMessage_ db NewMessage {direction, chatMsgEventType, msgBody} = do
createdAt <- getCurrentTime
DB.execute
db
[sql|
INSERT INTO messages
(msg_sent, chat_msg_event, msg_body, created_at) VALUES (?,?,?,?);
|]
(direction, chatMsgEventType, msgBody, createdAt)
insertedRowId db
createSndMsgDelivery_ :: DB.Connection -> SndMsgDelivery -> MessageId -> IO Int64
createSndMsgDelivery_ db SndMsgDelivery {connId, agentMsgId} messageId = do
chatTs <- getCurrentTime
DB.execute
db
[sql|
INSERT INTO msg_deliveries
(message_id, connection_id, agent_msg_id, agent_msg_meta, chat_ts)
VALUES (?,?,?,NULL,?);
|]
(messageId, connId, agentMsgId, chatTs)
insertedRowId db
createRcvMsgDelivery_ :: DB.Connection -> RcvMsgDelivery -> MessageId -> IO Int64
createRcvMsgDelivery_ db RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} messageId = do
DB.execute
db
[sql|
INSERT INTO msg_deliveries
(message_id, connection_id, agent_msg_id, agent_msg_meta, chat_ts)
VALUES (?,?,?,?,?);
|]
(messageId, connId, agentMsgId, msgMetaJson agentMsgMeta, snd $ broker agentMsgMeta)
insertedRowId db
createMsgDeliveryEvent_ :: DB.Connection -> Int64 -> MsgDeliveryStatus d -> IO ()
createMsgDeliveryEvent_ db msgDeliveryId msgDeliveryStatus = do
createdAt <- getCurrentTime
DB.execute
db
[sql|
INSERT INTO msg_delivery_events
(msg_delivery_id, delivery_status, created_at) VALUES (?,?,?);
|]
(msgDeliveryId, msgDeliveryStatus, createdAt)
getMsgDeliveryId_ :: DB.Connection -> Int64 -> AgentMsgId -> IO (Either StoreError Int64)
getMsgDeliveryId_ db connId agentMsgId =
toMsgDeliveryId
<$> DB.query
db
[sql|
SELECT msg_delivery_id
FROM msg_deliveries m
WHERE m.connection_id = ? AND m.agent_msg_id == ?
LIMIT 1;
|]
(connId, agentMsgId)
where
toMsgDeliveryId :: [Only Int64] -> Either StoreError Int64
toMsgDeliveryId [Only msgDeliveryId] = Right msgDeliveryId
toMsgDeliveryId _ = Left $ SENoMsgDelivery connId agentMsgId
-- | Saves unique local display name based on passed displayName, suffixed with _N if required.
-- This function should be called inside transaction.
withLocalDisplayName :: forall a. DB.Connection -> UserId -> Text -> (Text -> IO a) -> IO (Either StoreError a)
@@ -1771,9 +1473,6 @@ data StoreError
= SEDuplicateName
| SEContactNotFound ContactName
| SEContactNotReady ContactName
| SEDuplicateContactLink
| SEUserContactLinkNotFound
| SEContactRequestNotFound ContactName
| SEGroupNotFound GroupName
| SEGroupWithoutUser
| SEDuplicateGroupMember
@@ -1788,5 +1487,4 @@ data StoreError
| SEIntroNotFound
| SEUniqueID
| SEInternal ByteString
| SENoMsgDelivery Int64 AgentMsgId
deriving (Show, Exception)

View File

@@ -1,27 +1,18 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Simplex.Chat.Types where
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Int (Int64)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1)
import Data.Time.Clock (UTCTime)
import Data.Type.Equality
import Data.Typeable (Typeable)
import Database.SQLite.Simple (ResultError (..), SQLData (..))
import Database.SQLite.Simple.FromField (FieldParser, FromField (..), returnError)
@@ -29,9 +20,8 @@ import Database.SQLite.Simple.Internal (Field (..))
import Database.SQLite.Simple.Ok (Ok (Ok))
import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, ConnectionMode (..), ConnectionRequest, InvitationId, MsgMeta (..), serializeMsgIntegrity)
import Simplex.Messaging.Agent.Protocol (ConnId, SMPQueueInfo)
import Simplex.Messaging.Agent.Store.SQLite (fromTextField_)
import Simplex.Messaging.Protocol (MsgBody)
class IsContact a where
contactId' :: a -> Int64
@@ -67,28 +57,9 @@ data Contact = Contact
}
deriving (Eq, Show)
contactConn :: Contact -> Connection
contactConn = activeConn
contactConnId :: Contact -> ConnId
contactConnId Contact {activeConn = Connection {agentConnId}} = agentConnId
data UserContact = UserContact
{ userContactLinkId :: Int64,
connReqContact :: ConnReqContact
}
deriving (Eq, Show)
data UserContactRequest = UserContactRequest
{ contactRequestId :: Int64,
agentInvitationId :: InvitationId,
userContactLinkId :: Int64,
agentContactConnId :: ConnId,
localDisplayName :: ContactName,
profileId :: Int64
}
deriving (Eq, Show)
type ContactName = Text
type GroupName = Text
@@ -125,14 +96,14 @@ instance FromJSON GroupProfile
data GroupInvitation = GroupInvitation
{ fromMember :: (MemberId, GroupMemberRole),
invitedMember :: (MemberId, GroupMemberRole),
connRequest :: ConnReqInvitation,
queueInfo :: SMPQueueInfo,
groupProfile :: GroupProfile
}
deriving (Eq, Show)
data IntroInvitation = IntroInvitation
{ groupConnReq :: ConnReqInvitation,
directConnReq :: ConnReqInvitation
{ groupQInfo :: SMPQueueInfo,
directQInfo :: SMPQueueInfo
}
deriving (Eq, Show)
@@ -145,7 +116,7 @@ memberInfo m = MemberInfo (memberId m) (memberRole m) (memberProfile m)
data ReceivedGroupInvitation = ReceivedGroupInvitation
{ fromMember :: GroupMember,
userMember :: GroupMember,
connRequest :: ConnReqInvitation,
queueInfo :: SMPQueueInfo,
groupProfile :: GroupProfile
}
deriving (Eq, Show)
@@ -165,9 +136,6 @@ data GroupMember = GroupMember
}
deriving (Eq, Show)
memberConn :: GroupMember -> Maybe Connection
memberConn = activeConn
memberConnId :: GroupMember -> Maybe ConnId
memberConnId GroupMember {activeConn} = case activeConn of
Just Connection {agentConnId} -> Just agentConnId
@@ -348,7 +316,7 @@ data SndFileTransfer = SndFileTransfer
data FileInvitation = FileInvitation
{ fileName :: String,
fileSize :: Integer,
fileConnReq :: ConnReqInvitation
fileQInfo :: SMPQueueInfo
}
deriving (Eq, Show)
@@ -404,10 +372,6 @@ serializeFileStatus = \case
data RcvChunkStatus = RcvChunkOk | RcvChunkFinal | RcvChunkDuplicate | RcvChunkError
deriving (Eq, Show)
type ConnReqInvitation = ConnectionRequest 'CMInvitation
type ConnReqContact = ConnectionRequest 'CMContact
data Connection = Connection
{ connId :: Int64,
agentConnId :: ConnId,
@@ -415,7 +379,7 @@ data Connection = Connection
viaContact :: Maybe Int64,
connType :: ConnType,
connStatus :: ConnStatus,
entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID
entityId :: Maybe Int64, -- contact, group member or file ID
createdAt :: UTCTime
}
deriving (Eq, Show)
@@ -462,7 +426,7 @@ serializeConnStatus = \case
ConnReady -> "ready"
ConnDeleted -> "deleted"
data ConnType = ConnContact | ConnMember | ConnSndFile | ConnRcvFile | ConnUserContact
data ConnType = ConnContact | ConnMember | ConnSndFile | ConnRcvFile
deriving (Eq, Show)
instance FromField ConnType where fromField = fromTextField_ connTypeT
@@ -475,7 +439,6 @@ connTypeT = \case
"member" -> Just ConnMember
"snd_file" -> Just ConnSndFile
"rcv_file" -> Just ConnRcvFile
"user_contact" -> Just ConnUserContact
_ -> Nothing
serializeConnType :: ConnType -> Text
@@ -484,7 +447,6 @@ serializeConnType = \case
ConnMember -> "member"
ConnSndFile -> "snd_file"
ConnRcvFile -> "rcv_file"
ConnUserContact -> "user_contact"
data NewConnection = NewConnection
{ agentConnId :: ByteString,
@@ -533,133 +495,3 @@ serializeIntroStatus = \case
GMIntroReConnected -> "re-con"
GMIntroToConnected -> "to-con"
GMIntroConnected -> "con"
data Onboarding = Onboarding
{ contactsCount :: Int,
createdGroups :: Int,
membersCount :: Int,
filesSentCount :: Int,
addressCount :: Int
}
data NewMessage = NewMessage
{ direction :: MsgDirection,
chatMsgEventType :: Text,
msgBody :: MsgBody
}
type MessageId = Int64
data MsgDirection = MDRcv | MDSnd
data SMsgDirection (d :: MsgDirection) where
SMDRcv :: SMsgDirection 'MDRcv
SMDSnd :: SMsgDirection 'MDSnd
instance TestEquality SMsgDirection where
testEquality SMDRcv SMDRcv = Just Refl
testEquality SMDSnd SMDSnd = Just Refl
testEquality _ _ = Nothing
class MsgDirectionI (d :: MsgDirection) where
msgDirection :: SMsgDirection d
instance MsgDirectionI 'MDRcv where msgDirection = SMDRcv
instance MsgDirectionI 'MDSnd where msgDirection = SMDSnd
instance ToField MsgDirection where toField = toField . msgDirectionInt
msgDirectionInt :: MsgDirection -> Int
msgDirectionInt = \case
MDRcv -> 0
MDSnd -> 1
msgDirectionIntP :: Int -> Maybe MsgDirection
msgDirectionIntP = \case
0 -> Just MDRcv
1 -> Just MDSnd
_ -> Nothing
data SndMsgDelivery = SndMsgDelivery
{ connId :: Int64,
agentMsgId :: AgentMsgId
}
data RcvMsgDelivery = RcvMsgDelivery
{ connId :: Int64,
agentMsgId :: AgentMsgId,
agentMsgMeta :: MsgMeta
}
data MsgMetaJ = MsgMetaJ
{ integrity :: Text,
rcvId :: Int64,
rcvTs :: UTCTime,
serverId :: Text,
serverTs :: UTCTime,
sndId :: Int64
}
deriving (Generic, Eq, Show)
instance ToJSON MsgMetaJ where toEncoding = J.genericToEncoding J.defaultOptions
instance FromJSON MsgMetaJ
msgMetaToJson :: MsgMeta -> MsgMetaJ
msgMetaToJson MsgMeta {integrity, recipient = (rcvId, rcvTs), broker = (serverId, serverTs), sender = (sndId, _)} =
MsgMetaJ
{ integrity = (decodeLatin1 . serializeMsgIntegrity) integrity,
rcvId,
rcvTs,
serverId = (decodeLatin1 . B64.encode) serverId,
serverTs,
sndId
}
msgMetaJson :: MsgMeta -> Text
msgMetaJson = decodeLatin1 . LB.toStrict . J.encode . msgMetaToJson
data MsgDeliveryStatus (d :: MsgDirection) where
MDSRcvAgent :: MsgDeliveryStatus 'MDRcv
MDSRcvAcknowledged :: MsgDeliveryStatus 'MDRcv
MDSSndPending :: MsgDeliveryStatus 'MDSnd
MDSSndAgent :: MsgDeliveryStatus 'MDSnd
MDSSndSent :: MsgDeliveryStatus 'MDSnd
MDSSndReceived :: MsgDeliveryStatus 'MDSnd
MDSSndRead :: MsgDeliveryStatus 'MDSnd
data AMsgDeliveryStatus = forall d. AMDS (SMsgDirection d) (MsgDeliveryStatus d)
instance (Typeable d, MsgDirectionI d) => FromField (MsgDeliveryStatus d) where
fromField = fromTextField_ msgDeliveryStatusT'
instance ToField (MsgDeliveryStatus d) where toField = toField . serializeMsgDeliveryStatus
serializeMsgDeliveryStatus :: MsgDeliveryStatus d -> Text
serializeMsgDeliveryStatus = \case
MDSRcvAgent -> "rcv_agent"
MDSRcvAcknowledged -> "rcv_acknowledged"
MDSSndPending -> "snd_pending"
MDSSndAgent -> "snd_agent"
MDSSndSent -> "snd_sent"
MDSSndReceived -> "snd_received"
MDSSndRead -> "snd_read"
msgDeliveryStatusT :: Text -> Maybe AMsgDeliveryStatus
msgDeliveryStatusT = \case
"rcv_agent" -> Just $ AMDS SMDRcv MDSRcvAgent
"rcv_acknowledged" -> Just $ AMDS SMDRcv MDSRcvAcknowledged
"snd_pending" -> Just $ AMDS SMDSnd MDSSndPending
"snd_agent" -> Just $ AMDS SMDSnd MDSSndAgent
"snd_sent" -> Just $ AMDS SMDSnd MDSSndSent
"snd_received" -> Just $ AMDS SMDSnd MDSSndReceived
"snd_read" -> Just $ AMDS SMDSnd MDSSndRead
_ -> Nothing
msgDeliveryStatusT' :: forall d. MsgDirectionI d => Text -> Maybe (MsgDeliveryStatus d)
msgDeliveryStatusT' s =
msgDeliveryStatusT s >>= \(AMDS d st) ->
case testEquality d (msgDirection @d) of
Just Refl -> Just st
_ -> Nothing

View File

@@ -1,6 +1,5 @@
module Simplex.Chat.Util where
import Control.Monad (when)
import Data.ByteString.Char8 (ByteString)
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8With)
@@ -13,8 +12,5 @@ safeDecodeUtf8 = decodeUtf8With onError
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM ba t f = ba >>= \b -> if b then t else f
whenM :: Monad m => m Bool -> m () -> m ()
whenM ba a = ba >>= (`when` a)
unlessM :: Monad m => m Bool -> m () -> m ()
unlessM b = ifM b $ pure ()

View File

@@ -8,26 +8,14 @@
module Simplex.Chat.View
( printToView,
showInvitation,
showSentConfirmation,
showSentInvitation,
showInvalidConnReq,
showChatError,
showContactDeleted,
showContactGroups,
showContactsList,
showContactConnected,
showContactDisconnected,
showContactAnotherClient,
showContactSubscribed,
showContactSubError,
showUserContactLinkCreated,
showUserContactLinkDeleted,
showUserContactLink,
showReceivedContactRequest,
showAcceptingContactRequest,
showContactRequestRejected,
showUserContactLinkSubscribed,
showUserContactLinkSubError,
showGroupSubscribed,
showGroupEmpty,
showGroupRemoved,
@@ -67,7 +55,6 @@ module Simplex.Chat.View
showLeftMemberUser,
showLeftMember,
showGroupMembers,
showGroupsList,
showContactsMerged,
showUserProfile,
showUserProfileUpdated,
@@ -85,7 +72,7 @@ import Data.ByteString.Char8 (ByteString)
import Data.Composition ((.:), (.:.))
import Data.Function (on)
import Data.Int (Int64)
import Data.List (groupBy, intersperse, sort, sortOn)
import Data.List (groupBy, intersperse, sortOn)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (DiffTime, UTCTime)
@@ -100,28 +87,12 @@ import Simplex.Chat.Terminal (printToTerminal)
import Simplex.Chat.Types
import Simplex.Chat.Util (safeDecodeUtf8)
import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Protocol as SMP
import System.Console.ANSI.Types
type ChatReader m = (MonadUnliftIO m, MonadReader ChatController m)
showInvitation :: ChatReader m => ConnReqInvitation -> m ()
showInvitation = printToView . connReqInvitation_
showSentConfirmation :: ChatReader m => m ()
showSentConfirmation = printToView ["confirmation sent!"]
showSentInvitation :: ChatReader m => m ()
showSentInvitation = printToView ["connection request sent!"]
showInvalidConnReq :: ChatReader m => m ()
showInvalidConnReq =
printToView
[ "Connection link is invalid!",
"Possibly, it was created in a newer version (to check version: " <> highlight' "/v" <> ")",
"To upgrade (Linux/Mac):",
"curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash"
]
showInvitation :: ChatReader m => SMPQueueInfo -> m ()
showInvitation = printToView . invitation
showChatError :: ChatReader m => ChatError -> m ()
showChatError = printToView . chatError
@@ -132,9 +103,6 @@ showContactDeleted = printToView . contactDeleted
showContactGroups :: ChatReader m => ContactName -> [GroupName] -> m ()
showContactGroups = printToView .: contactGroups
showContactsList :: ChatReader m => [Contact] -> m ()
showContactsList = printToView . contactsList
showContactConnected :: ChatReader m => Contact -> m ()
showContactConnected = printToView . contactConnected
@@ -150,30 +118,6 @@ showContactSubscribed = printToView . contactSubscribed
showContactSubError :: ChatReader m => ContactName -> ChatError -> m ()
showContactSubError = printToView .: contactSubError
showUserContactLinkCreated :: ChatReader m => ConnReqContact -> m ()
showUserContactLinkCreated = printToView . userContactLinkCreated
showUserContactLinkDeleted :: ChatReader m => m ()
showUserContactLinkDeleted = printToView userContactLinkDeleted
showUserContactLink :: ChatReader m => ConnReqContact -> m ()
showUserContactLink = printToView . userContactLink
showReceivedContactRequest :: ChatReader m => ContactName -> Profile -> m ()
showReceivedContactRequest = printToView .: receivedContactRequest
showAcceptingContactRequest :: ChatReader m => ContactName -> m ()
showAcceptingContactRequest = printToView . acceptingContactRequest
showContactRequestRejected :: ChatReader m => ContactName -> m ()
showContactRequestRejected = printToView . contactRequestRejected
showUserContactLinkSubscribed :: ChatReader m => m ()
showUserContactLinkSubscribed = printToView ["Your address is active! To show: " <> highlight' "/sa"]
showUserContactLinkSubError :: ChatReader m => ChatError -> m ()
showUserContactLinkSubError = printToView . userContactLinkSubError
showGroupSubscribed :: ChatReader m => GroupName -> m ()
showGroupSubscribed = printToView . groupSubscribed
@@ -297,9 +241,6 @@ showLeftMember = printToView .: leftMember
showGroupMembers :: ChatReader m => Group -> m ()
showGroupMembers = printToView . groupMembers
showGroupsList :: ChatReader m => [(GroupName, Text)] -> m ()
showGroupsList = printToView . groupsList
showContactsMerged :: ChatReader m => Contact -> Contact -> m ()
showContactsMerged = printToView .: contactsMerged
@@ -315,13 +256,13 @@ showContactUpdated = printToView .: contactUpdated
showMessageError :: ChatReader m => Text -> Text -> m ()
showMessageError = printToView .: messageError
connReqInvitation_ :: ConnReqInvitation -> [StyledString]
connReqInvitation_ cReq =
[ "pass this invitation link to your contact (via another channel): ",
invitation :: SMPQueueInfo -> [StyledString]
invitation qInfo =
[ "pass this invitation to your contact (via another channel): ",
"",
(plain . serializeConnReq') cReq,
(plain . serializeSmpQueueInfo) qInfo,
"",
"and ask them to connect: " <> highlight' "/c <invitation_link_above>"
"and ask them to connect: " <> highlight' "/c <invitation_above>"
]
contactDeleted :: ContactName -> [StyledString]
@@ -335,11 +276,6 @@ contactGroups c gNames = [ttyContact c <> ": contact cannot be deleted, it is a
ttyGroups [g] = ttyGroup g
ttyGroups (g : gs) = ttyGroup g <> ", " <> ttyGroups gs
contactsList :: [Contact] -> [StyledString]
contactsList =
let ldn = T.toLower . (localDisplayName :: Contact -> ContactName)
in map ttyFullContact . sortOn ldn
contactConnected :: Contact -> [StyledString]
contactConnected ct = [ttyFullContact ct <> ": contact is connected"]
@@ -355,48 +291,6 @@ contactSubscribed c = [ttyContact c <> ": connected to server"]
contactSubError :: ContactName -> ChatError -> [StyledString]
contactSubError c e = [ttyContact c <> ": contact error " <> sShow e]
userContactLinkCreated :: ConnReqContact -> [StyledString]
userContactLinkCreated = connReqContact_ "Your new chat address is created!"
userContactLinkDeleted :: [StyledString]
userContactLinkDeleted =
[ "Your chat address is deleted - accepted contacts will remain connected.",
"To create a new chat address use " <> highlight' "/ad"
]
userContactLink :: ConnReqContact -> [StyledString]
userContactLink = connReqContact_ "Your chat address:"
connReqContact_ :: StyledString -> ConnReqContact -> [StyledString]
connReqContact_ intro cReq =
[ intro,
"",
(plain . serializeConnReq') cReq,
"",
"Anybody can send you contact requests with: " <> highlight' "/c <contact_link_above>",
"to show it again: " <> highlight' "/sa",
"to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)"
]
receivedContactRequest :: ContactName -> Profile -> [StyledString]
receivedContactRequest c Profile {fullName} =
[ ttyFullName c fullName <> " wants to connect to you!",
"to accept: " <> highlight ("/ac " <> c),
"to reject: " <> highlight ("/rc " <> c) <> " (the sender will NOT be notified)"
]
acceptingContactRequest :: ContactName -> [StyledString]
acceptingContactRequest c = [ttyContact c <> ": accepting contact request..."]
contactRequestRejected :: ContactName -> [StyledString]
contactRequestRejected c = [ttyContact c <> ": contact request rejected"]
userContactLinkSubError :: ChatError -> [StyledString]
userContactLinkSubError e =
[ "user address error: " <> sShow e,
"to delete your address: " <> highlight' "/da"
]
groupSubscribed :: GroupName -> [StyledString]
groupSubscribed g = [ttyGroup g <> ": connected to server(s)"]
@@ -492,12 +386,6 @@ groupMembers Group {membership, members} = map groupMember . filter (not . remov
GSMemCreator -> "created group"
_ -> ""
groupsList :: [(GroupName, Text)] -> [StyledString]
groupsList [] = ["you have no groups!", "to create: " <> highlight' "/g <name>"]
groupsList gs = map groupNames $ sort gs
where
groupNames (displayName, fullName) = ttyGroup displayName <> optFullName displayName fullName
contactsMerged :: Contact -> Contact -> [StyledString]
contactsMerged _to@Contact {localDisplayName = c1} _from@Contact {localDisplayName = c2} =
[ "contact " <> ttyContact c2 <> " is merged into " <> ttyContact c1,
@@ -737,13 +625,8 @@ chatError = \case
SEFileNotFound fileId -> fileNotFound fileId
SESndFileNotFound fileId -> fileNotFound fileId
SERcvFileNotFound fileId -> fileNotFound fileId
SEDuplicateContactLink -> ["you already have chat address, to show: " <> highlight' "/sa"]
SEUserContactLinkNotFound -> ["no chat address, to create: " <> highlight' "/ad"]
SEContactRequestNotFound c -> ["no contact request from " <> ttyContact c]
e -> ["chat db error: " <> sShow e]
ChatErrorAgent err -> case err of
SMP SMP.AUTH -> ["error: this connection is deleted"]
e -> ["smp agent error: " <> sShow e]
ChatErrorAgent e -> ["smp agent error: " <> sShow e]
ChatErrorMessage e -> ["chat message error: " <> sShow e]
where
fileNotFound fileId = ["file " <> sShow fileId <> " not found"]

View File

@@ -40,10 +40,10 @@ extra-deps:
- simple-logger-0.1.0@sha256:be8ede4bd251a9cac776533bae7fb643369ebd826eb948a9a18df1a8dd252ff8,1079
- sqlite-simple-0.4.18.0@sha256:3ceea56375c0a3590c814e411a4eb86943f8d31b93b110ca159c90689b6b39e5,3002
- terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
- simplexmq-0.5.0@sha256:3d9b84d986df7409839c19455a376722837d52a646cb5d136037cadd0b5a4b76,7828
- simplexmq-0.4.1@sha256:3a1bc40d85e4e398458e5b9b79757e0af4fe27b8ef44eb3157f7f1e07412a8e8,7640
# - ../simplexmq
# - github: simplex-chat/simplexmq
# commit: f15067cf6891bda3216c6cf6d2e3ecdba9b7269e
# commit: 35e6593581e68f7b444e0f8f4fb6a2e2cc59a5ea
#
# extra-deps: []

View File

@@ -33,13 +33,13 @@ testDBPrefix :: FilePath
testDBPrefix = "tests/tmp/test"
serverPort :: ServiceName
serverPort = "5001"
serverPort = "5000"
opts :: ChatOpts
opts =
ChatOpts
{ dbFile = undefined,
smpServers = ["localhost:5001"]
smpServers = ["localhost:5000"]
}
termSettings :: VirtualTerminalSettings

View File

@@ -46,10 +46,6 @@ chatTests = do
it "sender cancelled file transfer" testFileSndCancel
it "recipient cancelled file transfer" testFileRcvCancel
it "send and receive file to group" testGroupFileTransfer
describe "user contact link" $ do
it "should create and connect via contact link" testUserContactLink
it "should reject contact and delete contact link" testRejectContactAndDeleteUserContact
it "should delete connection requests when contact link deleted" testDeleteConnectionRequests
testAddContact :: IO ()
testAddContact =
@@ -58,7 +54,6 @@ testAddContact =
alice ##> "/c"
inv <- getInvitation alice
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
concurrently_
(bob <## "alice (Alice): contact is connected")
(alice <## "bob (Bob): contact is connected")
@@ -70,7 +65,6 @@ testAddContact =
alice ##> "/c"
inv' <- getInvitation alice
bob ##> ("/c " <> inv')
bob <## "confirmation sent!"
concurrently_
(bob <## "alice_1 (Alice): contact is connected")
(alice <## "bob_1 (Bob): contact is connected")
@@ -134,20 +128,6 @@ testGroup =
(alice <# "#team cath> hey")
(bob <# "#team cath> hey")
bob <##> cath
-- list groups
alice ##> "/gs"
alice <## "#team"
-- list group members
alice ##> "/ms team"
alice
<### [ "alice (Alice): owner, you, created group",
"bob (Bob): admin, invited, connected",
"cath (Catherine): admin, invited, connected"
]
-- list contacts
alice ##> "/cs"
alice <## "bob (Bob)"
alice <## "cath (Catherine)"
-- remove member
bob ##> "/rm team cath"
concurrentlyN_
@@ -550,73 +530,6 @@ testGroupFileTransfer =
cath <## "completed receiving file 1 (test.jpg) from alice"
]
testUserContactLink :: IO ()
testUserContactLink = testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
alice ##> "/ad"
cLink <- getContactLink alice True
bob ##> ("/c " <> cLink)
alice <#? bob
alice ##> "/ac bob"
alice <## "bob: accepting contact request..."
concurrently_
(bob <## "alice (Alice): contact is connected")
(alice <## "bob (Bob): contact is connected")
alice <##> bob
cath ##> ("/c " <> cLink)
alice <#? cath
alice ##> "/ac cath"
alice <## "cath: accepting contact request..."
concurrently_
(cath <## "alice (Alice): contact is connected")
(alice <## "cath (Catherine): contact is connected")
alice <##> cath
testRejectContactAndDeleteUserContact :: IO ()
testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
alice ##> "/ad"
cLink <- getContactLink alice True
bob ##> ("/c " <> cLink)
alice <#? bob
alice ##> "/rc bob"
alice <## "bob: contact request rejected"
(bob </)
alice ##> "/sa"
cLink' <- getContactLink alice False
cLink' `shouldBe` cLink
alice ##> "/da"
alice <## "Your chat address is deleted - accepted contacts will remain connected."
alice <## "To create a new chat address use /ad"
cath ##> ("/c " <> cLink)
cath <## "error: this connection is deleted"
testDeleteConnectionRequests :: IO ()
testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $
\alice bob cath -> do
alice ##> "/ad"
cLink <- getContactLink alice True
bob ##> ("/c " <> cLink)
alice <#? bob
cath ##> ("/c " <> cLink)
alice <#? cath
alice ##> "/da"
alice <## "Your chat address is deleted - accepted contacts will remain connected."
alice <## "To create a new chat address use /ad"
alice ##> "/ad"
cLink' <- getContactLink alice True
bob ##> ("/c " <> cLink')
-- same names are used here, as they were released at /da
alice <#? bob
cath ##> ("/c " <> cLink')
alice <#? cath
startFileTransfer :: TestCC -> TestCC -> IO ()
startFileTransfer alice bob = do
alice #> "/f @bob ./tests/fixtures/test.jpg"
@@ -643,7 +556,6 @@ connectUsers cc1 cc2 = do
cc1 ##> "/c"
inv <- getInvitation cc1
cc2 ##> ("/c " <> inv)
cc2 <## "confirmation sent!"
concurrently_
(cc2 <## (name1 <> ": contact is connected"))
(cc1 <## (name2 <> ": contact is connected"))
@@ -736,15 +648,6 @@ cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line
(</) :: TestCC -> Expectation
(</) cc = timeout 500000 (getTermLine cc) `shouldReturn` Nothing
(<#?) :: TestCC -> TestCC -> Expectation
cc1 <#? cc2 = do
name <- userName cc2
sName <- showName cc2
cc2 <## "connection request sent!"
cc1 <## (sName <> " wants to connect to you!")
cc1 <## ("to accept: /ac " <> name)
cc1 <## ("to reject: /rc " <> name <> " (the sender will NOT be notified)")
dropTime :: String -> String
dropTime msg = case splitAt 6 msg of
([m, m', ':', s, s', ' '], text) ->
@@ -756,20 +659,9 @@ getTermLine = atomically . readTQueue . termQ
getInvitation :: TestCC -> IO String
getInvitation cc = do
cc <## "pass this invitation link to your contact (via another channel):"
cc <## "pass this invitation to your contact (via another channel):"
cc <## ""
inv <- getTermLine cc
cc <## ""
cc <## "and ask them to connect: /c <invitation_link_above>"
cc <## "and ask them to connect: /c <invitation_above>"
pure inv
getContactLink :: TestCC -> Bool -> IO String
getContactLink cc created = do
cc <## if created then "Your new chat address is created!" else "Your chat address:"
cc <## ""
link <- getTermLine cc
cc <## ""
cc <## "Anybody can send you contact requests with: /c <contact_link_above>"
cc <## "to show it again: /sa"
cc <## "to delete it: /da (accepted contacts will remain connected)"
pure link

View File

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 136 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 MiB

View File

@@ -1,48 +0,0 @@
set -eu
APP_NAME="simplex-chat"
TARGET_DIR="$HOME/.local/bin"
PLATFORM="$(uname)"
if [ $PLATFORM == "Darwin" ]; then
PLATFORM="macos-x86-64"
elif [ $PLATFORM == "Linux" ]; then
PLATFORM="ubuntu-20_04-x86-64"
else
echo "Scripted installation on your platform is not supported."
echo "See compiled binaries in the latest release: https://github.com/simplex-chat/simplex-chat/releases/latest"
exit 1
fi
[ ! -d $TARGET_DIR ] && mkdir -p $TARGET_DIR
if [ -n "$(command -v curl)" ]; then
curl -L -o $TARGET_DIR/$APP_NAME "https://github.com/$APP_NAME/$APP_NAME/releases/latest/download/$APP_NAME-$PLATFORM"
elif [ -n "$(command -v wget)" ]; then
wget -O $TARGET_DIR/$APP_NAME "https://github.com/$APP_NAME/$APP_NAME/releases/latest/download/$APP_NAME-$PLATFORM"
else
echo "Cannot download simplex-chat - please install curl or wget"
exit 1
fi
chmod +x $TARGET_DIR/$APP_NAME
echo "$APP_NAME installed sucesfully!"
if [ -z "$(command -v simplex-chat)" ]; then
if [ -n "$($SHELL -c 'echo $ZSH_VERSION')" ]; then
SHELL_FILE="$HOME/.zshrc"
elif [ -n "$($SHELL -c 'echo $BASH_VERSION')" ]; then
SHELL_FILE="$HOME/.bashrc"
else
echo "Unknown shell - cannot add simplex-chat folder to PATH"
echo "Please add $TARGET_DIR to PATH variable"
echo "Or you can run simplex-chat via full path: $TARGET_DIR/simplex-chat"
fi
if [ -n "$SHELL_FILE" ]; then
echo "export PATH=\$PATH:$TARGET_DIR" >> $SHELL_FILE
echo "Source your $SHELL_FILE or open a new shell and type simplex-chat to run it"
fi
else
echo "Type simplex-chat in your terminal to run it"
fi

View File

@@ -1,29 +0,0 @@
CREATE TABLE user_contact_links (
user_contact_link_id INTEGER PRIMARY KEY,
conn_req_contact BLOB NOT NULL,
local_display_name TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
user_id INTEGER NOT NULL REFERENCES users,
UNIQUE (user_id, local_display_name)
);
CREATE TABLE contact_requests (
contact_request_id INTEGER PRIMARY KEY,
user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links
ON UPDATE CASCADE ON DELETE CASCADE,
agent_invitation_id BLOB NOT NULL,
contact_profile_id INTEGER REFERENCES contact_profiles
DEFERRABLE INITIALLY DEFERRED, -- NULL if it's an incognito profile
local_display_name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
user_id INTEGER NOT NULL REFERENCES users,
FOREIGN KEY (user_id, local_display_name)
REFERENCES display_names (user_id, local_display_name)
ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED,
UNIQUE (user_id, local_display_name),
UNIQUE (user_id, contact_profile_id)
);
ALTER TABLE connections ADD user_contact_link_id INTEGER
REFERENCES user_contact_links ON DELETE RESTRICT;

View File

@@ -1,200 +0,0 @@
DROP TABLE event_body_parts;
DROP TABLE contact_profile_events;
DROP TABLE group_profile_events;
DROP TABLE group_event_parents;
DROP TABLE group_events;
DROP TABLE message_events;
DROP TABLE message_content;
DROP TABLE events;
DROP TABLE messages;
-- all message events as received or sent, append only
-- maps to message deliveries as one-to-many for group messages
CREATE TABLE messages (
message_id INTEGER PRIMARY KEY,
msg_sent INTEGER NOT NULL, -- 0 for received, 1 for sent
chat_msg_event TEXT NOT NULL, -- message event type (the constructor of ChatMsgEvent)
msg_body BLOB, -- agent message body as received or sent
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- message deliveries communicated with the agent, append only
CREATE TABLE msg_deliveries (
msg_delivery_id INTEGER PRIMARY KEY,
message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, -- non UNIQUE for group messages
connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE,
agent_msg_id INTEGER, -- internal agent message ID (NULL while pending)
agent_msg_meta TEXT, -- JSON with timestamps etc. sent in MSG, NULL for sent
chat_ts TEXT NOT NULL DEFAULT (datetime('now')), -- broker_ts for received, created_at for sent
UNIQUE (connection_id, agent_msg_id)
);
-- TODO recovery for received messages with "rcv_agent" status - acknowledge to agent
-- changes of messagy delivery status, append only
CREATE TABLE msg_delivery_events (
msg_delivery_event_id INTEGER PRIMARY KEY,
msg_delivery_id INTEGER NOT NULL REFERENCES msg_deliveries ON DELETE CASCADE, -- non UNIQUE for multiple events per msg delivery
delivery_status TEXT NOT NULL, -- see MsgDeliveryStatus for allowed values
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (msg_delivery_id, delivery_status)
);
CREATE VIEW direct_messages AS
SELECT
ct.local_display_name AS contact,
m.message_id AS message_id,
m.msg_sent AS msg_sent,
m.chat_msg_event AS chat_msg_event,
m.msg_body AS msg_body,
md.msg_delivery_id AS delivery_id,
datetime(md.chat_ts) AS chat_dt,
md.agent_msg_meta AS msg_meta,
mde.delivery_status AS delivery_status,
datetime(mde.created_at) AS delivery_status_dt
FROM messages m
JOIN msg_deliveries md ON md.message_id = m.message_id
JOIN (
SELECT msg_delivery_id, MAX(created_at) MaxDate
FROM msg_delivery_events
GROUP BY msg_delivery_id
) MaxDates ON MaxDates.msg_delivery_id = md.msg_delivery_id
JOIN msg_delivery_events mde ON mde.msg_delivery_id = MaxDates.msg_delivery_id
AND mde.created_at = MaxDates.MaxDate
JOIN connections c ON c.connection_id = md.connection_id
JOIN contacts ct ON ct.contact_id = c.contact_id
ORDER BY chat_dt DESC;
CREATE VIEW direct_messages_plain AS
SELECT
dm.contact AS contact,
dm.msg_sent AS msg_sent,
dm.msg_body AS msg_body,
dm.chat_dt AS chat_dt
FROM direct_messages dm
WHERE dm.chat_msg_event = 'x.msg.new';
CREATE VIEW group_messages AS
SELECT
g.local_display_name AS group_name,
gm.local_display_name AS contact,
m.message_id AS message_id,
m.msg_sent AS msg_sent,
m.chat_msg_event AS chat_msg_event,
m.msg_body AS msg_body,
md.msg_delivery_id AS delivery_id,
datetime(md.chat_ts) AS chat_dt,
md.agent_msg_meta AS msg_meta,
mde.delivery_status AS delivery_status,
datetime(mde.created_at) AS delivery_status_dt
FROM messages m
JOIN msg_deliveries md ON md.message_id = m.message_id
JOIN (
SELECT msg_delivery_id, MAX(created_at) MaxDate
FROM msg_delivery_events
GROUP BY msg_delivery_id
) MaxDates ON MaxDates.msg_delivery_id = md.msg_delivery_id
JOIN msg_delivery_events mde ON mde.msg_delivery_id = MaxDates.msg_delivery_id
AND mde.created_at = MaxDates.MaxDate
JOIN connections c ON c.connection_id = md.connection_id
JOIN group_members gm ON gm.group_member_id = c.group_member_id
JOIN groups g ON g.group_id = gm.group_id
ORDER BY chat_dt DESC;
CREATE VIEW group_messages_plain AS
SELECT
gm.group_name AS group_name,
(CASE WHEN gm.msg_sent = 0 THEN gm.contact ELSE gm.group_name END) AS contact,
gm.msg_sent AS msg_sent,
gm.msg_body AS msg_body,
gm.chat_dt AS chat_dt
FROM group_messages gm
JOIN (
SELECT message_id, MIN(delivery_id) MinDeliveryId
FROM group_messages
GROUP BY message_id
) Deduplicated ON Deduplicated.message_id = gm.message_id
AND Deduplicated.MinDeliveryId = gm.delivery_id
WHERE gm.chat_msg_event = 'x.msg.new';
CREATE VIEW all_messages (
group_name,
contact,
message_id,
msg_sent,
chat_msg_event,
msg_body,
delivery_id,
chat_dt,
msg_meta,
delivery_status,
delivery_status_dt
) AS
SELECT * FROM (
SELECT NULL AS group_name, * FROM direct_messages
UNION
SELECT * FROM group_messages
)
ORDER BY chat_dt DESC;
CREATE VIEW all_messages_plain (
group_name,
contact,
msg_sent,
msg_body,
chat_dt
) AS
SELECT * FROM (
SELECT NULL AS group_name, * FROM direct_messages_plain
UNION
SELECT * FROM group_messages_plain
)
ORDER BY chat_dt DESC;
-- TODO group message parents and chat items not to be implemented in current scope
-- CREATE TABLE group_message_parents (
-- group_message_parent_id INTEGER PRIMARY KEY,
-- message_id INTEGER NOT NULL REFERENCES group_messages (event_id),
-- parent_group_member_id INTEGER REFERENCES group_members (group_member_id), -- can be NULL if parent_member_id is incorrect
-- parent_member_id BLOB, -- shared member ID, unique per group
-- parent_message_id INTEGER REFERENCES messages (message_id) ON DELETE CASCADE, -- can be NULL if received message references another message that's not received yet
-- parent_chat_msg_id INTEGER NOT NULL,
-- parent_msg_body_hash BLOB NOT NULL
-- );
-- CREATE INDEX group_event_parents_parent_chat_event_id_index
-- ON group_message_parents (parent_member_id, parent_chat_msg_id);
-- CREATE TABLE chat_items ( -- mutable chat_items presented to user
-- chat_item_id INTEGER PRIMARY KEY,
-- chat_msg_id INTEGER NOT NULL, -- sent as part of the message that created the item
-- item_deleted INTEGER NOT NULL, -- 1 for deleted
-- item_type TEXT NOT NULL,
-- item_text TEXT NOT NULL, -- textual representation
-- item_props TEXT NOT NULL -- JSON
-- );
-- CREATE TABLE direct_chat_items (
-- chat_item_id INTEGER NOT NULL UNIQUE REFERENCES chat_items ON DELETE CASCADE,
-- contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE RESTRICT,
-- item_sent INTEGER -- 1 for sent, 0 for received
-- );
-- CREATE TABLE group_chat_items (
-- chat_item_id INTEGER NOT NULL UNIQUE REFERENCES chat_items ON DELETE CASCADE,
-- group_member_id INTEGER REFERENCES group_members ON DELETE RESTRICT, -- NULL for sent
-- group_id INTEGER NOT NULL REFERENCES groups ON DELETE RESTRICT
-- );
-- CREATE TABLE chat_item_content (
-- chat_item_content_id INTEGER PRIMARY KEY,
-- chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
-- content_type TEXT NOT NULL,
-- content_size INTEGER NOT NULL,
-- content BLOB NOT NULL
-- );
-- CREATE TABLE chat_item_messages (
-- message_id INTEGER NOT NULL UNIQUE REFERENCES messages,
-- chat_item_id INTEGER NOT NULL REFERENCES chat_items
-- );

View File

@@ -0,0 +1,44 @@
include: package:lints/recommended.yaml
linter:
rules:
prefer_single_quotes: true
constant_identifier_names: false
always_declare_return_types: true
avoid_dynamic_calls: true
avoid_empty_else: true
avoid_relative_lib_imports: true
avoid_shadowing_type_parameters: true
avoid_slow_async_io: true
avoid_types_as_parameter_names: true
await_only_futures: true
camel_case_extensions: true
camel_case_types: true
cancel_subscriptions: true
curly_braces_in_flow_control_structures: true
directives_ordering: true
empty_catches: true
hash_and_equals: true
iterable_contains_unrelated_type: true
list_remove_unrelated_type: true
no_adjacent_strings_in_list: true
no_duplicate_case_values: true
package_api_docs: true
package_prefixed_library_names: true
prefer_generic_function_type_aliases: true
prefer_is_empty: true
prefer_is_not_empty: true
prefer_iterable_whereType: true
prefer_typing_uninitialized_variables: true
sort_child_properties_last: true
test_types_in_equals: true
throw_in_finally: true
unawaited_futures: true
unnecessary_import: true
unnecessary_null_aware_assignments: true
unnecessary_statements: true
unnecessary_type_check: true
unrelated_type_equality_checks: true
unsafe_html: true
use_full_hex_values_for_flutter_colors: true
valid_regexps: true

23
packages/repo_support/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# Intellij
*.idea/
*.iml
# VS code
.vscode
# Local
.local/

View File

@@ -0,0 +1,8 @@
# repo_support
App flutter utils repo support
```
dart run tool/run_ci.dart
```

View File

@@ -0,0 +1,11 @@
name: simplex_repo_support
description: SimpleX build tools
version: 0.2.0
publish_to: none
environment:
sdk: '>=2.12.0 <3.0.0'
dev_dependencies:
dev_test:

View File

@@ -0,0 +1,14 @@
// ignore_for_file: prefer_double_quotes
import 'package:dev_test/package.dart';
import 'package:path/path.dart';
Future main() async {
for (var dir in [
'simplex_app',
'simplexmq',
'repo_support',
]) {
await packageRunCi(join('..', dir));
}
}

2
packages/simplex_app/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Keep for apps
!pubspec.lock

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 4cc385b4b84ac2f816d939a49ea1f328c4e0b48e
channel: stable
project_type: app

View File

@@ -0,0 +1,16 @@
# simplex_app
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,45 @@
# https://dart.dev/guides/language/analysis-options
include: package:flutter_lints/flutter.yaml
linter:
# https://dart-lang.github.io/linter/lints/index.html.
rules:
prefer_double_quotes: true
always_declare_return_types: true
avoid_dynamic_calls: true
avoid_empty_else: true
avoid_relative_lib_imports: true
avoid_shadowing_type_parameters: true
avoid_slow_async_io: true
avoid_types_as_parameter_names: true
await_only_futures: true
camel_case_extensions: true
camel_case_types: true
cancel_subscriptions: true
curly_braces_in_flow_control_structures: true
directives_ordering: true
empty_catches: true
hash_and_equals: true
iterable_contains_unrelated_type: true
list_remove_unrelated_type: true
no_adjacent_strings_in_list: true
no_duplicate_case_values: true
package_api_docs: true
package_prefixed_library_names: true
prefer_generic_function_type_aliases: true
prefer_is_empty: true
prefer_is_not_empty: true
prefer_iterable_whereType: true
prefer_typing_uninitialized_variables: true
sort_child_properties_last: true
test_types_in_equals: true
throw_in_finally: true
unawaited_futures: true
unnecessary_import: true
unnecessary_null_aware_assignments: true
unnecessary_statements: true
unnecessary_type_check: true
unrelated_type_equality_checks: true
unsafe_html: true
use_full_hex_values_for_flutter_colors: true
valid_regexps: true

13
packages/simplex_app/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,68 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 30
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "chat.simplex.app"
minSdkVersion 16
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.simplex.app">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,41 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.simplex.app">
<application
android:label="simplex_chat"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@@ -0,0 +1,6 @@
package chat.simplex.app
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="chat.simplex.app">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,29 @@
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View File

@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

View File

@@ -0,0 +1 @@
{"packages/cupertino_icons/assets/CupertinoIcons.ttf":["packages/cupertino_icons/assets/CupertinoIcons.ttf"]}

View File

@@ -0,0 +1 @@
[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]},{"family":"packages/cupertino_icons/CupertinoIcons","fonts":[{"asset":"packages/cupertino_icons/assets/CupertinoIcons.ttf"}]}]

Binary file not shown.

33
packages/simplex_app/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1,471 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Some files were not shown because too many files have changed in this diff Show More