diff --git a/INSTALL.md b/INSTALL.md
index 3711da39d..b0dc9c2f6 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -6,7 +6,7 @@ The config file [`tinode.conf`](./server/tinode.conf) contains extensive instruc
1. Visit the [Releases page](https://github.com/tinode/chat/releases/), choose the latest or otherwise the most suitable release. From the list of binaries download the one for your database and platform. Once the binary is downloaded, unpack it to a directory of your choosing, `cd` to that directory.
-2. Make sure your database is running. Make sure it's configured to accept connections from `localhost`. In case of MySQL, Tinode will try to connect as `root` without the password. See notes below (_Building from Source_, section 4) on how to configure Tinode to use a different user or a password. MySQL 5.7 or above is required. MySQL 5.6 or below **will not work**.
+2. Make sure your database is running. Make sure it's configured to accept connections from `localhost`. In case of MySQL, Tinode will try to connect as `root` without the password. In case of PostgreSQL, Tinode will try connect as `postgres` with the password `postgres`. See notes below (_Building from Source_, section 4) on how to configure Tinode to use a different user or a password. MySQL 5.7 or above is required. MySQL 5.6 or below **will not work**. PostgreSQL 13 or above is required. PostgreSQL 12 or below **will not work**.
3. Run the database initializer `init-db` (or `init-db.exe` on Windows):
```
@@ -34,7 +34,8 @@ See [instructions](./docker/README.md)
3. Make sure one of the following databases is installed and running:
* MySQL 5.7 or above. MySQL 5.6 or below **will not work**.
- * MongoDB 4.0 or above.
+ * PostgreSQL 13 or above. PostgreSQL 12 or below **will not work**.
+ * MongoDB 4.2 or above.
* RethinkDB.
4. Fetch, build Tinode server and tinode-db database initializer:
@@ -43,6 +44,11 @@ See [instructions](./docker/README.md)
go install -tags mysql github.com/tinode/chat/server@latest
go install -tags mysql github.com/tinode/chat/tinode-db@latest
```
+ - **PostgreSQL**:
+ ```
+ go install -tags postgres github.com/tinode/chat/server@latest
+ go install -tags postgres github.com/tinode/chat/tinode-db@latest
+ ```
- **MongoDB**:
```
go install -tags mongodb github.com/tinode/chat/server@latest
@@ -55,13 +61,13 @@ See [instructions](./docker/README.md)
```
- **All** (bundle all of the above DB adapters):
```
- go install -tags "mysql rethinkdb mongodb" github.com/tinode/chat/server@latest
- go install -tags "mysql rethinkdb mongodb" github.com/tinode/chat/tinode-db@latest
+ go install -tags "mysql rethinkdb mongodb postgres" github.com/tinode/chat/server@latest
+ go install -tags "mysql rethinkdb mongodb postgres" github.com/tinode/chat/tinode-db@latest
```
The steps above install Tinode binaries at `$GOPATH/bin/`, sorces and supporting files are located at `$GOPATH/pkg/mod/github.com/tinode/chat@vX.XX.X/` where `X.XX.X` is the version you installed, such as `0.19.1`.
- Note the required **`-tags rethinkdb`**, **`-tags mysql`** or **`-tags mongodb`** build option.
+ Note the required **`-tags rethinkdb`**, **`-tags mysql`**, **`-tags mongodb`** or **`-tags postgres`** build option.
You may also optionally define `main.buildstamp` for the server by adding a build option, for instance, with a timestamp:
```
@@ -109,6 +115,10 @@ cd $GOPATH/pkg/mod/github.com/tinode/chat@vX.XX.X
```
mysql.server start
```
+ - **PostgreSQL**: https://www.postgresql.org/docs/current/app-pg-ctl.html
+ ```
+ pg_ctl start
+ ```
- **MongoDB**: https://docs.mongodb.com/manual/administration/install-community/
MongoDB should run as single node replicaset. See https://docs.mongodb.com/manual/administration/replica-set-deployment/
```
@@ -202,6 +212,8 @@ Tinode does not provide ICE servers out of the box. You must install and configu
Once you obtain the ICE TURN/STUN configuration from your service provider, add it to `tinode.conf` section `"webrtc"` - `"ice_servers"` (or `"ice_servers_file"`). Also change `"webrtc"` - `"enabled"` to `true`. An example configuration is provided in the `tinode.conf` for illustration only. IT WILL NOT FUNCTION because it uses dummy values instead of actual server addresses.
+You may find this information useful for choosing the servers: https://gist.github.com/yetithefoot/7592580
+
### Note on Running the Server in Background
diff --git a/README.md b/README.md
index 18299a54e..f74543bce 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
# Tinode Instant Messaging Server
-
Instant messaging server. Backend in pure [Go](http://golang.org) (license [GPL 3.0](http://www.gnu.org/licenses/gpl-3.0.en.html)), client-side binding in Java, Javascript, and Swift, as well as [gRPC](https://grpc.io/) client support for C++, C#, Go, Java, Node, PHP, Python, Ruby, Objective-C, etc. (license [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)). Wire transport is JSON over websocket (long polling is also available) for custom bindings, or [protobuf](https://developers.google.com/protocol-buffers/) with gRPC. Persistent storage is any one of MySQL, MongoDB or [RethinkDB](http://rethinkdb.com/). Other databases can be supported by writing custom adapters.
+
Instant messaging server. Backend in pure [Go](http://golang.org) (license [GPL 3.0](http://www.gnu.org/licenses/gpl-3.0.en.html)), client-side binding in Java, Javascript, and Swift, as well as [gRPC](https://grpc.io/) client support for C++, C#, Go, Java, Node, PHP, Python, Ruby, Objective-C, etc. (license [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)). Wire transport is JSON over websocket (long polling is also available) for custom bindings, or [protobuf](https://developers.google.com/protocol-buffers/) with gRPC.
Tinode is *not* XMPP/Jabber. It is *not* compatible with XMPP. It's meant as a replacement for XMPP. On the surface, it's a lot like open source WhatsApp or Telegram.
-Version 0.20. This is beta-quality software: feature-complete and stable but probably with a few bugs or missing features. Follow [instructions](INSTALL.md) to install and run or use one of the cloud services below. Read [API documentation](docs/API.md).
+This is beta-quality software: feature-complete and stable but probably with a few bugs or missing features. Follow [instructions](INSTALL.md) to install and run or use one of the cloud services below. Read [API documentation](docs/API.md).
@@ -105,14 +105,18 @@ When you register a new account you are asked for an email address to send valid
* Scriptable [command-line tool](tn-cli/) for server administration.
* Performance, reliability and development:
* Sharded clustering with failover.
- * Storage and out of band transfer of large objects like images or document files using local file system or Amazon S3 (other storage systems can be supported with plugins).
+ * Storage and out of band transfer of large objects like images or document files using local file system or Amazon S3 (other storage systems can be supported with [media handlers](https://github.com/tinode/chat/blob/master/server/media/media.go#L21)).
* JSON or [protobuf version 3](https://developers.google.com/protocol-buffers/) wire protocols.
* Bindings for various programming languages:
* Javascript with no external dependencies.
* Java with dependencies on [Jackson](https://github.com/FasterXML/jackson) and [Java-Websocket](https://github.com/TooTallNate/Java-WebSocket). Suitable for Android but with no Android SDK dependencies.
* Swift with no external dependencies.
* C/C++, C#, Go, Python, PHP, Ruby and many other languages using [gRPC](https://grpc.io/docs/languages/).
- * Choice of a database backend: MySQL, RethinkDB, MongoDB.
+ * Choice of a database backend. Other databases can be added with by writing [adapters](server/db/adapter.go).
+ * MySQL
+ * PostgreSQL
+ * MongoDB
+ * [RethinkDB](http://rethinkdb.com/)
### Planned
@@ -139,6 +143,7 @@ All client software has support for [internationalization](docs/translations.md)
| Chinese (traditional) | | ✓ | ✓ | ✓ |
| French | ✓ | ✓ | ✓ | |
| German | | ✓ | ✓ | |
+| Hindi | | | ✓ | |
| Korean | | ✓ | ✓ | |
| Portugese | ✓ | | ✓ | |
| Romanian | | ✓ | | |
@@ -146,7 +151,7 @@ All client software has support for [internationalization](docs/translations.md)
| Spanish | ✓ | ✓ | ✓ | ✓ |
| Vietnamese | ✓ | | | |
-More translations are [welcome](docs/translations.md). Particularly interested in Arabic, Vietnamese, Persian, Indonesian, Portuguese, Hindi, Bengali, Turkish.
+More translations are [welcome](docs/translations.md). In addition to languages listed above, particularly interested in Arabic, Bengali, Indonesian, Urdu, Japanese, Turkish, Vietnamese, Persian.
## Third-Party
@@ -200,6 +205,7 @@ Words 'chat' and 'instant messaging' in Chinese, Russian, Persian and a few othe
* 인스턴트 메신저
* پیام رسان فوری
* تراسل فوري
+* فوری پیغام رسانی
* Nhắn tin tức thời
* anlık mesajlaşma sohbet
* mensageiro instantâneo
@@ -207,3 +213,4 @@ Words 'chat' and 'instant messaging' in Chinese, Russian, Persian and a few othe
* mensajería instantánea
* চ্যাট ইন্সট্যান্ট মেসেজিং
* चैट त्वरित संदेश
+* তাৎক্ষণিক বার্তা আদান প্রদান
diff --git a/build-all.sh b/build-all.sh
index 09088f3e8..fd407366f 100755
--- a/build-all.sh
+++ b/build-all.sh
@@ -14,7 +14,7 @@ goarc=( amd64 arm64 amd64 amd64 )
buildCount=${#goplat[@]}
# Supported database tags
-dbadapters=( mysql mongodb rethinkdb )
+dbadapters=( mysql mongodb rethinkdb postgres )
dbtags=( ${dbadapters[@]} alldbs )
for line in $@; do
@@ -72,7 +72,7 @@ then
cp ./server/static/manifest.json ./releases/tmp/static
cp ./server/static/service-worker.js ./releases/tmp/static
# Create empty FCM client-side config.
- touch ./releases/tmp/static/firebase-init.js
+ echo 'const FIREBASE_INIT = {};' > ./releases/tmp/static/firebase-init.js
else
echo "TinodeWeb not found, skipping"
fi
diff --git a/docker-build.sh b/docker-build.sh
index 7905074ac..dcfc5a4d4 100755
--- a/docker-build.sh
+++ b/docker-build.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-# Build Tinode docker linux/amd64 images.
+# Build Tinode docker linux/amd64 images.
# You may have to install buildx https://docs.docker.com/buildx/working-with-buildx/
# if your build host and target architectures are different (e.g. building on a Mac
# with Apple silicon).
@@ -30,7 +30,7 @@ if [ `uname -m` != 'x86_64' ]; then
buildcmd='buildx build --platform=linux/amd64'
fi
-dbtags=( mysql mongodb rethinkdb alldbs )
+dbtags=( mysql postgres mongodb rethinkdb alldbs )
# Build an images for various DB backends
for dbtag in "${dbtags[@]}"
diff --git a/docker-release.sh b/docker-release.sh
index d434cc494..87dfa0966 100755
--- a/docker-release.sh
+++ b/docker-release.sh
@@ -31,7 +31,7 @@ if [[ ${ver[2]} != *"-"* ]]; then
FULLRELEASE=1
fi
-dbtags=( mysql mongodb rethinkdb alldbs )
+dbtags=( mysql postgres mongodb rethinkdb alldbs )
# Read dockerhub login/password from a separate file
source .dockerhub
diff --git a/docker/README.md b/docker/README.md
index 66aaacf3f..3412c0e51 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -11,17 +11,19 @@ All images are available at https://hub.docker.com/r/tinode/
3. Decide which database backend you want to use: RethinkDB, MySQL or MongoDB. Run the selected database container, attaching it to `tinode-net` network:
- 1. **RethinkDB**: If you've decided to use RethinkDB backend, run the official RethinkDB Docker container:
+ 1. **MySQL**: If you've decided to use MySQL backend, run the official MySQL Docker container:
```
- $ docker run --name rethinkdb --network tinode-net --restart always -d rethinkdb:2.3
+ $ docker run --name mysql --network tinode-net --restart always --env MYSQL_ALLOW_EMPTY_PASSWORD=yes -d mysql:5.7
```
- See [instructions](https://hub.docker.com/_/rethinkdb/) for more options.
+ See [instructions](https://hub.docker.com/_/mysql/) for more options. MySQL 5.7 or above is required.
- 2. **MySQL**: If you've decided to use MySQL backend, run the official MySQL Docker container:
+ 2. **PostgreSQL**: If you've decided to use PostgreSQL backend, run the official PostgreSQL Docker container:
```
- $ docker run --name mysql --network tinode-net --restart always --env MYSQL_ALLOW_EMPTY_PASSWORD=yes -d mysql:5.7
+ $ docker run --name postgres --network tinode-net --restart always --env POSTGRES_PASSWORD=postgres -d postgres:13
```
- See [instructions](https://hub.docker.com/_/mysql/) for more options. MySQL 5.7 or above is required.
+ See [instructions](https://hub.docker.com/_/postgres/) for more options. PostgresSQL 13 or above is required.
+
+ The name `rethinkdb`, `mysql`, `mongodb` or `postgres` in the `--name` assignment is important. It's used by other containers as a database's host name.
3. **MongoDB**: If you've decided to use MongoDB backend, run the official MongoDB Docker container and initialise it as single node replica set (you can change "rs0" if you wish):
```
@@ -34,18 +36,22 @@ All images are available at https://hub.docker.com/r/tinode/
```
See [instructions](https://hub.docker.com/_/mongo/) for more options. MongoDB 4.2 or above is required.
- The name `rethinkdb`, `mysql` or `mongodb` in the `--name` assignment is important. It's used by other containers as a database's host name.
+ 4. **RethinkDB**: If you've decided to use RethinkDB backend, run the official RethinkDB Docker container:
+ ```
+ $ docker run --name rethinkdb --network tinode-net --restart always -d rethinkdb:2.3
+ ```
+ See [instructions](https://hub.docker.com/_/rethinkdb/) for more options.
4. Run the Tinode container for the appropriate database:
- 1. **RethinkDB**:
+ 1. **MySQL**:
```
- $ docker run -p 6060:6060 -d --name tinode-srv --network tinode-net tinode/tinode-rethinkdb:latest
+ $ docker run -p 6060:6060 -d --name tinode-srv --network tinode-net tinode/tinode-mysql:latest
```
- 2. **MySQL**:
+ 2. **PostgreSQL**:
```
- $ docker run -p 6060:6060 -d --name tinode-srv --network tinode-net tinode/tinode-mysql:latest
+ $ docker run -p 6060:6060 -d --name tinode-srv --network tinode-net tinode/tinode-postgresql:latest
```
3. **MongoDB**:
@@ -53,6 +59,11 @@ All images are available at https://hub.docker.com/r/tinode/
$ docker run -p 6060:6060 -d --name tinode-srv --network tinode-net tinode/tinode-mongodb:latest
```
+ 4. **RethinkDB**:
+ ```
+ $ docker run -p 6060:6060 -d --name tinode-srv --network tinode-net tinode/tinode-rethinkdb:latest
+ ```
+
You can also run Tinode with the `tinode/tinode` image (which has all of the above DB adapters compiled in). You will need to specify the database adapter via `STORE_USE_ADAPTER` environment variable. E.g. for `mysql`, the command line will look like
```
$ docker run -p 6060:6060 -d -e STORE_USE_ADAPTER mysql --name tinode-srv --network tinode-net tinode/tinode:latest
@@ -64,9 +75,10 @@ All images are available at https://hub.docker.com/r/tinode/
You may replace `:latest` with a different tag. See all all available tags here:
* [MySQL tags](https://hub.docker.com/r/tinode/tinode-mysql/tags/)
- * [RethinkDB tags](https://hub.docker.com/r/tinode/tinode-rethink/tags/)
+ * [PostgreSQL tags](https://hub.docker.com/r/tinode/tinode-postgresql/tags/) (beta version)
* [MongoDB tags](https://hub.docker.com/r/tinode/tinode-mongodb/tags/)
- * [All bundle tags](https://hub.docker.com/r/tinode/tinode/tags/) (comming soon)
+ * [RethinkDB tags](https://hub.docker.com/r/tinode/tinode-rethink/tags/)
+ * [All bundle tags](https://hub.docker.com/r/tinode/tinode/tags/)
5. Test the installation by pointing your browser to [http://localhost:6060/](http://localhost:6060/).
@@ -189,6 +201,7 @@ You can specify the following environment variables when issuing `docker run` co
| `MEDIA_HANDLER` | string | `fs` | Handler of large files, either `fs` or `s3` |
| `MYSQL_DSN` | string | `'root@tcp(mysql)/tinode'` | MySQL [DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name). |
| `PLUGIN_PYTHON_CHAT_BOT_ENABLED` | bool | `false` | Enable calling into the plugin provided by Python chatbot |
+| `POSTGRES_DSN` | string | `'postgresql://postgres:postgres@localhost:5432/tinode'` | PostgreSQL [DSN](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). |
| `RESET_DB` | bool | `false` | Drop and recreate the database. |
| `SAMPLE_DATA` | string | _see comment →_ | File with sample data to load. Default `data.json` when resetting or generating new DB, none when upgrading. Use `` (empty string) to disable |
| `SMTP_DOMAINS` | string | | White list of email domains; when non-empty, accept registrations with emails from these domains only (email verification). |
diff --git a/docker/docker-compose/README.md b/docker/docker-compose/README.md
index e8bda7dc1..271e68daa 100644
--- a/docker/docker-compose/README.md
+++ b/docker/docker-compose/README.md
@@ -10,8 +10,11 @@ By default, this command starts up a mysql instance, Tinode server(s) and Tinode
Tinode server(s) is(are) configured similar to [Tinode Demo/Sandbox](../../README.md#demosandbox) and
maps its web port to the host's port 6060 (6061, 6062). Tinode exporter(s) serve(s) metrics for InfluxDB.
-Reference configuration for [RethinkDB 2.4.0](https://hub.docker.com/_/rethinkdb?tab=tags) and [MongoDB 4.2.3](https://hub.docker.com/_/mongo?tab=tags) is also available
-in the override files.
+Reference configuration for the following databases is also available in the override files:
+* [PostgreSQL 15.2](https://hub.docker.com/_/postgres/tags)
+* [MongoDB 4.2.3](https://hub.docker.com/_/mongo/tags)
+* [RethinkDB 2.4.2](https://hub.docker.com/_/rethinkdb/tags)
+
## Commands
@@ -20,12 +23,15 @@ To bring up the full stack, you can use the following commands:
* MySql:
- Single-instance setup: `docker-compose -f single-instance.yml up -d`
- Cluster: `docker-compose -f cluster.yml up -d`
-* RethinkDb:
- - Single-instance setup: `docker-compose -f single-instance.yml -f single-instance.rethinkdb.yml up -d`
- - Cluster: `docker-compose -f cluster.yml -f cluster.rethinkdb.yml up -d`
+* PostgreSQL:
+ - Single-instance setup: `docker-compose -f single-instance.yml -f single-instance.postgres.yml up -d`
+ - Cluster: `docker-compose -f cluster.yml -f cluster.postgres.yml up -d`
* MongoDb:
- Single-instance setup: `docker-compose -f single-instance.yml -f single-instance.mongodb.yml up -d`
- Cluster: `docker-compose -f cluster.yml -f cluster.mongodb.yml up -d`
+* RethinkDb:
+ - Single-instance setup: `docker-compose -f single-instance.yml -f single-instance.rethinkdb.yml up -d`
+ - Cluster: `docker-compose -f cluster.yml -f cluster.rethinkdb.yml up -d`
You can run individual/separate components of the setup by providing their names to the `docker-compose` command.
E.g. to start the Tinode server in the single-instance MySql setup,
diff --git a/docker/docker-compose/cluster.mongodb.yml b/docker/docker-compose/cluster.mongodb.yml
index 229fb0277..7a666914e 100644
--- a/docker/docker-compose/cluster.mongodb.yml
+++ b/docker/docker-compose/cluster.mongodb.yml
@@ -1,4 +1,4 @@
-version: '3.7'
+version: '3.8'
x-mongodb-tinode-env-vars: &mongodb-tinode-env-vars
"STORE_USE_ADAPTER": "mongodb"
diff --git a/docker/docker-compose/cluster.postgres.yml b/docker/docker-compose/cluster.postgres.yml
new file mode 100644
index 000000000..dbf9ee871
--- /dev/null
+++ b/docker/docker-compose/cluster.postgres.yml
@@ -0,0 +1,24 @@
+version: '3.8'
+
+x-postgres-tinode-env-vars: &postgres-tinode-env-vars
+ "STORE_USE_ADAPTER": "postgres"
+
+services:
+ db:
+ image: postgres:15.2
+ container_name: postgres
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready"]
+
+ tinode-0:
+ environment:
+ << : *postgres-tinode-env-vars
+ "WAIT_FOR": "postgres:5432"
+
+ tinode-1:
+ environment:
+ << : *postgres-tinode-env-vars
+
+ tinode-2:
+ environment:
+ << : *postgres-tinode-env-vars
diff --git a/docker/docker-compose/cluster.rethinkdb.yml b/docker/docker-compose/cluster.rethinkdb.yml
index 4226ff703..68b0a9315 100644
--- a/docker/docker-compose/cluster.rethinkdb.yml
+++ b/docker/docker-compose/cluster.rethinkdb.yml
@@ -1,11 +1,11 @@
-version: '3.7'
+version: '3.8'
x-rethinkdb-tinode-env-vars: &rethinkdb-tinode-env-vars
"STORE_USE_ADAPTER": "rethinkdb"
services:
db:
- image: rethinkdb:2.4.0
+ image: rethinkdb:2.4.2
container_name: rethinkdb
healthcheck:
test: ["CMD", "curl -f http://localhost:8080/ || exit 1"]
diff --git a/docker/docker-compose/cluster.yml b/docker/docker-compose/cluster.yml
index 85d9edc95..ebd98aa5f 100644
--- a/docker/docker-compose/cluster.yml
+++ b/docker/docker-compose/cluster.yml
@@ -4,7 +4,7 @@
# * 3 Tinode servers
# * 3 exporters
-version: '3.7'
+version: '3.8'
# Base Tinode template.
x-tinode:
diff --git a/docker/docker-compose/single-instance.mongodb.yml b/docker/docker-compose/single-instance.mongodb.yml
index 19d6025ab..9233aa4eb 100644
--- a/docker/docker-compose/single-instance.mongodb.yml
+++ b/docker/docker-compose/single-instance.mongodb.yml
@@ -1,4 +1,4 @@
-version: '3.7'
+version: '3.8'
x-mongodb-tinode-env-vars: &mongodb-tinode-env-vars
"STORE_USE_ADAPTER": "mongodb"
diff --git a/docker/docker-compose/single-instance.postgres.yml b/docker/docker-compose/single-instance.postgres.yml
new file mode 100644
index 000000000..ef7e18481
--- /dev/null
+++ b/docker/docker-compose/single-instance.postgres.yml
@@ -0,0 +1,16 @@
+version: '3.8'
+
+x-postgres-tinode-env-vars: &postgres-tinode-env-vars
+ "STORE_USE_ADAPTER": "postgres"
+
+services:
+ db:
+ image: postgres:15.2
+ container_name: postgres
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready"]
+
+ tinode-0:
+ environment:
+ << : *postgres-tinode-env-vars
+ "WAIT_FOR": "postgres:5432"
diff --git a/docker/docker-compose/single-instance.rethinkdb.yml b/docker/docker-compose/single-instance.rethinkdb.yml
index 5dfa6afdc..f3df41239 100644
--- a/docker/docker-compose/single-instance.rethinkdb.yml
+++ b/docker/docker-compose/single-instance.rethinkdb.yml
@@ -1,4 +1,4 @@
-version: '3.7'
+version: '3.8'
x-rethinkdb-tinode-env-vars: &rethinkdb-tinode-env-vars
"STORE_USE_ADAPTER": "rethinkdb"
diff --git a/docker/docker-compose/single-instance.yml b/docker/docker-compose/single-instance.yml
index 381d4f3e5..d69fed207 100644
--- a/docker/docker-compose/single-instance.yml
+++ b/docker/docker-compose/single-instance.yml
@@ -4,7 +4,7 @@
# * Tinode server
# * Tinode exporters
-version: '3.7'
+version: '3.8'
# Base Tinode template.
x-tinode:
diff --git a/docker/tinode/Dockerfile b/docker/tinode/Dockerfile
index 850ccf716..6645c96bf 100644
--- a/docker/tinode/Dockerfile
+++ b/docker/tinode/Dockerfile
@@ -21,12 +21,11 @@ LABEL version=$VERSION
# Build-time options.
-# Database selector. Builds for RethinkDB by default.
-# Alternatively use
-# `--build-arg TARGET_DB=mysql` to build for MySQL or
-# `--build-arg TARGET_DB=mongodb` to build for MongoDB.
-# `--build-arg TARGET_DB=alldbs` to build a generic Tinode docker image.
-ARG TARGET_DB=rethinkdb
+# Database selector. Builds for MySQL by default.
+# Alternatively use one of: postgres mongodb rethinkdb for a corresponsing
+# DB backend or alldbs to build a generic Tinode docker image, for example:
+# `--build-arg TARGET_DB=postgres` to build for PostgreSQL.
+ARG TARGET_DB=mysql
ENV TARGET_DB=$TARGET_DB
# Runtime options.
@@ -51,6 +50,9 @@ ENV SAMPLE_DATA=$SAMPLE_DATA
# The MySQL DSN connection.
ENV MYSQL_DSN='root@tcp(mysql)/tinode'
+# The PostgreSQL DSN connection.
+ENV POSTGRES_DSN='postgresql://postgres:postgres@localhost:5432/tinode'
+
# Disable chatbot plugin by default.
ENV PLUGIN_PYTHON_CHAT_BOT_ENABLED=false
diff --git a/docker/tinode/config.template b/docker/tinode/config.template
index d07428553..5349c8505 100644
--- a/docker/tinode/config.template
+++ b/docker/tinode/config.template
@@ -63,6 +63,10 @@
"database": "tinode",
"dsn": "$MYSQL_DSN?parseTime=true&collation=utf8mb4_unicode_ci"
},
+ "postgres": {
+ "database": "tinode",
+ "dsn": "$POSTGRES_DSN?sslmode=disable"
+ },
"rethinkdb": {
"database": "tinode",
"addresses": "rethinkdb"
diff --git a/docs/API.md b/docs/API.md
index 22c4d1bf4..e54aa054b 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -628,7 +628,7 @@ Every client to server message contains the main payload described in the sectio
}
```
The `attachments` array lists URLs of files uploaded out of band. Such listing increments use counter of these files. Once the use counter drops to 0, the files will be automatically deleted.
-The `obo` can be set by the `root` user. If the `obo` is set, the server will treat the message as if it came from the sepcified user as opposite to the actual sender.
+The `obo` (On Behalf Of) can be set by the `root` user. If the `obo` is set, the server will treat the message as if it came from the specified user as opposite to the actual sender.
The `authlevel` is supplementary to the `obo` and permits setting custom authentication level for the user. A an `"auth"` level is used if the field is unset.
#### `{hi}`
@@ -668,7 +668,12 @@ acc: {
// default: current user, optional
token: "XMgS...8+BO0=", // string, authentication token to use for the request if the
// session is not authenticated, optional
+ // Temporary authentication parameters for one-off actions, like password reset.
+ tmpscheme: "code", // name of the temp wuth scheme
+ tmpsecret: "XMgS...8+BO0=", // temp auth secret
status: "ok", // change user's status; no default value, optional.
+ authlevel: "auth", // authentication level of the user when UserID is set and not equal
+ // to the current user; Either "", "auth" or "anon"; default: ""
scheme: "basic", // authentication scheme for this account, required;
// "basic" and "anon" are currently supported for account creation.
secret: base64encode("username:password"), // string, base64 encoded secret for the chosen
@@ -876,10 +881,8 @@ The following values are currently defined for the `head` field:
* `attachments`: an array of paths indicating media attached to this message `["/v0/file/s/sJOD_tZDPz0.jpg"]`.
* `auto`: `true` when the message was sent automatically, i.e. by a chatbot or an auto-responder.
* `forwarded`: an indicator that the message is a forwarded message, a unique ID of the original message, `"grp1XUtEhjv6HND:123"`.
- * `hashtags`: an array of hashtags in the message without the leading `#` symbol: `["onehash", "twohash"]`.
* `mentions`: an array of user IDs mentioned (`@alice`) in the message: `["usr1XUtEhjv6HND", "usr2il9suCbuko"]`.
* `mime`: MIME-type of the message content, `"text/x-drafty"`; a `null` or a missing value is interpreted as `"text/plain"`.
- * `priority`: message display priority: hint for the client that the message should be displayed more prominently for a set period of time; only `"high"` is currently defined; `{"level": "high", "expires": "2019-10-06T18:07:30.038Z"}`; `priority` can be set by the topic owner or administrator (`A` permission) only. The `"expires"` qualifier is optional.
* `replace`: an indicator that the message is a correction/replacement for another message, a topic-unique ID of the message being updated/replaced, `":123"`
* `reply`: an indicator that the message is a reply to another message, a unique ID of the original message, `"grp1XUtEhjv6HND:123"`.
* `sender`: a user ID of the sender added by the server when the message is sent on behalf of another user, `"usr1XUtEhjv6HND"`.
@@ -888,8 +891,9 @@ The following values are currently defined for the `head` field:
* `"started"`: call has been initiated and being established
* `"accepted"`: call has been accepted and established
* `"finished"`: previously successfully established call has been ended
- * `"missed"`: call was hung up by the caller or timed out before getting established
+ * `"missed"`: call timed out before getting established
* `"declined"`: call was hung up by the callee before getting established
+ * `"busy"`: the call was declined due to the callee being in another call.
* `"disconnected"`: call was terminated by the server for other reasons (e.g. due to an error)
* `webrtc-duration`: a number representing a video call duration (in milliseconds).
diff --git a/docs/drafty.md b/docs/drafty.md
index 5b496d8ab..ab0b8fe88 100644
--- a/docs/drafty.md
+++ b/docs/drafty.md
@@ -312,8 +312,9 @@ Video call `data` contains current state of the call and its duration:
* `duration`: call duration in milliseconds.
* `state`: surrent call state; supported states:
* `accepted`: a call is established (ongoing).
+ * `busy`: a call cannot be established because the callee is already in another call.
* `finished`: a previously establied call has successfully finished.
- * `disconnected`: the call is dropped for example because of an error.
+ * `disconnected`: the call is dropped, for example because of an error.
* `missed`: the call is missed, i.e. the callee didn't pick up the phone.
* `declined`: the call is declined, i.e. the callee hung up before picking up.
* `incoming`: true if the call is incoming, otherwise the call is outgoing.
diff --git a/docs/faq.md b/docs/faq.md
index e13222656..86e0a3a07 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -18,7 +18,7 @@ Alternatively, you can instruct the docker container to save the logs to a direc
### Q: What are the options for enabling push notifications?
**A**: You can use Tinode Push Gateway (TNPG) or you can use Google FCM:
* _Tinode Push Gateway_ requires minimum configuration changes by sending pushes on behalf of Tinode.
- * _Google FCM_ does not rely on Tinode infrastructure for pushes but requires you to recompile mobile apps (iOS and Android).
+ * _Google FCM_ does not rely on Tinode infrastructure for pushes but requires you to build your own mobile apps (iOS and Android).
### Q: How to setup push notifications with Tinode Push Gateway?
@@ -29,7 +29,9 @@ See detailed instructions [here](../server/push/tnpg/).
### Q: How to setup push notifications with Google FCM?
-**A**: Enabling FCM push notifications requires the following steps:
+**A**: This option requires you to build and release your own mobile apps. If you do not want to do it, the the TNPG option above.
+
+Enabling FCM push notifications requires the following steps:
* enable push sending from the server
* enable receiving pushes in the clients
diff --git a/go.mod b/go.mod
index b018111c1..18ecdddfb 100644
--- a/go.mod
+++ b/go.mod
@@ -1,34 +1,79 @@
module github.com/tinode/chat
-go 1.16
+go 1.20
require (
- cloud.google.com/go v0.102.0 // indirect
- cloud.google.com/go/firestore v1.6.1 // indirect
firebase.google.com/go v3.13.0+incompatible
- github.com/aws/aws-sdk-go v1.33.0
+ github.com/aws/aws-sdk-go v1.44.204
github.com/felixge/httpsnoop v1.0.3 // indirect
- github.com/go-sql-driver/mysql v1.6.0
+ github.com/go-sql-driver/mysql v1.7.0
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0
- github.com/google/go-cmp v0.5.8
- github.com/google/uuid v1.3.0 // indirect
+ github.com/google/go-cmp v0.5.9
github.com/gorilla/handlers v1.5.1
github.com/gorilla/websocket v1.5.0
+ github.com/jackc/pgconn v1.14.0
+ github.com/jackc/pgx/v4 v4.18.1
github.com/jmoiron/sqlx v1.3.5
- github.com/nyaruka/phonenumbers v1.0.75
- github.com/prometheus/client_golang v1.12.2
- github.com/prometheus/common v0.34.0
+ github.com/nyaruka/phonenumbers v1.1.6
+ github.com/prometheus/client_golang v1.14.0
+ github.com/prometheus/common v0.39.0
github.com/tinode/jsonco v1.0.0
github.com/tinode/snowflake v1.0.0
- go.mongodb.org/mongo-driver v1.9.1
- golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
- golang.org/x/net v0.0.0-20220526153639-5463443f8c37
- golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
- golang.org/x/text v0.3.7
- google.golang.org/api v0.81.0
- google.golang.org/genproto v0.0.0-20220526192754-51939a95c655 // indirect
- google.golang.org/grpc v1.46.2
- google.golang.org/protobuf v1.28.0
- gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1
+ go.mongodb.org/mongo-driver v1.11.2
+ golang.org/x/crypto v0.6.0
+ golang.org/x/oauth2 v0.5.0
+ golang.org/x/text v0.7.0
+ google.golang.org/api v0.110.0
+ google.golang.org/grpc v1.53.0
+ google.golang.org/protobuf v1.28.1
+ gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.2
+)
+
+require (
+ cloud.google.com/go v0.110.0 // indirect
+ cloud.google.com/go/compute v1.18.0 // indirect
+ cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/firestore v1.9.0 // indirect
+ cloud.google.com/go/iam v0.12.0 // indirect
+ cloud.google.com/go/longrunning v0.4.1 // indirect
+ cloud.google.com/go/storage v1.29.0 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/golang/protobuf v1.5.2 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
+ github.com/googleapis/gax-go/v2 v2.7.0 // indirect
+ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
+ github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+ github.com/jackc/pgio v1.0.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgproto3/v2 v2.3.2 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgtype v1.14.0 // indirect
+ github.com/jackc/puddle v1.3.0 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/klauspost/compress v1.15.15 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
+ github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
+ github.com/montanaflynn/stats v0.7.0 // indirect
+ github.com/opentracing/opentracing-go v1.2.0 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/prometheus/client_model v0.3.0 // indirect
+ github.com/prometheus/procfs v0.9.0 // indirect
+ github.com/sirupsen/logrus v1.9.0 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.1.2 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
+ go.opencensus.io v0.24.0 // indirect
+ golang.org/x/net v0.7.0 // indirect
+ golang.org/x/sync v0.1.0 // indirect
+ golang.org/x/sys v0.5.0 // indirect
+ golang.org/x/time v0.3.0 // indirect
+ golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
+ google.golang.org/appengine v1.6.7 // indirect
+ google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect
+ gopkg.in/cenkalti/backoff.v2 v2.2.1 // indirect
)
diff --git a/go.sum b/go.sum
index 5f687fd8f..4d0ae6cfb 100644
--- a/go.sum
+++ b/go.sum
@@ -1,79 +1,24 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
-cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
-cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
-cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
-cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
-cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
-cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
-cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
-cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
-cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8=
-cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
-cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
-cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
-cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
-cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc=
-cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw=
-cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
-cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
-cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.22.1 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg=
-cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
+cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
+cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
+cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
+cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
+cloud.google.com/go/iam v0.12.0 h1:DRtTY29b75ciH6Ov1PHb4/iat2CLCvrOm40Q0a6DFpE=
+cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY=
+cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
+cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
+cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
+cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/aws/aws-sdk-go v1.33.0 h1:Bq5Y6VTLbfnJp1IV8EL/qUU5qO1DYHda/zis/sqevkY=
-github.com/aws/aws-sdk-go v1.33.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/aws/aws-sdk-go v1.44.204 h1:7/tPUXfNOHB390A63t6fJIwmlwVQAkAwcbzKsU2/6OQ=
+github.com/aws/aws-sdk-go v1.44.204/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0=
@@ -81,24 +26,15 @@ github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENU
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
-github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
+github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -106,55 +42,29 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
-github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
-github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
+github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
+github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@@ -163,179 +73,181 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
-github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
-github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
-github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
-github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
-github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
-github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
-github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
-github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
+github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
+github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
+github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
-github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
+github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
+github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
+github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
+github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
+github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
+github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
+github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
+github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
+github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
+github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
+github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
+github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
+github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
+github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
+github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
+github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
+github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
+github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
+github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
+github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
+github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
+github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
+github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
+github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
+github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/nyaruka/phonenumbers v1.0.75 h1:OCwKXSjTi6IzuI4gVi8zfY+0s60DQUC6ks8Ll4j0eyU=
-github.com/nyaruka/phonenumbers v1.0.75/go.mod h1:cGaEsOrLjIL0iKGqJR5Rfywy86dSkbApEpXuM9KySNA=
+github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes=
+github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
-github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
-github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
-github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
+github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE=
-github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
-github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI=
+github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y=
+github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
+github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
+github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tinode/jsonco v1.0.0 h1:zVcpjzDvjuA1G+HLrckI5EiiRyq9jgV3x37OQl6e5FE=
@@ -344,464 +256,167 @@ github.com/tinode/snowflake v1.0.0 h1:YciQ9ZKn1TrnvpS8yZErt044XJaxWVtR9aMO9rOZVO
github.com/tinode/snowflake v1.0.0/go.mod h1:5JiaCe3o7QdDeyRcAeZBGVghwRS+ygt2CF/hxmAoptQ=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
-github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w=
-github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
-github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
-github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
-github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
+github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
+github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-go.mongodb.org/mongo-driver v1.9.1 h1:m078y9v7sBItkt1aaoe2YlvWEXcD263e1a4E1fBrJ1c=
-go.mongodb.org/mongo-driver v1.9.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+go.mongodb.org/mongo-driver v1.11.2 h1:+1v2rDQUWNcGW7/7E0Jvdz51V38XXxJfhzbV17aNHCw=
+go.mongodb.org/mongo-driver v1.11.2/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
-golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
-golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
-golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
+golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
-golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
-golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
-google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
-google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
-google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
-google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
-google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
-google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
-google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
-google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
-google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
-google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
-google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
-google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
-google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
-google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
-google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8=
-google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
+golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
+google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
+google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
-google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
-google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220526192754-51939a95c655 h1:56rmjc5LUAanErbiNrY+s/Nd47wDQEJkpqS7i43M1I0=
-google.golang.org/genproto v0.0.0-20220526192754-51939a95c655/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
+google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 h1:EfLuoKW5WfkgVdDy7dTK8qSbH37AX5mj/MFh+bGPz14=
+google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
-google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
+google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
+google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -810,45 +425,32 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
-gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
-gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
+gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.2 h1:tczPZjdz6soV2thcuq1IFOuNLrBUGonFyUXBbIWXWis=
+gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.2/go.mod h1:c7Wo0IjB7JL9B9Avv0UZKorYJCUhiergpj3u1WtGT1E=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/monitoring/exporter/main.go b/monitoring/exporter/main.go
index eacaa10a1..e64105af0 100644
--- a/monitoring/exporter/main.go
+++ b/monitoring/exporter/main.go
@@ -40,13 +40,18 @@ func parseMetricList(list string) []string {
}
// Build version number defined by the compiler:
-// -ldflags "-X main.buildstamp=value_to_assign_to_buildstamp"
+//
+// -ldflags "-X main.buildstamp=value_to_assign_to_buildstamp"
+//
// Reported to clients in response to {hi} message.
// For instance, to define the buildstamp as a timestamp of when the server was built add a
// flag to compiler command line:
-// -ldflags "-X main.buildstamp=`date -u '+%Y%m%dT%H:%M:%SZ'`"
+//
+// -ldflags "-X main.buildstamp=`date -u '+%Y%m%dT%H:%M:%SZ'`"
+//
// or to set it to git tag:
-// -ldflags "-X main.buildstamp=`git describe --tags`"
+//
+// -ldflags "-X main.buildstamp=`git describe --tags`"
var buildstamp = "undef"
func main() {
diff --git a/pbx/model.pb.go b/pbx/model.pb.go
index 978af17a2..6e5b52996 100644
--- a/pbx/model.pb.go
+++ b/pbx/model.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
-// protoc-gen-go v1.28.1
-// protoc v3.21.6
+// protoc-gen-go v1.30.0
+// protoc v3.21.12
// source: model.proto
package pbx
@@ -1242,6 +1242,11 @@ type ClientAcc struct {
Token []byte `protobuf:"bytes,9,opt,name=token,proto3" json:"token,omitempty"`
// Account state: normal ("ok"), suspended
State string `protobuf:"bytes,10,opt,name=state,proto3" json:"state,omitempty"`
+ // AuthLevel
+ AuthLevel AuthLevel `protobuf:"varint,11,opt,name=auth_level,json=authLevel,proto3,enum=pbx.AuthLevel" json:"auth_level,omitempty"`
+ // Temporary auth params for one-off actions like password reset.
+ TmpScheme string `protobuf:"bytes,12,opt,name=tmp_scheme,json=tmpScheme,proto3" json:"tmp_scheme,omitempty"`
+ TmpSecret []byte `protobuf:"bytes,13,opt,name=tmp_secret,json=tmpSecret,proto3" json:"tmp_secret,omitempty"`
}
func (x *ClientAcc) Reset() {
@@ -1346,6 +1351,27 @@ func (x *ClientAcc) GetState() string {
return ""
}
+func (x *ClientAcc) GetAuthLevel() AuthLevel {
+ if x != nil {
+ return x.AuthLevel
+ }
+ return AuthLevel_NONE
+}
+
+func (x *ClientAcc) GetTmpScheme() string {
+ if x != nil {
+ return x.TmpScheme
+ }
+ return ""
+}
+
+func (x *ClientAcc) GetTmpSecret() []byte {
+ if x != nil {
+ return x.TmpSecret
+ }
+ return nil
+}
+
// Login {login} message
type ClientLogin struct {
state protoimpl.MessageState
@@ -2048,6 +2074,7 @@ type ClientMsg struct {
unknownFields protoimpl.UnknownFields
// Types that are assignable to Message:
+ //
// *ClientMsg_Hi
// *ClientMsg_Acc
// *ClientMsg_Login
@@ -2331,6 +2358,7 @@ type TopicDesc struct {
StateAt int64 `protobuf:"varint,13,opt,name=state_at,json=stateAt,proto3" json:"state_at,omitempty"`
Trusted []byte `protobuf:"bytes,14,opt,name=trusted,proto3" json:"trusted,omitempty"`
IsChan bool `protobuf:"varint,17,opt,name=is_chan,json=isChan,proto3" json:"is_chan,omitempty"` // 17!
+ Online bool `protobuf:"varint,18,opt,name=online,proto3" json:"online,omitempty"`
// P2P only: other user's last online timestamp & user agent
LastSeenTime int64 `protobuf:"varint,15,opt,name=last_seen_time,json=lastSeenTime,proto3" json:"last_seen_time,omitempty"`
LastSeenUserAgent string `protobuf:"bytes,16,opt,name=last_seen_user_agent,json=lastSeenUserAgent,proto3" json:"last_seen_user_agent,omitempty"`
@@ -2475,6 +2503,13 @@ func (x *TopicDesc) GetIsChan() bool {
return false
}
+func (x *TopicDesc) GetOnline() bool {
+ if x != nil {
+ return x.Online
+ }
+ return false
+}
+
func (x *TopicDesc) GetLastSeenTime() int64 {
if x != nil {
return x.LastSeenTime
@@ -3238,13 +3273,17 @@ type ServerMsg struct {
unknownFields protoimpl.UnknownFields
// Types that are assignable to Message:
+ //
// *ServerMsg_Ctrl
// *ServerMsg_Data
// *ServerMsg_Pres
// *ServerMsg_Meta
// *ServerMsg_Info
Message isServerMsg_Message `protobuf_oneof:"Message"`
+ // DEPRECATED. Will be removed soon.
// When response is sent to Root, send internal topic name too.
+ //
+ // Deprecated: Marked as deprecated in model.proto.
Topic string `protobuf:"bytes,6,opt,name=topic,proto3" json:"topic,omitempty"`
}
@@ -3322,6 +3361,7 @@ func (x *ServerMsg) GetInfo() *ServerInfo {
return nil
}
+// Deprecated: Marked as deprecated in model.proto.
func (x *ServerMsg) GetTopic() string {
if x != nil {
return x.Topic
@@ -4081,7 +4121,7 @@ var file_model_proto_rawDesc = []byte{
0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12,
0x1e, 0x0a, 0x0a, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x07, 0x20,
0x01, 0x28, 0x08, 0x52, 0x0a, 0x62, 0x61, 0x63, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x22,
- 0x81, 0x02, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x12, 0x0e, 0x0a,
+ 0xee, 0x02, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x12, 0x0e, 0x0a,
0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x17, 0x0a,
0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65,
@@ -4097,420 +4137,428 @@ var file_model_proto_rawDesc = []byte{
0x64, 0x52, 0x04, 0x63, 0x72, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a,
0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74,
- 0x61, 0x74, 0x65, 0x22, 0x8b, 0x01, 0x0a, 0x0b, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x6f,
- 0x67, 0x69, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73,
- 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x65, 0x63,
- 0x72, 0x65, 0x74, 0x12, 0x23, 0x0a, 0x04, 0x63, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x03, 0x28,
- 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x72,
- 0x65, 0x64, 0x52, 0x04, 0x63, 0x72, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x64, 0x6b, 0x5f,
- 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x64, 0x6b, 0x4b, 0x65,
- 0x79, 0x22, 0x89, 0x01, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x12,
- 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
- 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
- 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x2a, 0x0a, 0x09, 0x73, 0x65, 0x74, 0x5f, 0x71, 0x75, 0x65,
- 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53,
- 0x65, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x08, 0x73, 0x65, 0x74, 0x51, 0x75, 0x65, 0x72,
- 0x79, 0x12, 0x2a, 0x0a, 0x09, 0x67, 0x65, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x04,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x47, 0x65, 0x74, 0x51, 0x75,
- 0x65, 0x72, 0x79, 0x52, 0x08, 0x67, 0x65, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x22, 0x49, 0x0a,
- 0x0b, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x61, 0x76, 0x65, 0x12, 0x0e, 0x0a, 0x02,
+ 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6c, 0x65, 0x76, 0x65,
+ 0x6c, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x41, 0x75,
+ 0x74, 0x68, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x4c, 0x65, 0x76,
+ 0x65, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6d, 0x70, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65,
+ 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6d, 0x70, 0x53, 0x63, 0x68, 0x65, 0x6d,
+ 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6d, 0x70, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18,
+ 0x0d, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x74, 0x6d, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
+ 0x22, 0x8b, 0x01, 0x0a, 0x0b, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e,
+ 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64,
+ 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x63, 0x72,
+ 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
+ 0x12, 0x23, 0x0a, 0x04, 0x63, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f,
+ 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x72, 0x65, 0x64, 0x52,
+ 0x04, 0x63, 0x72, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x64, 0x6b, 0x5f, 0x6b, 0x65, 0x79,
+ 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x64, 0x6b, 0x4b, 0x65, 0x79, 0x22, 0x89,
+ 0x01, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x12, 0x0e, 0x0a, 0x02,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05,
0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70,
- 0x69, 0x63, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x6e, 0x73, 0x75, 0x62, 0x18, 0x03, 0x20, 0x01, 0x28,
- 0x08, 0x52, 0x05, 0x75, 0x6e, 0x73, 0x75, 0x62, 0x22, 0xcb, 0x01, 0x0a, 0x09, 0x43, 0x6c, 0x69,
- 0x65, 0x6e, 0x74, 0x50, 0x75, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x17, 0x0a, 0x07,
- 0x6e, 0x6f, 0x5f, 0x65, 0x63, 0x68, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e,
- 0x6f, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x2c, 0x0a, 0x04, 0x68, 0x65, 0x61, 0x64, 0x18, 0x04, 0x20,
- 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
- 0x50, 0x75, 0x62, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x68,
- 0x65, 0x61, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x05,
- 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x1a, 0x37, 0x0a,
- 0x09, 0x48, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
- 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
- 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c,
- 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x56, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
- 0x47, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x69, 0x63, 0x12, 0x2a, 0x0a, 0x09, 0x73, 0x65, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x74, 0x51,
+ 0x75, 0x65, 0x72, 0x79, 0x52, 0x08, 0x73, 0x65, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x2a,
+ 0x0a, 0x09, 0x67, 0x65, 0x74, 0x5f, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x47, 0x65, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79,
+ 0x52, 0x08, 0x67, 0x65, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x22, 0x49, 0x0a, 0x0b, 0x43, 0x6c,
+ 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x61, 0x76, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70,
+ 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12,
+ 0x14, 0x0a, 0x05, 0x75, 0x6e, 0x73, 0x75, 0x62, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05,
+ 0x75, 0x6e, 0x73, 0x75, 0x62, 0x22, 0xcb, 0x01, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
+ 0x50, 0x75, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x23, 0x0a, 0x05, 0x71, 0x75, 0x65,
- 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x47,
- 0x65, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x56,
- 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
- 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74,
- 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69,
- 0x63, 0x12, 0x23, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
- 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52,
- 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x95, 0x02, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e,
- 0x74, 0x44, 0x65, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x27, 0x0a, 0x04, 0x77, 0x68,
- 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43,
- 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x44, 0x65, 0x6c, 0x2e, 0x57, 0x68, 0x61, 0x74, 0x52, 0x04, 0x77,
- 0x68, 0x61, 0x74, 0x12, 0x26, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x5f, 0x73, 0x65, 0x71, 0x18, 0x04,
- 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x71, 0x52, 0x61,
- 0x6e, 0x67, 0x65, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x53, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75,
- 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73,
- 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x04, 0x63, 0x72, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43,
- 0x72, 0x65, 0x64, 0x52, 0x04, 0x63, 0x72, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x72,
- 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x61, 0x72, 0x64, 0x22, 0x3f, 0x0a,
- 0x04, 0x57, 0x68, 0x61, 0x74, 0x12, 0x06, 0x0a, 0x02, 0x58, 0x30, 0x10, 0x00, 0x12, 0x07, 0x0a,
- 0x03, 0x4d, 0x53, 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x4f, 0x50, 0x49, 0x43, 0x10,
- 0x02, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x55, 0x42, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x55, 0x53,
- 0x45, 0x52, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x52, 0x45, 0x44, 0x10, 0x05, 0x22, 0xce,
- 0x01, 0x0a, 0x0a, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x12, 0x14, 0x0a,
- 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
- 0x70, 0x69, 0x63, 0x12, 0x21, 0x0a, 0x04, 0x77, 0x68, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
- 0x0e, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x4e, 0x6f, 0x74, 0x65,
- 0x52, 0x04, 0x77, 0x68, 0x61, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69, 0x64,
- 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x18, 0x0a,
- 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07,
- 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, 0x72, 0x65, 0x61,
- 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x12,
- 0x24, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e,
- 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05,
- 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
- 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22,
- 0x80, 0x01, 0x0a, 0x0b, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x78, 0x74, 0x72, 0x61, 0x12,
- 0x20, 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01,
- 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74,
- 0x73, 0x12, 0x20, 0x0a, 0x0c, 0x6f, 0x6e, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x6c, 0x66, 0x5f, 0x6f,
- 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x6e, 0x42, 0x65, 0x68, 0x61, 0x6c,
- 0x66, 0x4f, 0x66, 0x12, 0x2d, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6c, 0x65, 0x76, 0x65,
- 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x41, 0x75,
- 0x74, 0x68, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x4c, 0x65, 0x76,
- 0x65, 0x6c, 0x22, 0xb2, 0x03, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x73, 0x67,
- 0x12, 0x1f, 0x0a, 0x02, 0x68, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70,
- 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x69, 0x48, 0x00, 0x52, 0x02, 0x68,
- 0x69, 0x12, 0x22, 0x0a, 0x03, 0x61, 0x63, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e,
- 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x48, 0x00,
- 0x52, 0x03, 0x61, 0x63, 0x63, 0x12, 0x28, 0x0a, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x03,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e,
- 0x74, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x48, 0x00, 0x52, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x12,
- 0x22, 0x0a, 0x03, 0x73, 0x75, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70,
- 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x48, 0x00, 0x52, 0x03,
- 0x73, 0x75, 0x62, 0x12, 0x28, 0x0a, 0x05, 0x6c, 0x65, 0x61, 0x76, 0x65, 0x18, 0x05, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c,
- 0x65, 0x61, 0x76, 0x65, 0x48, 0x00, 0x52, 0x05, 0x6c, 0x65, 0x61, 0x76, 0x65, 0x12, 0x22, 0x0a,
- 0x03, 0x70, 0x75, 0x62, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78,
- 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x75, 0x62, 0x48, 0x00, 0x52, 0x03, 0x70, 0x75,
- 0x62, 0x12, 0x22, 0x0a, 0x03, 0x67, 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e,
- 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x47, 0x65, 0x74, 0x48, 0x00,
- 0x52, 0x03, 0x67, 0x65, 0x74, 0x12, 0x22, 0x0a, 0x03, 0x73, 0x65, 0x74, 0x18, 0x08, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53,
- 0x65, 0x74, 0x48, 0x00, 0x52, 0x03, 0x73, 0x65, 0x74, 0x12, 0x22, 0x0a, 0x03, 0x64, 0x65, 0x6c,
- 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69,
- 0x65, 0x6e, 0x74, 0x44, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x03, 0x64, 0x65, 0x6c, 0x12, 0x25, 0x0a,
- 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62,
- 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04,
- 0x6e, 0x6f, 0x74, 0x65, 0x12, 0x26, 0x0a, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x18, 0x0d, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
- 0x45, 0x78, 0x74, 0x72, 0x61, 0x52, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x42, 0x09, 0x0a, 0x07,
- 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x4e, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65,
- 0x72, 0x43, 0x72, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x14, 0x0a,
- 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61,
- 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
- 0x08, 0x52, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x22, 0x9c, 0x04, 0x0a, 0x09, 0x54, 0x6f, 0x70, 0x69,
- 0x63, 0x44, 0x65, 0x73, 0x63, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
- 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74,
- 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f,
- 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65,
- 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61,
- 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x64,
- 0x41, 0x74, 0x12, 0x2b, 0x0a, 0x06, 0x64, 0x65, 0x66, 0x61, 0x63, 0x73, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
- 0x41, 0x63, 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x64, 0x65, 0x66, 0x61, 0x63, 0x73, 0x12,
- 0x21, 0x0a, 0x03, 0x61, 0x63, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70,
- 0x62, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x03, 0x61,
- 0x63, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01,
- 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61,
- 0x64, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64,
- 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20,
- 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x63, 0x76, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x64,
- 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x6c,
- 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x0a, 0x20, 0x01,
- 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72,
- 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69,
- 0x76, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74,
- 0x61, 0x74, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x73, 0x74,
- 0x61, 0x74, 0x65, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64,
- 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x12,
- 0x17, 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08,
- 0x52, 0x06, 0x69, 0x73, 0x43, 0x68, 0x61, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x61, 0x73, 0x74,
- 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x03,
- 0x52, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2f,
- 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x75, 0x73, 0x65, 0x72,
- 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6c, 0x61,
- 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12,
- 0x15, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0xe8, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, 0xf3, 0x03, 0x0a, 0x08, 0x54, 0x6f, 0x70, 0x69, 0x63,
- 0x53, 0x75, 0x62, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61,
- 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64,
- 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41,
- 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18,
- 0x11, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74,
- 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
- 0x52, 0x06, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x21, 0x0a, 0x03, 0x61, 0x63, 0x73, 0x18,
- 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x65,
- 0x73, 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x03, 0x61, 0x63, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x72,
- 0x65, 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65,
- 0x61, 0x64, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x76, 0x5f, 0x69, 0x64, 0x18,
- 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x63, 0x76, 0x49, 0x64, 0x12, 0x16, 0x0a,
- 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70,
- 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64,
- 0x18, 0x10, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x12,
- 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c,
- 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65,
- 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72,
- 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x0a, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x75, 0x63,
- 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f,
- 0x75, 0x63, 0x68, 0x65, 0x64, 0x41, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69,
- 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x15,
- 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
- 0x64, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65,
- 0x65, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c,
- 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x14, 0x6c,
- 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x67,
- 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x53,
- 0x65, 0x65, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x22, 0x4a, 0x0a, 0x09,
- 0x44, 0x65, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x65, 0x6c,
- 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x49, 0x64,
- 0x12, 0x26, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x5f, 0x73, 0x65, 0x71, 0x18, 0x02, 0x20, 0x03, 0x28,
+ 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x17, 0x0a, 0x07, 0x6e, 0x6f, 0x5f,
+ 0x65, 0x63, 0x68, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6e, 0x6f, 0x45, 0x63,
+ 0x68, 0x6f, 0x12, 0x2c, 0x0a, 0x04, 0x68, 0x65, 0x61, 0x64, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x18, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x50, 0x75, 0x62,
+ 0x2e, 0x48, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x68, 0x65, 0x61, 0x64,
+ 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28,
+ 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x1a, 0x37, 0x0a, 0x09, 0x48, 0x65,
+ 0x61, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c,
+ 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a,
+ 0x02, 0x38, 0x01, 0x22, 0x56, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x47, 0x65, 0x74,
+ 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64,
+ 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x23, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x47, 0x65, 0x74, 0x51,
+ 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x56, 0x0a, 0x09, 0x43,
+ 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69,
+ 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x23,
+ 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e,
+ 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x74, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75,
+ 0x65, 0x72, 0x79, 0x22, 0x95, 0x02, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x44, 0x65,
+ 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69,
+ 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x27, 0x0a, 0x04, 0x77, 0x68, 0x61, 0x74, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65,
+ 0x6e, 0x74, 0x44, 0x65, 0x6c, 0x2e, 0x57, 0x68, 0x61, 0x74, 0x52, 0x04, 0x77, 0x68, 0x61, 0x74,
+ 0x12, 0x26, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x5f, 0x73, 0x65, 0x71, 0x18, 0x04, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x71, 0x52, 0x61, 0x6e, 0x67, 0x65,
- 0x52, 0x06, 0x64, 0x65, 0x6c, 0x53, 0x65, 0x71, 0x22, 0xca, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72,
- 0x76, 0x65, 0x72, 0x43, 0x74, 0x72, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x12, 0x0a,
- 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x63, 0x6f, 0x64,
- 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x33, 0x0a, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x18,
- 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76,
- 0x65, 0x72, 0x43, 0x74, 0x72, 0x6c, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x45, 0x6e, 0x74,
- 0x72, 0x79, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x50, 0x61,
- 0x72, 0x61, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76,
- 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
- 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x9a, 0x02, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x44, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x20, 0x0a, 0x0c, 0x66, 0x72,
- 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x0a, 0x66, 0x72, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09,
- 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52,
- 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65,
- 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
- 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71,
- 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64,
- 0x12, 0x2d, 0x0a, 0x04, 0x68, 0x65, 0x61, 0x64, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19,
- 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x2e,
- 0x48, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x68, 0x65, 0x61, 0x64, 0x12,
- 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c,
- 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x1a, 0x37, 0x0a, 0x09, 0x48, 0x65, 0x61,
- 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
- 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
- 0x38, 0x01, 0x22, 0xbf, 0x03, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x65,
- 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x72, 0x63, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x72, 0x63, 0x12, 0x28, 0x0a, 0x04, 0x77, 0x68, 0x61,
- 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65,
- 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x65, 0x73, 0x2e, 0x57, 0x68, 0x61, 0x74, 0x52, 0x04, 0x77,
- 0x68, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x67, 0x65, 0x6e,
- 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x41, 0x67, 0x65,
- 0x6e, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01,
- 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x65, 0x6c,
- 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x49, 0x64,
- 0x12, 0x26, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x5f, 0x73, 0x65, 0x71, 0x18, 0x07, 0x20, 0x03, 0x28,
- 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x71, 0x52, 0x61, 0x6e, 0x67, 0x65,
- 0x52, 0x06, 0x64, 0x65, 0x6c, 0x53, 0x65, 0x71, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
- 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x22,
- 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
- 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72,
- 0x49, 0x64, 0x12, 0x21, 0x0a, 0x03, 0x61, 0x63, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32,
- 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x6f, 0x64, 0x65,
- 0x52, 0x03, 0x61, 0x63, 0x73, 0x22, 0x7d, 0x0a, 0x04, 0x57, 0x68, 0x61, 0x74, 0x12, 0x06, 0x0a,
- 0x02, 0x58, 0x33, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4e, 0x10, 0x01, 0x12, 0x07, 0x0a,
- 0x03, 0x4f, 0x46, 0x46, 0x10, 0x02, 0x12, 0x06, 0x0a, 0x02, 0x55, 0x41, 0x10, 0x03, 0x12, 0x07,
- 0x0a, 0x03, 0x55, 0x50, 0x44, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x4f, 0x4e, 0x45, 0x10,
- 0x05, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x43, 0x53, 0x10, 0x06, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x45,
- 0x52, 0x4d, 0x10, 0x07, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x53, 0x47, 0x10, 0x08, 0x12, 0x08, 0x0a,
- 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x09, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x43, 0x56, 0x10,
- 0x0a, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x45, 0x4c, 0x10, 0x0b, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x41,
- 0x47, 0x53, 0x10, 0x0c, 0x22, 0xd2, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d,
- 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x22, 0x0a, 0x04, 0x64, 0x65, 0x73,
- 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x54, 0x6f,
- 0x70, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x12, 0x1f, 0x0a,
- 0x03, 0x73, 0x75, 0x62, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78,
- 0x2e, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x53, 0x75, 0x62, 0x52, 0x03, 0x73, 0x75, 0x62, 0x12, 0x20,
- 0x0a, 0x03, 0x64, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62,
- 0x78, 0x2e, 0x44, 0x65, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x03, 0x64, 0x65, 0x6c,
- 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04,
- 0x74, 0x61, 0x67, 0x73, 0x12, 0x23, 0x0a, 0x04, 0x63, 0x72, 0x65, 0x64, 0x18, 0x07, 0x20, 0x03,
- 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43,
- 0x72, 0x65, 0x64, 0x52, 0x04, 0x63, 0x72, 0x65, 0x64, 0x22, 0xea, 0x01, 0x0a, 0x0a, 0x53, 0x65,
- 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69,
- 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x20,
- 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x66, 0x72, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
- 0x12, 0x21, 0x0a, 0x04, 0x77, 0x68, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d,
+ 0x52, 0x06, 0x64, 0x65, 0x6c, 0x53, 0x65, 0x71, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72,
+ 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49,
+ 0x64, 0x12, 0x23, 0x0a, 0x04, 0x63, 0x72, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x72, 0x65, 0x64,
+ 0x52, 0x04, 0x63, 0x72, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x72, 0x64, 0x18, 0x07,
+ 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x61, 0x72, 0x64, 0x22, 0x3f, 0x0a, 0x04, 0x57, 0x68,
+ 0x61, 0x74, 0x12, 0x06, 0x0a, 0x02, 0x58, 0x30, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x53,
+ 0x47, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x4f, 0x50, 0x49, 0x43, 0x10, 0x02, 0x12, 0x07,
+ 0x0a, 0x03, 0x53, 0x55, 0x42, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x55, 0x53, 0x45, 0x52, 0x10,
+ 0x04, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x52, 0x45, 0x44, 0x10, 0x05, 0x22, 0xce, 0x01, 0x0a, 0x0a,
+ 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f,
+ 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63,
+ 0x12, 0x21, 0x0a, 0x04, 0x77, 0x68, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d,
0x2e, 0x70, 0x62, 0x78, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x4e, 0x6f, 0x74, 0x65, 0x52, 0x04, 0x77,
- 0x68, 0x61, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
- 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x72,
- 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x72, 0x63, 0x12, 0x18, 0x0a, 0x07,
- 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63,
- 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18,
- 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x61, 0x6c, 0x6c,
- 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07,
- 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70,
- 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0xef, 0x01, 0x0a, 0x09, 0x53, 0x65, 0x72, 0x76, 0x65,
- 0x72, 0x4d, 0x73, 0x67, 0x12, 0x25, 0x0a, 0x04, 0x63, 0x74, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43,
- 0x74, 0x72, 0x6c, 0x48, 0x00, 0x52, 0x04, 0x63, 0x74, 0x72, 0x6c, 0x12, 0x25, 0x0a, 0x04, 0x64,
- 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e,
- 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x04, 0x64, 0x61,
- 0x74, 0x61, 0x12, 0x25, 0x0a, 0x04, 0x70, 0x72, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
- 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x65,
- 0x73, 0x48, 0x00, 0x52, 0x04, 0x70, 0x72, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x04, 0x6d, 0x65, 0x74,
- 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65,
- 0x72, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x48, 0x00, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61,
- 0x12, 0x25, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f,
- 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x48,
- 0x00, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63,
- 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x42, 0x09, 0x0a,
- 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x81, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72,
- 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
- 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x52, 0x65,
- 0x73, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26,
- 0x0a, 0x06, 0x73, 0x72, 0x76, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e,
- 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x73, 0x67, 0x52, 0x06,
- 0x73, 0x72, 0x76, 0x6d, 0x73, 0x67, 0x12, 0x24, 0x0a, 0x05, 0x63, 0x6c, 0x6d, 0x73, 0x67, 0x18,
- 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65,
- 0x6e, 0x74, 0x4d, 0x73, 0x67, 0x52, 0x05, 0x63, 0x6c, 0x6d, 0x73, 0x67, 0x22, 0xe9, 0x01, 0x0a,
- 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73,
- 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65,
- 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f,
- 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64,
+ 0x68, 0x61, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f,
+ 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e,
+ 0x74, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x18, 0x05,
+ 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x75, 0x6e, 0x72, 0x65, 0x61, 0x64, 0x12, 0x24, 0x0a, 0x05,
+ 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x70, 0x62,
+ 0x78, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65,
+ 0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x07, 0x20,
+ 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x80, 0x01, 0x0a,
+ 0x0b, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x78, 0x74, 0x72, 0x61, 0x12, 0x20, 0x0a, 0x0b,
+ 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
+ 0x09, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x20,
+ 0x0a, 0x0c, 0x6f, 0x6e, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x6c, 0x66, 0x5f, 0x6f, 0x66, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x6e, 0x42, 0x65, 0x68, 0x61, 0x6c, 0x66, 0x4f, 0x66,
0x12, 0x2d, 0x0a, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4c,
- 0x65, 0x76, 0x65, 0x6c, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12,
- 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x04,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72,
- 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x05,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12,
- 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08,
- 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
- 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x4f, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65,
- 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x20, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d,
- 0x73, 0x67, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x20, 0x0a, 0x04, 0x73, 0x65, 0x73, 0x73, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x73, 0x73,
- 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x73, 0x65, 0x73, 0x73, 0x22, 0x3c, 0x0a, 0x0b, 0x53, 0x65, 0x61,
- 0x72, 0x63, 0x68, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72,
- 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49,
- 0x64, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x71, 0x0a, 0x0b, 0x53, 0x65, 0x61, 0x72, 0x63,
- 0x68, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x52, 0x65, 0x73,
- 0x70, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a,
- 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75,
- 0x65, 0x72, 0x79, 0x12, 0x25, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20,
- 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x53,
- 0x75, 0x62, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x67, 0x0a, 0x0a, 0x54, 0x6f,
- 0x70, 0x69, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69,
- 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x09, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43,
- 0x72, 0x75, 0x64, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e,
- 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12,
- 0x22, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e,
- 0x70, 0x62, 0x78, 0x2e, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x52, 0x04, 0x64,
- 0x65, 0x73, 0x63, 0x22, 0xac, 0x01, 0x0a, 0x0c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x45,
- 0x76, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x0e, 0x32, 0x09, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x72, 0x75, 0x64, 0x52,
- 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f,
- 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64,
- 0x12, 0x34, 0x0a, 0x0b, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x63, 0x73, 0x18,
- 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x44, 0x65, 0x66, 0x61,
- 0x75, 0x6c, 0x74, 0x41, 0x63, 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x0a, 0x64, 0x65, 0x66, 0x61,
- 0x75, 0x6c, 0x74, 0x41, 0x63, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
- 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x12,
- 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61,
- 0x67, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x11, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
- 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69,
- 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x09, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43,
- 0x72, 0x75, 0x64, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x74,
- 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69,
- 0x63, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x65,
- 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x49,
- 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01,
- 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65,
- 0x63, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x63,
- 0x76, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28,
- 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x6f,
- 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76,
- 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61,
- 0x74, 0x65, 0x22, 0x54, 0x0a, 0x0c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x45, 0x76, 0x65,
- 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0e, 0x32, 0x09, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x72, 0x75, 0x64, 0x52, 0x06, 0x61,
- 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x44,
- 0x61, 0x74, 0x61, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x2a, 0x33, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68,
- 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12,
- 0x08, 0x0a, 0x04, 0x41, 0x4e, 0x4f, 0x4e, 0x10, 0x0a, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x55, 0x54,
- 0x48, 0x10, 0x14, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x4f, 0x4f, 0x54, 0x10, 0x1e, 0x2a, 0x52, 0x0a,
- 0x08, 0x49, 0x6e, 0x66, 0x6f, 0x4e, 0x6f, 0x74, 0x65, 0x12, 0x06, 0x0a, 0x02, 0x58, 0x31, 0x10,
- 0x00, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52,
- 0x45, 0x43, 0x56, 0x10, 0x02, 0x12, 0x06, 0x0a, 0x02, 0x4b, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a,
- 0x04, 0x43, 0x41, 0x4c, 0x4c, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x59, 0x50, 0x41, 0x53,
- 0x53, 0x10, 0x05, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10,
- 0x06, 0x2a, 0x6f, 0x0a, 0x09, 0x43, 0x61, 0x6c, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x06,
- 0x0a, 0x02, 0x58, 0x32, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54,
- 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x4e, 0x53, 0x57, 0x45, 0x52, 0x10, 0x02, 0x12, 0x0b,
- 0x0a, 0x07, 0x48, 0x41, 0x4e, 0x47, 0x5f, 0x55, 0x50, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x49,
- 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x44, 0x49, 0x44, 0x41, 0x54, 0x45, 0x10, 0x04, 0x12, 0x0a,
- 0x0a, 0x06, 0x49, 0x4e, 0x56, 0x49, 0x54, 0x45, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x46,
- 0x46, 0x45, 0x52, 0x10, 0x06, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x49, 0x4e, 0x47, 0x49, 0x4e, 0x47,
- 0x10, 0x07, 0x2a, 0x3c, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x0c,
- 0x0a, 0x08, 0x43, 0x4f, 0x4e, 0x54, 0x49, 0x4e, 0x55, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04,
- 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e,
- 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x10, 0x03,
- 0x2a, 0x2a, 0x0a, 0x04, 0x43, 0x72, 0x75, 0x64, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41,
- 0x54, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x01,
- 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x02, 0x32, 0x3b, 0x0a, 0x04,
- 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x33, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4c,
- 0x6f, 0x6f, 0x70, 0x12, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
- 0x4d, 0x73, 0x67, 0x1a, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x4d, 0x73, 0x67, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x32, 0x9f, 0x02, 0x0a, 0x06, 0x50, 0x6c,
- 0x75, 0x67, 0x69, 0x6e, 0x12, 0x2d, 0x0a, 0x08, 0x46, 0x69, 0x72, 0x65, 0x48, 0x6f, 0x73, 0x65,
- 0x12, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71,
- 0x1a, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73,
- 0x70, 0x22, 0x00, 0x12, 0x2c, 0x0a, 0x04, 0x46, 0x69, 0x6e, 0x64, 0x12, 0x10, 0x2e, 0x70, 0x62,
- 0x78, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x51, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x10, 0x2e,
- 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22,
- 0x00, 0x12, 0x2b, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x11, 0x2e, 0x70,
- 0x62, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a,
- 0x0b, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x22, 0x00, 0x12, 0x27,
- 0x0a, 0x05, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x54, 0x6f,
- 0x70, 0x69, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x55,
- 0x6e, 0x75, 0x73, 0x65, 0x64, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x0c, 0x53, 0x75, 0x62, 0x73, 0x63,
- 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x75,
- 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a,
- 0x0b, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x22, 0x00, 0x12, 0x2b,
- 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x78, 0x2e,
- 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x0b, 0x2e, 0x70,
- 0x62, 0x78, 0x2e, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x67,
- 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x69, 0x6e, 0x6f, 0x64, 0x65,
- 0x2f, 0x63, 0x68, 0x61, 0x74, 0x2f, 0x70, 0x62, 0x78, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
- 0x33,
+ 0x65, 0x76, 0x65, 0x6c, 0x52, 0x09, 0x61, 0x75, 0x74, 0x68, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x22,
+ 0xb2, 0x03, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x73, 0x67, 0x12, 0x1f, 0x0a,
+ 0x02, 0x68, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e,
+ 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x48, 0x69, 0x48, 0x00, 0x52, 0x02, 0x68, 0x69, 0x12, 0x22,
+ 0x0a, 0x03, 0x61, 0x63, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62,
+ 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x41, 0x63, 0x63, 0x48, 0x00, 0x52, 0x03, 0x61,
+ 0x63, 0x63, 0x12, 0x28, 0x0a, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x6f,
+ 0x67, 0x69, 0x6e, 0x48, 0x00, 0x52, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x22, 0x0a, 0x03,
+ 0x73, 0x75, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e,
+ 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x75, 0x62, 0x48, 0x00, 0x52, 0x03, 0x73, 0x75, 0x62,
+ 0x12, 0x28, 0x0a, 0x05, 0x6c, 0x65, 0x61, 0x76, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x10, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4c, 0x65, 0x61, 0x76,
+ 0x65, 0x48, 0x00, 0x52, 0x05, 0x6c, 0x65, 0x61, 0x76, 0x65, 0x12, 0x22, 0x0a, 0x03, 0x70, 0x75,
+ 0x62, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c,
+ 0x69, 0x65, 0x6e, 0x74, 0x50, 0x75, 0x62, 0x48, 0x00, 0x52, 0x03, 0x70, 0x75, 0x62, 0x12, 0x22,
+ 0x0a, 0x03, 0x67, 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62,
+ 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x47, 0x65, 0x74, 0x48, 0x00, 0x52, 0x03, 0x67,
+ 0x65, 0x74, 0x12, 0x22, 0x0a, 0x03, 0x73, 0x65, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x74, 0x48,
+ 0x00, 0x52, 0x03, 0x73, 0x65, 0x74, 0x12, 0x22, 0x0a, 0x03, 0x64, 0x65, 0x6c, 0x18, 0x09, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74,
+ 0x44, 0x65, 0x6c, 0x48, 0x00, 0x52, 0x03, 0x64, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x04, 0x6e, 0x6f,
+ 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43,
+ 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4e, 0x6f, 0x74, 0x65, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x6f, 0x74,
+ 0x65, 0x12, 0x26, 0x0a, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x10, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x45, 0x78, 0x74,
+ 0x72, 0x61, 0x52, 0x05, 0x65, 0x78, 0x74, 0x72, 0x61, 0x42, 0x09, 0x0a, 0x07, 0x4d, 0x65, 0x73,
+ 0x73, 0x61, 0x67, 0x65, 0x22, 0x4e, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x72,
+ 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
+ 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
+ 0x12, 0x12, 0x0a, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04,
+ 0x64, 0x6f, 0x6e, 0x65, 0x22, 0xb4, 0x04, 0x0a, 0x09, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x44, 0x65,
+ 0x73, 0x63, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41,
+ 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74,
+ 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x64, 0x41, 0x74, 0x12,
+ 0x2b, 0x0a, 0x06, 0x64, 0x65, 0x66, 0x61, 0x63, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x13, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x63, 0x73,
+ 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x64, 0x65, 0x66, 0x61, 0x63, 0x73, 0x12, 0x21, 0x0a, 0x03,
+ 0x61, 0x63, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e,
+ 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x03, 0x61, 0x63, 0x73, 0x12,
+ 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52,
+ 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x69,
+ 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x49, 0x64, 0x12,
+ 0x17, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05,
+ 0x52, 0x06, 0x72, 0x65, 0x63, 0x76, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x5f,
+ 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x49, 0x64, 0x12,
+ 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52,
+ 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61,
+ 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74,
+ 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x74, 0x65,
+ 0x5f, 0x61, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x73, 0x74, 0x61, 0x74, 0x65,
+ 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x18, 0x0e, 0x20,
+ 0x01, 0x28, 0x0c, 0x52, 0x07, 0x74, 0x72, 0x75, 0x73, 0x74, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07,
+ 0x69, 0x73, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69,
+ 0x73, 0x43, 0x68, 0x61, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x18,
+ 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x24, 0x0a,
+ 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
+ 0x0f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x54,
+ 0x69, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e,
+ 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x41,
+ 0x67, 0x65, 0x6e, 0x74, 0x12, 0x15, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0xe8, 0x07,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x22, 0xf3, 0x03, 0x0a, 0x08,
+ 0x54, 0x6f, 0x70, 0x69, 0x63, 0x53, 0x75, 0x62, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x70, 0x64, 0x61,
+ 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x75, 0x70,
+ 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x6c, 0x65, 0x74,
+ 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x65, 0x6c,
+ 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
+ 0x64, 0x5f, 0x61, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61,
+ 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x21, 0x0a,
+ 0x03, 0x61, 0x63, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78,
+ 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x03, 0x61, 0x63, 0x73,
+ 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28,
+ 0x05, 0x52, 0x06, 0x72, 0x65, 0x61, 0x64, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x63,
+ 0x76, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x63, 0x76,
+ 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x07, 0x20, 0x01,
+ 0x28, 0x0c, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x18, 0x0a, 0x07, 0x74, 0x72,
+ 0x75, 0x73, 0x74, 0x65, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x74, 0x72, 0x75,
+ 0x73, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18,
+ 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x17,
+ 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63,
+ 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x1d, 0x0a,
+ 0x0a, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x09, 0x74, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x64, 0x41, 0x74, 0x12, 0x15, 0x0a, 0x06,
+ 0x73, 0x65, 0x71, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x65,
+ 0x71, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20,
+ 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6c, 0x61,
+ 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01,
+ 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65,
+ 0x12, 0x2f, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x75, 0x73,
+ 0x65, 0x72, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11,
+ 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x55, 0x73, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e,
+ 0x74, 0x22, 0x4a, 0x0a, 0x09, 0x44, 0x65, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x15,
+ 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
+ 0x64, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x5f, 0x73, 0x65, 0x71,
+ 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x71,
+ 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x53, 0x65, 0x71, 0x22, 0xca, 0x01,
+ 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x74, 0x72, 0x6c, 0x12, 0x0e, 0x0a, 0x02,
+ 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05,
+ 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70,
+ 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05,
+ 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x04,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x33, 0x0a, 0x06, 0x70, 0x61,
+ 0x72, 0x61, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x62, 0x78,
+ 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x74, 0x72, 0x6c, 0x2e, 0x50, 0x61, 0x72, 0x61,
+ 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x1a,
+ 0x39, 0x0a, 0x0b, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
+ 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
+ 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
+ 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x9a, 0x02, 0x0a, 0x0a, 0x53,
+ 0x65, 0x72, 0x76, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70,
+ 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12,
+ 0x20, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x66, 0x72, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x49,
+ 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07,
+ 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12,
+ 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x15,
+ 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
+ 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x04, 0x68, 0x65, 0x61, 0x64, 0x18, 0x05, 0x20,
+ 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
+ 0x44, 0x61, 0x74, 0x61, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04,
+ 0x68, 0x65, 0x61, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18,
+ 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x1a, 0x37,
+ 0x0a, 0x09, 0x48, 0x65, 0x61, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
+ 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a,
+ 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61,
+ 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xbf, 0x03, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76,
+ 0x65, 0x72, 0x50, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x10, 0x0a, 0x03,
+ 0x73, 0x72, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x72, 0x63, 0x12, 0x28,
+ 0x0a, 0x04, 0x77, 0x68, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x70,
+ 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x65, 0x73, 0x2e, 0x57, 0x68,
+ 0x61, 0x74, 0x52, 0x04, 0x77, 0x68, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72,
+ 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x73,
+ 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f, 0x69,
+ 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12, 0x15,
+ 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05,
+ 0x64, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x5f, 0x73, 0x65, 0x71,
+ 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x71,
+ 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x64, 0x65, 0x6c, 0x53, 0x65, 0x71, 0x12, 0x24, 0x0a,
+ 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
+ 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65,
+ 0x72, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65,
+ 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f,
+ 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x03, 0x61, 0x63, 0x73, 0x18, 0x0a,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73,
+ 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x03, 0x61, 0x63, 0x73, 0x22, 0x7d, 0x0a, 0x04, 0x57, 0x68,
+ 0x61, 0x74, 0x12, 0x06, 0x0a, 0x02, 0x58, 0x33, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4e,
+ 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x46, 0x46, 0x10, 0x02, 0x12, 0x06, 0x0a, 0x02, 0x55,
+ 0x41, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x50, 0x44, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04,
+ 0x47, 0x4f, 0x4e, 0x45, 0x10, 0x05, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x43, 0x53, 0x10, 0x06, 0x12,
+ 0x08, 0x0a, 0x04, 0x54, 0x45, 0x52, 0x4d, 0x10, 0x07, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x53, 0x47,
+ 0x10, 0x08, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x09, 0x12, 0x08, 0x0a, 0x04,
+ 0x52, 0x45, 0x43, 0x56, 0x10, 0x0a, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x45, 0x4c, 0x10, 0x0b, 0x12,
+ 0x08, 0x0a, 0x04, 0x54, 0x41, 0x47, 0x53, 0x10, 0x0c, 0x22, 0xd2, 0x01, 0x0a, 0x0a, 0x53, 0x65,
+ 0x72, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69,
+ 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x22,
+ 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70,
+ 0x62, 0x78, 0x2e, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x52, 0x04, 0x64, 0x65,
+ 0x73, 0x63, 0x12, 0x1f, 0x0a, 0x03, 0x73, 0x75, 0x62, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32,
+ 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x53, 0x75, 0x62, 0x52, 0x03,
+ 0x73, 0x75, 0x62, 0x12, 0x20, 0x0a, 0x03, 0x64, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x44, 0x65, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73,
+ 0x52, 0x03, 0x64, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x06, 0x20,
+ 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x23, 0x0a, 0x04, 0x63, 0x72, 0x65,
+ 0x64, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65,
+ 0x72, 0x76, 0x65, 0x72, 0x43, 0x72, 0x65, 0x64, 0x52, 0x04, 0x63, 0x72, 0x65, 0x64, 0x22, 0xea,
+ 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x14, 0x0a,
+ 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
+ 0x70, 0x69, 0x63, 0x12, 0x20, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72,
+ 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x66, 0x72, 0x6f, 0x6d, 0x55,
+ 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x04, 0x77, 0x68, 0x61, 0x74, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x4e, 0x6f,
+ 0x74, 0x65, 0x52, 0x04, 0x77, 0x68, 0x61, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x73, 0x65, 0x71, 0x5f,
+ 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x65, 0x71, 0x49, 0x64, 0x12,
+ 0x10, 0x0a, 0x03, 0x73, 0x72, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x72,
+ 0x63, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01,
+ 0x28, 0x0c, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x05, 0x65,
+ 0x76, 0x65, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78,
+ 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e,
+ 0x74, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x08, 0x20, 0x01,
+ 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0xf3, 0x01, 0x0a, 0x09,
+ 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x73, 0x67, 0x12, 0x25, 0x0a, 0x04, 0x63, 0x74, 0x72,
+ 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65,
+ 0x72, 0x76, 0x65, 0x72, 0x43, 0x74, 0x72, 0x6c, 0x48, 0x00, 0x52, 0x04, 0x63, 0x74, 0x72, 0x6c,
+ 0x12, 0x25, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f,
+ 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x48,
+ 0x00, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x04, 0x70, 0x72, 0x65, 0x73, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76,
+ 0x65, 0x72, 0x50, 0x72, 0x65, 0x73, 0x48, 0x00, 0x52, 0x04, 0x70, 0x72, 0x65, 0x73, 0x12, 0x25,
+ 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70,
+ 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x48, 0x00, 0x52,
+ 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x05, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
+ 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x05,
+ 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52,
+ 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x42, 0x09, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
+ 0x65, 0x22, 0x81, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
+ 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e,
+ 0x32, 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x52,
+ 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x26, 0x0a, 0x06, 0x73, 0x72, 0x76, 0x6d, 0x73,
+ 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65,
+ 0x72, 0x76, 0x65, 0x72, 0x4d, 0x73, 0x67, 0x52, 0x06, 0x73, 0x72, 0x76, 0x6d, 0x73, 0x67, 0x12,
+ 0x24, 0x0a, 0x05, 0x63, 0x6c, 0x6d, 0x73, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e,
+ 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x73, 0x67, 0x52, 0x05,
+ 0x63, 0x6c, 0x6d, 0x73, 0x67, 0x22, 0xe9, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f,
+ 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64,
+ 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2d, 0x0a, 0x0a, 0x61, 0x75, 0x74,
+ 0x68, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e,
+ 0x70, 0x62, 0x78, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x09, 0x61,
+ 0x75, 0x74, 0x68, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, 0x6d, 0x6f,
+ 0x74, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72,
+ 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65,
+ 0x72, 0x5f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75,
+ 0x73, 0x65, 0x72, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69,
+ 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76,
+ 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67,
+ 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67,
+ 0x65, 0x22, 0x4f, 0x0a, 0x09, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x20,
+ 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62,
+ 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x73, 0x67, 0x52, 0x03, 0x6d, 0x73, 0x67,
+ 0x12, 0x20, 0x0a, 0x04, 0x73, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c,
+ 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x73, 0x65,
+ 0x73, 0x73, 0x22, 0x3c, 0x0a, 0x0b, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x51, 0x75, 0x65, 0x72,
+ 0x79, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75,
+ 0x65, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79,
+ 0x22, 0x71, 0x0a, 0x0b, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x12,
+ 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
+ 0x0d, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x06,
+ 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x25, 0x0a, 0x06,
+ 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x70,
+ 0x62, 0x78, 0x2e, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x53, 0x75, 0x62, 0x52, 0x06, 0x72, 0x65, 0x73,
+ 0x75, 0x6c, 0x74, 0x22, 0x67, 0x0a, 0x0a, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x45, 0x76, 0x65, 0x6e,
+ 0x74, 0x12, 0x21, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0e, 0x32, 0x09, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x72, 0x75, 0x64, 0x52, 0x06, 0x61, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63,
+ 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x54, 0x6f, 0x70,
+ 0x69, 0x63, 0x44, 0x65, 0x73, 0x63, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x22, 0xac, 0x01, 0x0a,
+ 0x0c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a,
+ 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x09, 0x2e,
+ 0x70, 0x62, 0x78, 0x2e, 0x43, 0x72, 0x75, 0x64, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x0b, 0x64, 0x65, 0x66,
+ 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x63, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13,
+ 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x63, 0x73, 0x4d,
+ 0x6f, 0x64, 0x65, 0x52, 0x0a, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x63, 0x73, 0x12,
+ 0x16, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
+ 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18,
+ 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x22, 0xed, 0x01, 0x0a, 0x11,
+ 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e,
+ 0x74, 0x12, 0x21, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0e, 0x32, 0x09, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x43, 0x72, 0x75, 0x64, 0x52, 0x06, 0x61, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
+ 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65,
+ 0x72, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
+ 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65,
+ 0x61, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x61,
+ 0x64, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x06,
+ 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, 0x63, 0x76, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x04,
+ 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x78,
+ 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64,
+ 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01,
+ 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0x54, 0x0a, 0x0c, 0x4d,
+ 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x06, 0x61,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x09, 0x2e, 0x70, 0x62,
+ 0x78, 0x2e, 0x43, 0x72, 0x75, 0x64, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21,
+ 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62,
+ 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x52, 0x03, 0x6d, 0x73,
+ 0x67, 0x2a, 0x33, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x08,
+ 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x4e, 0x4f, 0x4e,
+ 0x10, 0x0a, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x55, 0x54, 0x48, 0x10, 0x14, 0x12, 0x08, 0x0a, 0x04,
+ 0x52, 0x4f, 0x4f, 0x54, 0x10, 0x1e, 0x2a, 0x52, 0x0a, 0x08, 0x49, 0x6e, 0x66, 0x6f, 0x4e, 0x6f,
+ 0x74, 0x65, 0x12, 0x06, 0x0a, 0x02, 0x58, 0x31, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45,
+ 0x41, 0x44, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x43, 0x56, 0x10, 0x02, 0x12, 0x06,
+ 0x0a, 0x02, 0x4b, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x41, 0x4c, 0x4c, 0x10, 0x04,
+ 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x59, 0x50, 0x41, 0x53, 0x53, 0x10, 0x05, 0x12, 0x0c, 0x0a, 0x08,
+ 0x52, 0x45, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x06, 0x2a, 0x6f, 0x0a, 0x09, 0x43, 0x61,
+ 0x6c, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x06, 0x0a, 0x02, 0x58, 0x32, 0x10, 0x00, 0x12,
+ 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x41,
+ 0x4e, 0x53, 0x57, 0x45, 0x52, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x48, 0x41, 0x4e, 0x47, 0x5f,
+ 0x55, 0x50, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x44,
+ 0x49, 0x44, 0x41, 0x54, 0x45, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x49, 0x4e, 0x56, 0x49, 0x54,
+ 0x45, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x46, 0x46, 0x45, 0x52, 0x10, 0x06, 0x12, 0x0b,
+ 0x0a, 0x07, 0x52, 0x49, 0x4e, 0x47, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x2a, 0x3c, 0x0a, 0x08, 0x52,
+ 0x65, 0x73, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x4e, 0x54, 0x49,
+ 0x4e, 0x55, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x12,
+ 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07,
+ 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x10, 0x03, 0x2a, 0x2a, 0x0a, 0x04, 0x43, 0x72, 0x75,
+ 0x64, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x00, 0x12, 0x0a, 0x0a,
+ 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c,
+ 0x45, 0x54, 0x45, 0x10, 0x02, 0x32, 0x3b, 0x0a, 0x04, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x33, 0x0a,
+ 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4c, 0x6f, 0x6f, 0x70, 0x12, 0x0e, 0x2e, 0x70,
+ 0x62, 0x78, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4d, 0x73, 0x67, 0x1a, 0x0e, 0x2e, 0x70,
+ 0x62, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x73, 0x67, 0x22, 0x00, 0x28, 0x01,
+ 0x30, 0x01, 0x32, 0x9f, 0x02, 0x0a, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x2d, 0x0a,
+ 0x08, 0x46, 0x69, 0x72, 0x65, 0x48, 0x6f, 0x73, 0x65, 0x12, 0x0e, 0x2e, 0x70, 0x62, 0x78, 0x2e,
+ 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x1a, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e,
+ 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x22, 0x00, 0x12, 0x2c, 0x0a, 0x04,
+ 0x46, 0x69, 0x6e, 0x64, 0x12, 0x10, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63,
+ 0x68, 0x51, 0x75, 0x65, 0x72, 0x79, 0x1a, 0x10, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x65, 0x61,
+ 0x72, 0x63, 0x68, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x07, 0x41, 0x63,
+ 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x41, 0x63, 0x63, 0x6f,
+ 0x75, 0x6e, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x55,
+ 0x6e, 0x75, 0x73, 0x65, 0x64, 0x22, 0x00, 0x12, 0x27, 0x0a, 0x05, 0x54, 0x6f, 0x70, 0x69, 0x63,
+ 0x12, 0x0f, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x54, 0x6f, 0x70, 0x69, 0x63, 0x45, 0x76, 0x65, 0x6e,
+ 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x55, 0x6e, 0x75, 0x73, 0x65, 0x64, 0x22, 0x00,
+ 0x12, 0x35, 0x0a, 0x0c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+ 0x12, 0x16, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
+ 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x55,
+ 0x6e, 0x75, 0x73, 0x65, 0x64, 0x22, 0x00, 0x12, 0x2b, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61,
+ 0x67, 0x65, 0x12, 0x11, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
+ 0x45, 0x76, 0x65, 0x6e, 0x74, 0x1a, 0x0b, 0x2e, 0x70, 0x62, 0x78, 0x2e, 0x55, 0x6e, 0x75, 0x73,
+ 0x65, 0x64, 0x22, 0x00, 0x42, 0x1c, 0x5a, 0x1a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+ 0x6f, 0x6d, 0x2f, 0x74, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x74, 0x2f, 0x70,
+ 0x62, 0x78, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -4592,84 +4640,85 @@ var file_model_proto_depIdxs = []int32{
11, // 7: pbx.SetQuery.cred:type_name -> pbx.ClientCred
12, // 8: pbx.ClientAcc.desc:type_name -> pbx.SetDesc
11, // 9: pbx.ClientAcc.cred:type_name -> pbx.ClientCred
- 11, // 10: pbx.ClientLogin.cred:type_name -> pbx.ClientCred
- 15, // 11: pbx.ClientSub.set_query:type_name -> pbx.SetQuery
- 14, // 12: pbx.ClientSub.get_query:type_name -> pbx.GetQuery
- 49, // 13: pbx.ClientPub.head:type_name -> pbx.ClientPub.HeadEntry
- 14, // 14: pbx.ClientGet.query:type_name -> pbx.GetQuery
- 15, // 15: pbx.ClientSet.query:type_name -> pbx.SetQuery
- 5, // 16: pbx.ClientDel.what:type_name -> pbx.ClientDel.What
- 16, // 17: pbx.ClientDel.del_seq:type_name -> pbx.SeqRange
- 11, // 18: pbx.ClientDel.cred:type_name -> pbx.ClientCred
- 1, // 19: pbx.ClientNote.what:type_name -> pbx.InfoNote
- 2, // 20: pbx.ClientNote.event:type_name -> pbx.CallEvent
- 0, // 21: pbx.ClientExtra.auth_level:type_name -> pbx.AuthLevel
- 17, // 22: pbx.ClientMsg.hi:type_name -> pbx.ClientHi
- 18, // 23: pbx.ClientMsg.acc:type_name -> pbx.ClientAcc
- 19, // 24: pbx.ClientMsg.login:type_name -> pbx.ClientLogin
- 20, // 25: pbx.ClientMsg.sub:type_name -> pbx.ClientSub
- 21, // 26: pbx.ClientMsg.leave:type_name -> pbx.ClientLeave
- 22, // 27: pbx.ClientMsg.pub:type_name -> pbx.ClientPub
- 23, // 28: pbx.ClientMsg.get:type_name -> pbx.ClientGet
- 24, // 29: pbx.ClientMsg.set:type_name -> pbx.ClientSet
- 25, // 30: pbx.ClientMsg.del:type_name -> pbx.ClientDel
- 26, // 31: pbx.ClientMsg.note:type_name -> pbx.ClientNote
- 27, // 32: pbx.ClientMsg.extra:type_name -> pbx.ClientExtra
- 8, // 33: pbx.TopicDesc.defacs:type_name -> pbx.DefaultAcsMode
- 9, // 34: pbx.TopicDesc.acs:type_name -> pbx.AccessMode
- 9, // 35: pbx.TopicSub.acs:type_name -> pbx.AccessMode
- 16, // 36: pbx.DelValues.del_seq:type_name -> pbx.SeqRange
- 50, // 37: pbx.ServerCtrl.params:type_name -> pbx.ServerCtrl.ParamsEntry
- 51, // 38: pbx.ServerData.head:type_name -> pbx.ServerData.HeadEntry
- 6, // 39: pbx.ServerPres.what:type_name -> pbx.ServerPres.What
- 16, // 40: pbx.ServerPres.del_seq:type_name -> pbx.SeqRange
- 9, // 41: pbx.ServerPres.acs:type_name -> pbx.AccessMode
- 30, // 42: pbx.ServerMeta.desc:type_name -> pbx.TopicDesc
- 31, // 43: pbx.ServerMeta.sub:type_name -> pbx.TopicSub
- 32, // 44: pbx.ServerMeta.del:type_name -> pbx.DelValues
- 29, // 45: pbx.ServerMeta.cred:type_name -> pbx.ServerCred
- 1, // 46: pbx.ServerInfo.what:type_name -> pbx.InfoNote
- 2, // 47: pbx.ServerInfo.event:type_name -> pbx.CallEvent
- 33, // 48: pbx.ServerMsg.ctrl:type_name -> pbx.ServerCtrl
- 34, // 49: pbx.ServerMsg.data:type_name -> pbx.ServerData
- 35, // 50: pbx.ServerMsg.pres:type_name -> pbx.ServerPres
- 36, // 51: pbx.ServerMsg.meta:type_name -> pbx.ServerMeta
- 37, // 52: pbx.ServerMsg.info:type_name -> pbx.ServerInfo
- 3, // 53: pbx.ServerResp.status:type_name -> pbx.RespCode
- 38, // 54: pbx.ServerResp.srvmsg:type_name -> pbx.ServerMsg
- 28, // 55: pbx.ServerResp.clmsg:type_name -> pbx.ClientMsg
- 0, // 56: pbx.Session.auth_level:type_name -> pbx.AuthLevel
- 28, // 57: pbx.ClientReq.msg:type_name -> pbx.ClientMsg
- 40, // 58: pbx.ClientReq.sess:type_name -> pbx.Session
- 3, // 59: pbx.SearchFound.status:type_name -> pbx.RespCode
- 31, // 60: pbx.SearchFound.result:type_name -> pbx.TopicSub
- 4, // 61: pbx.TopicEvent.action:type_name -> pbx.Crud
- 30, // 62: pbx.TopicEvent.desc:type_name -> pbx.TopicDesc
- 4, // 63: pbx.AccountEvent.action:type_name -> pbx.Crud
- 8, // 64: pbx.AccountEvent.default_acs:type_name -> pbx.DefaultAcsMode
- 4, // 65: pbx.SubscriptionEvent.action:type_name -> pbx.Crud
- 9, // 66: pbx.SubscriptionEvent.mode:type_name -> pbx.AccessMode
- 4, // 67: pbx.MessageEvent.action:type_name -> pbx.Crud
- 34, // 68: pbx.MessageEvent.msg:type_name -> pbx.ServerData
- 28, // 69: pbx.Node.MessageLoop:input_type -> pbx.ClientMsg
- 41, // 70: pbx.Plugin.FireHose:input_type -> pbx.ClientReq
- 42, // 71: pbx.Plugin.Find:input_type -> pbx.SearchQuery
- 45, // 72: pbx.Plugin.Account:input_type -> pbx.AccountEvent
- 44, // 73: pbx.Plugin.Topic:input_type -> pbx.TopicEvent
- 46, // 74: pbx.Plugin.Subscription:input_type -> pbx.SubscriptionEvent
- 47, // 75: pbx.Plugin.Message:input_type -> pbx.MessageEvent
- 38, // 76: pbx.Node.MessageLoop:output_type -> pbx.ServerMsg
- 39, // 77: pbx.Plugin.FireHose:output_type -> pbx.ServerResp
- 43, // 78: pbx.Plugin.Find:output_type -> pbx.SearchFound
- 7, // 79: pbx.Plugin.Account:output_type -> pbx.Unused
- 7, // 80: pbx.Plugin.Topic:output_type -> pbx.Unused
- 7, // 81: pbx.Plugin.Subscription:output_type -> pbx.Unused
- 7, // 82: pbx.Plugin.Message:output_type -> pbx.Unused
- 76, // [76:83] is the sub-list for method output_type
- 69, // [69:76] is the sub-list for method input_type
- 69, // [69:69] is the sub-list for extension type_name
- 69, // [69:69] is the sub-list for extension extendee
- 0, // [0:69] is the sub-list for field type_name
+ 0, // 10: pbx.ClientAcc.auth_level:type_name -> pbx.AuthLevel
+ 11, // 11: pbx.ClientLogin.cred:type_name -> pbx.ClientCred
+ 15, // 12: pbx.ClientSub.set_query:type_name -> pbx.SetQuery
+ 14, // 13: pbx.ClientSub.get_query:type_name -> pbx.GetQuery
+ 49, // 14: pbx.ClientPub.head:type_name -> pbx.ClientPub.HeadEntry
+ 14, // 15: pbx.ClientGet.query:type_name -> pbx.GetQuery
+ 15, // 16: pbx.ClientSet.query:type_name -> pbx.SetQuery
+ 5, // 17: pbx.ClientDel.what:type_name -> pbx.ClientDel.What
+ 16, // 18: pbx.ClientDel.del_seq:type_name -> pbx.SeqRange
+ 11, // 19: pbx.ClientDel.cred:type_name -> pbx.ClientCred
+ 1, // 20: pbx.ClientNote.what:type_name -> pbx.InfoNote
+ 2, // 21: pbx.ClientNote.event:type_name -> pbx.CallEvent
+ 0, // 22: pbx.ClientExtra.auth_level:type_name -> pbx.AuthLevel
+ 17, // 23: pbx.ClientMsg.hi:type_name -> pbx.ClientHi
+ 18, // 24: pbx.ClientMsg.acc:type_name -> pbx.ClientAcc
+ 19, // 25: pbx.ClientMsg.login:type_name -> pbx.ClientLogin
+ 20, // 26: pbx.ClientMsg.sub:type_name -> pbx.ClientSub
+ 21, // 27: pbx.ClientMsg.leave:type_name -> pbx.ClientLeave
+ 22, // 28: pbx.ClientMsg.pub:type_name -> pbx.ClientPub
+ 23, // 29: pbx.ClientMsg.get:type_name -> pbx.ClientGet
+ 24, // 30: pbx.ClientMsg.set:type_name -> pbx.ClientSet
+ 25, // 31: pbx.ClientMsg.del:type_name -> pbx.ClientDel
+ 26, // 32: pbx.ClientMsg.note:type_name -> pbx.ClientNote
+ 27, // 33: pbx.ClientMsg.extra:type_name -> pbx.ClientExtra
+ 8, // 34: pbx.TopicDesc.defacs:type_name -> pbx.DefaultAcsMode
+ 9, // 35: pbx.TopicDesc.acs:type_name -> pbx.AccessMode
+ 9, // 36: pbx.TopicSub.acs:type_name -> pbx.AccessMode
+ 16, // 37: pbx.DelValues.del_seq:type_name -> pbx.SeqRange
+ 50, // 38: pbx.ServerCtrl.params:type_name -> pbx.ServerCtrl.ParamsEntry
+ 51, // 39: pbx.ServerData.head:type_name -> pbx.ServerData.HeadEntry
+ 6, // 40: pbx.ServerPres.what:type_name -> pbx.ServerPres.What
+ 16, // 41: pbx.ServerPres.del_seq:type_name -> pbx.SeqRange
+ 9, // 42: pbx.ServerPres.acs:type_name -> pbx.AccessMode
+ 30, // 43: pbx.ServerMeta.desc:type_name -> pbx.TopicDesc
+ 31, // 44: pbx.ServerMeta.sub:type_name -> pbx.TopicSub
+ 32, // 45: pbx.ServerMeta.del:type_name -> pbx.DelValues
+ 29, // 46: pbx.ServerMeta.cred:type_name -> pbx.ServerCred
+ 1, // 47: pbx.ServerInfo.what:type_name -> pbx.InfoNote
+ 2, // 48: pbx.ServerInfo.event:type_name -> pbx.CallEvent
+ 33, // 49: pbx.ServerMsg.ctrl:type_name -> pbx.ServerCtrl
+ 34, // 50: pbx.ServerMsg.data:type_name -> pbx.ServerData
+ 35, // 51: pbx.ServerMsg.pres:type_name -> pbx.ServerPres
+ 36, // 52: pbx.ServerMsg.meta:type_name -> pbx.ServerMeta
+ 37, // 53: pbx.ServerMsg.info:type_name -> pbx.ServerInfo
+ 3, // 54: pbx.ServerResp.status:type_name -> pbx.RespCode
+ 38, // 55: pbx.ServerResp.srvmsg:type_name -> pbx.ServerMsg
+ 28, // 56: pbx.ServerResp.clmsg:type_name -> pbx.ClientMsg
+ 0, // 57: pbx.Session.auth_level:type_name -> pbx.AuthLevel
+ 28, // 58: pbx.ClientReq.msg:type_name -> pbx.ClientMsg
+ 40, // 59: pbx.ClientReq.sess:type_name -> pbx.Session
+ 3, // 60: pbx.SearchFound.status:type_name -> pbx.RespCode
+ 31, // 61: pbx.SearchFound.result:type_name -> pbx.TopicSub
+ 4, // 62: pbx.TopicEvent.action:type_name -> pbx.Crud
+ 30, // 63: pbx.TopicEvent.desc:type_name -> pbx.TopicDesc
+ 4, // 64: pbx.AccountEvent.action:type_name -> pbx.Crud
+ 8, // 65: pbx.AccountEvent.default_acs:type_name -> pbx.DefaultAcsMode
+ 4, // 66: pbx.SubscriptionEvent.action:type_name -> pbx.Crud
+ 9, // 67: pbx.SubscriptionEvent.mode:type_name -> pbx.AccessMode
+ 4, // 68: pbx.MessageEvent.action:type_name -> pbx.Crud
+ 34, // 69: pbx.MessageEvent.msg:type_name -> pbx.ServerData
+ 28, // 70: pbx.Node.MessageLoop:input_type -> pbx.ClientMsg
+ 41, // 71: pbx.Plugin.FireHose:input_type -> pbx.ClientReq
+ 42, // 72: pbx.Plugin.Find:input_type -> pbx.SearchQuery
+ 45, // 73: pbx.Plugin.Account:input_type -> pbx.AccountEvent
+ 44, // 74: pbx.Plugin.Topic:input_type -> pbx.TopicEvent
+ 46, // 75: pbx.Plugin.Subscription:input_type -> pbx.SubscriptionEvent
+ 47, // 76: pbx.Plugin.Message:input_type -> pbx.MessageEvent
+ 38, // 77: pbx.Node.MessageLoop:output_type -> pbx.ServerMsg
+ 39, // 78: pbx.Plugin.FireHose:output_type -> pbx.ServerResp
+ 43, // 79: pbx.Plugin.Find:output_type -> pbx.SearchFound
+ 7, // 80: pbx.Plugin.Account:output_type -> pbx.Unused
+ 7, // 81: pbx.Plugin.Topic:output_type -> pbx.Unused
+ 7, // 82: pbx.Plugin.Subscription:output_type -> pbx.Unused
+ 7, // 83: pbx.Plugin.Message:output_type -> pbx.Unused
+ 77, // [77:84] is the sub-list for method output_type
+ 70, // [70:77] is the sub-list for method input_type
+ 70, // [70:70] is the sub-list for extension type_name
+ 70, // [70:70] is the sub-list for extension extendee
+ 0, // [0:70] is the sub-list for field type_name
}
func init() { file_model_proto_init() }
diff --git a/pbx/model.proto b/pbx/model.proto
index 398c7a818..75d6a33db 100644
--- a/pbx/model.proto
+++ b/pbx/model.proto
@@ -172,6 +172,11 @@ message ClientAcc {
bytes token = 9;
// Account state: normal ("ok"), suspended
string state = 10;
+ // AuthLevel
+ AuthLevel auth_level = 11;
+ // Temporary auth params for one-off actions like password reset.
+ string tmp_scheme = 12;
+ bytes tmp_secret = 13;
}
// Login {login} message
@@ -349,6 +354,7 @@ message TopicDesc {
int64 state_at = 13;
bytes trusted = 14;
bool is_chan = 17; // 17!
+ bool online = 18;
// P2P only: other user's last online timestamp & user agent
int64 last_seen_time = 15;
@@ -486,8 +492,9 @@ message ServerMsg {
ServerMeta meta = 4;
ServerInfo info = 5;
}
+ // DEPRECATED. Will be removed soon.
// When response is sent to Root, send internal topic name too.
- string topic = 6;
+ string topic = 6 [deprecated = true];
}
// Plugin response codes
diff --git a/pbx/model_grpc.pb.go b/pbx/model_grpc.pb.go
index 20fba8c97..f70ffc8b6 100644
--- a/pbx/model_grpc.pb.go
+++ b/pbx/model_grpc.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
-// - protoc-gen-go-grpc v1.2.0
-// - protoc v3.21.6
+// - protoc-gen-go-grpc v1.3.0
+// - protoc v3.21.12
// source: model.proto
package pbx
@@ -18,6 +18,10 @@ import (
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
+const (
+ Node_MessageLoop_FullMethodName = "/pbx.Node/MessageLoop"
+)
+
// NodeClient is the client API for Node service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
@@ -35,7 +39,7 @@ func NewNodeClient(cc grpc.ClientConnInterface) NodeClient {
}
func (c *nodeClient) MessageLoop(ctx context.Context, opts ...grpc.CallOption) (Node_MessageLoopClient, error) {
- stream, err := c.cc.NewStream(ctx, &Node_ServiceDesc.Streams[0], "/pbx.Node/MessageLoop", opts...)
+ stream, err := c.cc.NewStream(ctx, &Node_ServiceDesc.Streams[0], Node_MessageLoop_FullMethodName, opts...)
if err != nil {
return nil, err
}
@@ -138,6 +142,15 @@ var Node_ServiceDesc = grpc.ServiceDesc{
Metadata: "model.proto",
}
+const (
+ Plugin_FireHose_FullMethodName = "/pbx.Plugin/FireHose"
+ Plugin_Find_FullMethodName = "/pbx.Plugin/Find"
+ Plugin_Account_FullMethodName = "/pbx.Plugin/Account"
+ Plugin_Topic_FullMethodName = "/pbx.Plugin/Topic"
+ Plugin_Subscription_FullMethodName = "/pbx.Plugin/Subscription"
+ Plugin_Message_FullMethodName = "/pbx.Plugin/Message"
+)
+
// PluginClient is the client API for Plugin service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
@@ -171,7 +184,7 @@ func NewPluginClient(cc grpc.ClientConnInterface) PluginClient {
func (c *pluginClient) FireHose(ctx context.Context, in *ClientReq, opts ...grpc.CallOption) (*ServerResp, error) {
out := new(ServerResp)
- err := c.cc.Invoke(ctx, "/pbx.Plugin/FireHose", in, out, opts...)
+ err := c.cc.Invoke(ctx, Plugin_FireHose_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@@ -180,7 +193,7 @@ func (c *pluginClient) FireHose(ctx context.Context, in *ClientReq, opts ...grpc
func (c *pluginClient) Find(ctx context.Context, in *SearchQuery, opts ...grpc.CallOption) (*SearchFound, error) {
out := new(SearchFound)
- err := c.cc.Invoke(ctx, "/pbx.Plugin/Find", in, out, opts...)
+ err := c.cc.Invoke(ctx, Plugin_Find_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@@ -189,7 +202,7 @@ func (c *pluginClient) Find(ctx context.Context, in *SearchQuery, opts ...grpc.C
func (c *pluginClient) Account(ctx context.Context, in *AccountEvent, opts ...grpc.CallOption) (*Unused, error) {
out := new(Unused)
- err := c.cc.Invoke(ctx, "/pbx.Plugin/Account", in, out, opts...)
+ err := c.cc.Invoke(ctx, Plugin_Account_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@@ -198,7 +211,7 @@ func (c *pluginClient) Account(ctx context.Context, in *AccountEvent, opts ...gr
func (c *pluginClient) Topic(ctx context.Context, in *TopicEvent, opts ...grpc.CallOption) (*Unused, error) {
out := new(Unused)
- err := c.cc.Invoke(ctx, "/pbx.Plugin/Topic", in, out, opts...)
+ err := c.cc.Invoke(ctx, Plugin_Topic_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@@ -207,7 +220,7 @@ func (c *pluginClient) Topic(ctx context.Context, in *TopicEvent, opts ...grpc.C
func (c *pluginClient) Subscription(ctx context.Context, in *SubscriptionEvent, opts ...grpc.CallOption) (*Unused, error) {
out := new(Unused)
- err := c.cc.Invoke(ctx, "/pbx.Plugin/Subscription", in, out, opts...)
+ err := c.cc.Invoke(ctx, Plugin_Subscription_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@@ -216,7 +229,7 @@ func (c *pluginClient) Subscription(ctx context.Context, in *SubscriptionEvent,
func (c *pluginClient) Message(ctx context.Context, in *MessageEvent, opts ...grpc.CallOption) (*Unused, error) {
out := new(Unused)
- err := c.cc.Invoke(ctx, "/pbx.Plugin/Message", in, out, opts...)
+ err := c.cc.Invoke(ctx, Plugin_Message_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@@ -292,7 +305,7 @@ func _Plugin_FireHose_Handler(srv interface{}, ctx context.Context, dec func(int
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/pbx.Plugin/FireHose",
+ FullMethod: Plugin_FireHose_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PluginServer).FireHose(ctx, req.(*ClientReq))
@@ -310,7 +323,7 @@ func _Plugin_Find_Handler(srv interface{}, ctx context.Context, dec func(interfa
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/pbx.Plugin/Find",
+ FullMethod: Plugin_Find_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PluginServer).Find(ctx, req.(*SearchQuery))
@@ -328,7 +341,7 @@ func _Plugin_Account_Handler(srv interface{}, ctx context.Context, dec func(inte
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/pbx.Plugin/Account",
+ FullMethod: Plugin_Account_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PluginServer).Account(ctx, req.(*AccountEvent))
@@ -346,7 +359,7 @@ func _Plugin_Topic_Handler(srv interface{}, ctx context.Context, dec func(interf
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/pbx.Plugin/Topic",
+ FullMethod: Plugin_Topic_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PluginServer).Topic(ctx, req.(*TopicEvent))
@@ -364,7 +377,7 @@ func _Plugin_Subscription_Handler(srv interface{}, ctx context.Context, dec func
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/pbx.Plugin/Subscription",
+ FullMethod: Plugin_Subscription_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PluginServer).Subscription(ctx, req.(*SubscriptionEvent))
@@ -382,7 +395,7 @@ func _Plugin_Message_Handler(srv interface{}, ctx context.Context, dec func(inte
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: "/pbx.Plugin/Message",
+ FullMethod: Plugin_Message_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(PluginServer).Message(ctx, req.(*MessageEvent))
diff --git a/server/auth/auth.go b/server/auth/auth.go
index 1c26e7446..a7aaa50e8 100644
--- a/server/auth/auth.go
+++ b/server/auth/auth.go
@@ -218,6 +218,8 @@ type Rec struct {
Tags []string `json:"tags,omitempty"`
// User account state received or read by the authenticator.
State types.ObjState
+ // Credential 'method:value' associated with this record.
+ Credential string `json:"cred,omitempty"`
// Authenticator may request the server to create a new account.
// These are the account parameters which can be used for creating the account.
diff --git a/server/auth/code/auth_code.go b/server/auth/code/auth_code.go
new file mode 100644
index 000000000..4ae0ebc44
--- /dev/null
+++ b/server/auth/code/auth_code.go
@@ -0,0 +1,209 @@
+// Package code implements temporary no-login authentication by short numeric code.
+package code
+
+import (
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "math/big"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/tinode/chat/server/auth"
+ "github.com/tinode/chat/server/store"
+ "github.com/tinode/chat/server/store/types"
+)
+
+// authenticator is a singleton instance of the authenticator.
+type authenticator struct {
+ name string
+ codeLength int
+ maxCodeValue *big.Int
+ lifetime time.Duration
+ maxRetries int
+}
+
+// Init initializes the authenticator: parses the config and sets internal state.
+func (ca *authenticator) Init(jsonconf json.RawMessage, name string) error {
+ if name == "" {
+ return errors.New("auth_code: authenticator name cannot be blank")
+ }
+
+ if ca.name != "" {
+ return errors.New("auth_code: already initialized as " + ca.name + "; " + name)
+ }
+
+ type configType struct {
+ // Length of the security code.
+ CodeLength int `json:"code_length"`
+ // Code expiration time in seconds.
+ ExpireIn int `json:"expire_in"`
+ // Maximum number of verification attempts per code.
+ MaxRetries int `json:"max_retries"`
+ }
+ var config configType
+ if err := json.Unmarshal(jsonconf, &config); err != nil {
+ return errors.New("auth_code: failed to parse config: " + err.Error() + "(" + string(jsonconf) + ")")
+ }
+
+ if config.ExpireIn <= 0 {
+ return errors.New("auth_code: invalid expiration period")
+ }
+
+ if config.CodeLength < 4 {
+ return errors.New("auth_code: invalid code length")
+ }
+
+ if config.MaxRetries < 1 {
+ return errors.New("auth_code: invalid reties count")
+ }
+
+ ca.name = name
+ ca.codeLength = config.CodeLength
+ ca.maxCodeValue = big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(ca.codeLength)), nil)
+ ca.lifetime = time.Duration(config.ExpireIn) * time.Second
+ ca.maxRetries = config.MaxRetries
+
+ return nil
+}
+
+// IsInitialized returns true if the handler is initialized.
+func (ca *authenticator) IsInitialized() bool {
+ return ca.name != ""
+}
+
+// AddRecord is not supported, will produce an error.
+func (authenticator) AddRecord(rec *auth.Rec, secret []byte, remoteAddr string) (*auth.Rec, error) {
+ return nil, types.ErrUnsupported
+}
+
+// UpdateRecord is not supported, will produce an error.
+func (authenticator) UpdateRecord(rec *auth.Rec, secret []byte, remoteAddr string) (*auth.Rec, error) {
+ return nil, types.ErrUnsupported
+}
+
+// Authenticate checks validity of provided short code.
+// The secret is structured as ::, "123456:email:alice@example.com".
+func (ca *authenticator) Authenticate(secret []byte, remoteAddr string, sdkKey string) (*auth.Rec, []byte, error) {
+ parts := strings.SplitN(string(secret), ":", 2)
+ if len(parts) != 2 {
+ return nil, nil, types.ErrMalformed
+ }
+
+ code, cred := parts[0], parts[1]
+ key := sanitizeKey(realName + "_" + cred)
+
+ value, err := store.PCache.Get(key)
+ if err != nil {
+ if err == types.ErrNotFound {
+ err = types.ErrFailed
+ }
+ return nil, nil, err
+ }
+
+ // code:count:uid
+ parts = strings.Split(value, ":")
+ if len(parts) != 3 {
+ return nil, nil, types.ErrInternal
+ }
+
+ count, err := strconv.Atoi(parts[1])
+ if err != nil {
+ return nil, nil, types.ErrInternal
+ }
+
+ if count >= ca.maxRetries {
+ return nil, nil, types.ErrFailed
+ }
+
+ if parts[0] != code {
+ // Update count of attempts. If the update fails, the error is ignored.
+ store.PCache.Upsert(key, parts[0]+":"+strconv.Itoa(count+1)+":"+parts[2], false)
+ return nil, nil, types.ErrFailed
+ }
+
+ // Success. Remove no longer needed entry. The error is ignored here.
+ store.PCache.Delete(key)
+
+ return &auth.Rec{
+ Uid: types.ParseUid(parts[2]),
+ AuthLevel: auth.LevelNone,
+ Lifetime: auth.Duration(ca.lifetime),
+ Features: auth.FeatureNoLogin,
+ State: types.StateUndefined,
+ Credential: cred}, nil, nil
+}
+
+// GenSecret generates a new code.
+func (ca *authenticator) GenSecret(rec *auth.Rec) ([]byte, time.Time, error) {
+ // Run garbage collection.
+ store.PCache.Expire(realName+"_", time.Now().UTC().Add(-ca.lifetime))
+
+ // Generate random code.
+ code, err := rand.Int(rand.Reader, ca.maxCodeValue)
+ if err != nil {
+ return nil, time.Time{}, types.ErrInternal
+ }
+
+ // Convert the code to fixed length string.
+ resp := strconv.FormatInt(code.Int64(), 10)
+ resp = strings.Repeat("0", ca.codeLength-len(resp)) + resp
+
+ if rec.Lifetime == 0 {
+ rec.Lifetime = auth.Duration(ca.lifetime)
+ } else if rec.Lifetime < 0 {
+ return nil, time.Time{}, types.ErrExpired
+ }
+
+ // Save "code:counter:uid" to the database. The key is code_.
+ if err = store.PCache.Upsert(sanitizeKey(realName+"_"+rec.Credential), resp+":0:"+rec.Uid.String(), true); err != nil {
+ return nil, time.Time{}, err
+ }
+
+ expires := time.Now().Add(time.Duration(rec.Lifetime)).UTC().Round(time.Millisecond)
+
+ return []byte(resp), expires, nil
+}
+
+// AsTag is not supported, will produce an empty string.
+func (authenticator) AsTag(token string) string {
+ return ""
+}
+
+// IsUnique is not supported, will produce an error.
+func (authenticator) IsUnique(secret []byte, remoteAddr string) (bool, error) {
+ return false, types.ErrUnsupported
+}
+
+// DelRecords adds disabled user ID to a stop list.
+func (authenticator) DelRecords(uid types.Uid) error {
+ return nil
+}
+
+// RestrictedTags returns tag namespaces restricted by this authenticator (none for short code).
+func (authenticator) RestrictedTags() ([]string, error) {
+ return nil, nil
+}
+
+// GetResetParams returns authenticator parameters passed to password reset handler
+// (none for short code).
+func (authenticator) GetResetParams(uid types.Uid) (map[string]interface{}, error) {
+ return nil, nil
+}
+
+// Replace all occurences of % with / to ensure SQL LIKE query works correctly.
+func sanitizeKey(key string) string {
+ return strings.ReplaceAll(key, "%", "/")
+}
+
+const realName = "code"
+
+// GetRealName returns the hardcoded name of the authenticator.
+func (authenticator) GetRealName() string {
+ return realName
+}
+
+func init() {
+ store.RegisterAuthScheme(realName, &authenticator{})
+}
diff --git a/server/auth/token/auth_token.go b/server/auth/token/auth_token.go
index 017fe1278..f33564703 100644
--- a/server/auth/token/auth_token.go
+++ b/server/auth/token/auth_token.go
@@ -81,7 +81,7 @@ func (ta *authenticator) IsInitialized() bool {
return ta.name != ""
}
-// AddRecord is not supprted, will produce an error.
+// AddRecord is not supported, will produce an error.
func (authenticator) AddRecord(rec *auth.Rec, secret []byte, remoteAddr string) (*auth.Rec, error) {
return nil, types.ErrUnsupported
}
diff --git a/server/calls.go b/server/calls.go
index a1e7ee583..bf01fd538 100644
--- a/server/calls.go
+++ b/server/calls.go
@@ -83,9 +83,9 @@ type videoCall struct {
// Call message seq ID.
seq int
// Call message content.
- content interface{}
+ content any
// Call message content mime type.
- contentMime interface{}
+ contentMime any
// Time when the call was accepted.
acceptedAt time.Time
}
@@ -133,26 +133,28 @@ func initVideoCalls(jsconfig json.RawMessage) error {
globals.iceServers = config.ICEServers
} else if config.ICEServersFile != "" {
var iceConfig []iceServer
- if file, err := os.Open(config.ICEServersFile); err != nil {
+ file, err := os.Open(config.ICEServersFile)
+ if err != nil {
return fmt.Errorf("failed to read ICE config: %w", err)
- } else {
- jr := jcr.New(file)
- if err = json.NewDecoder(jr).Decode(&iceConfig); err != nil {
- switch jerr := err.(type) {
- case *json.UnmarshalTypeError:
- lnum, cnum, _ := jr.LineAndChar(jerr.Offset)
- return fmt.Errorf("unmarshall error in ICE config in %s at %d:%d (offset %d bytes): %w",
- jerr.Field, lnum, cnum, jerr.Offset, jerr)
- case *json.SyntaxError:
- lnum, cnum, _ := jr.LineAndChar(jerr.Offset)
- return fmt.Errorf("syntax error in config file at %d:%d (offset %d bytes): %w",
- lnum, cnum, jerr.Offset, jerr)
- default:
- return fmt.Errorf("failed to parse config file: %w", err)
- }
+ }
+
+ jr := jcr.New(file)
+ if err = json.NewDecoder(jr).Decode(&iceConfig); err != nil {
+ switch jerr := err.(type) {
+ case *json.UnmarshalTypeError:
+ lnum, cnum, _ := jr.LineAndChar(jerr.Offset)
+ return fmt.Errorf("unmarshall error in ICE config in %s at %d:%d (offset %d bytes): %w",
+ jerr.Field, lnum, cnum, jerr.Offset, jerr)
+ case *json.SyntaxError:
+ lnum, cnum, _ := jr.LineAndChar(jerr.Offset)
+ return fmt.Errorf("syntax error in config file at %d:%d (offset %d bytes): %w",
+ lnum, cnum, jerr.Offset, jerr)
+ default:
+ return fmt.Errorf("failed to parse config file: %w", err)
}
- file.Close()
}
+ file.Close()
+
globals.iceServers = iceConfig
}
@@ -169,13 +171,20 @@ func initVideoCalls(jsconfig json.RawMessage) error {
return nil
}
-func (call *videoCall) messageHead(newState string, duration int) map[string]interface{} {
- head := map[string]interface{}{
- "replace": ":" + strconv.Itoa(call.seq),
- "webrtc": newState,
+// Add webRTC-related headers to message Head. The original Head may already contain some entries,
+// like 'sender', preserve them.
+func (call *videoCall) messageHead(head map[string]any, newState string, duration int) map[string]any {
+ if head == nil {
+ head = map[string]any{}
}
+
+ head["replace"] = ":" + strconv.Itoa(call.seq)
+ head["webrtc"] = newState
+
if duration > 0 {
head["webrtc-duration"] = duration
+ } else {
+ delete(head, "webrtc-duration")
}
if call.contentMime != nil {
head["mime"] = call.contentMime
@@ -249,8 +258,7 @@ func (t *Topic) handleCallEvent(msg *ClientComMessage) {
asUid := types.ParseUserId(msg.AsUser)
- _, userFound := t.perUser[asUid]
- if !userFound {
+ if _, userFound := t.perUser[asUid]; !userFound {
// User not found in topic.
logs.Warn.Printf("topic[%s]: could not find user %s", t.name, asUid.UserId())
return
@@ -280,10 +288,14 @@ func (t *Topic) handleCallEvent(msg *ClientComMessage) {
if call.Event == constCallEventAccept {
// The call has been accepted.
// Send a replacement {data} message to the topic.
- replaceWith := constCallMsgAccepted
- head := t.currentCall.messageHead(replaceWith, 0)
msgCopy := *msg
msgCopy.AsUser = originatorUid.UserId()
+ replaceWith := constCallMsgAccepted
+ var origHead map[string]any
+ if msgCopy.Pub != nil {
+ origHead = msgCopy.Pub.Head
+ } // else fetch the original message from store and use its head.
+ head := t.currentCall.messageHead(origHead, replaceWith, 0)
if err := t.saveAndBroadcastMessage(&msgCopy, originatorUid, false, nil,
head, t.currentCall.content); err != nil {
return
@@ -373,7 +385,7 @@ func (t *Topic) maybeEndCallInProgress(from string, msg *ClientComMessage, callD
if from != "" && len(t.currentCall.parties) == 2 {
// This is a call in progress.
replaceWith = constCallMsgFinished
- callDuration = time.Now().Sub(t.currentCall.acceptedAt).Milliseconds()
+ callDuration = time.Since(t.currentCall.acceptedAt).Milliseconds()
} else {
if from != "" {
// User originated hang-up.
@@ -396,9 +408,13 @@ func (t *Topic) maybeEndCallInProgress(from string, msg *ClientComMessage, callD
}
// Send a message indicating the call has ended.
- head := t.currentCall.messageHead(replaceWith, int(callDuration))
msgCopy := *msg
msgCopy.AsUser = originatorUid.UserId()
+ var origHead map[string]any
+ if msgCopy.Pub != nil {
+ origHead = msgCopy.Pub.Head
+ } // else fetch the original message from store and use its head.
+ head := t.currentCall.messageHead(origHead, replaceWith, int(callDuration))
if err := t.saveAndBroadcastMessage(&msgCopy, originatorUid, false, nil, head, t.currentCall.content); err != nil {
logs.Err.Printf("topic[%s]: failed to write finalizing message for call seq id %d - '%s'", t.name, t.currentCall.seq, err)
}
diff --git a/server/cluster.go b/server/cluster.go
index 1059d885d..b7449e394 100644
--- a/server/cluster.go
+++ b/server/cluster.go
@@ -29,8 +29,6 @@ const (
clusterHashReplicas = 20
// Buffer size for sending requests from proxy to master.
clusterProxyToMasterBuffer = 512
- // Buffer size for master to proxy answers, per node.
- clusterMasterToProxyBuffer = 128
// Buffer size for receiving responses from other nodes, per node.
clusterRpcCompletionBuffer = 512
)
@@ -305,7 +303,7 @@ func (n *ClusterNode) reconnect() {
}
}
-func (n *ClusterNode) call(proc string, req, resp interface{}) error {
+func (n *ClusterNode) call(proc string, req, resp any) error {
if !n.connected {
return errors.New("cluster: node '" + n.name + "' not connected")
}
@@ -341,7 +339,7 @@ func (n *ClusterNode) handleRpcResponse(call *rpc.Call) {
}
}
-func (n *ClusterNode) callAsync(proc string, req, resp interface{}, done chan *rpc.Call) *rpc.Call {
+func (n *ClusterNode) callAsync(proc string, req, resp any, done chan *rpc.Call) *rpc.Call {
if done != nil && cap(done) == 0 {
logs.Err.Panic("cluster: RPC done channel is unbuffered")
}
@@ -966,8 +964,8 @@ func clusterInit(configString json.RawMessage, self *string) int {
return 1
}
- gob.Register([]interface{}{})
- gob.Register(map[string]interface{}{})
+ gob.Register([]any{})
+ gob.Register(map[string]any{})
gob.Register(map[string]int{})
gob.Register(map[string]string{})
gob.Register(MsgAccessMode{})
@@ -1116,7 +1114,7 @@ func (c *Cluster) rehash(nodes []string) []string {
// TODO: consider resubscribing to topics instead of forcing sessions to resubscribe.
func (c *Cluster) invalidateProxySubs(forNode string) {
sessions := make(map[*Session][]string)
- globals.hub.topics.Range(func(_, v interface{}) bool {
+ globals.hub.topics.Range(func(_, v any) bool {
topic := v.(*Topic)
if !topic.isProxy {
// Topic isn't a proxy.
diff --git a/server/datamodel.go b/server/datamodel.go
index 79d9c5e91..b05d56344 100644
--- a/server/datamodel.go
+++ b/server/datamodel.go
@@ -63,9 +63,9 @@ type MsgSetSub struct {
// MsgSetDesc is a C2S in set.what == "desc", acc, sub message.
type MsgSetDesc struct {
DefaultAcs *MsgDefaultAcsMode `json:"defacs,omitempty"` // default access mode
- Public interface{} `json:"public,omitempty"` // description of the user or topic
- Trusted interface{} `json:"trusted,omitempty"` // trusted (system-provided) user or topic data
- Private interface{} `json:"private,omitempty"` // per-subscription private data
+ Public any `json:"public,omitempty"` // description of the user or topic
+ Trusted any `json:"trusted,omitempty"` // trusted (system-provided) user or topic data
+ Private any `json:"private,omitempty"` // per-subscription private data
}
// MsgCredClient is an account credential such as email or phone number.
@@ -77,7 +77,7 @@ type MsgCredClient struct {
// Verification response
Response string `json:"resp,omitempty"`
// Request parameters, such as preferences. Passed to valiator without interpretation.
- Params map[string]interface{} `json:"params,omitempty"`
+ Params map[string]any `json:"params,omitempty"`
}
// MsgSetQuery is an update to topic or user metadata: description, subscriptions, tags, credentials.
@@ -127,13 +127,14 @@ type MsgClientAcc struct {
Id string `json:"id,omitempty"`
// "newXYZ" to create a new user or UserId to update a user; default: current user.
User string `json:"user,omitempty"`
+ // Temporary authentication parameters for one-off actions, like password reset.
+ TmpScheme string `json:"tmpscheme,omitempty"`
+ TmpSecret []byte `json:"tmpsecret,omitempty"`
// Account state: normal, suspended.
State string `json:"status,omitempty"`
// Authentication level of the user when UserID is set and not equal to the current user.
// Either "", "auth" or "anon". Default: ""
AuthLevel string `json:"authlevel,omitempty"`
- // Authentication token for resetting the password and maybe other one-time actions.
- Token []byte `json:"token,omitempty"`
// The initial authentication scheme the account can use
Scheme string `json:"scheme,omitempty"`
// Shared secret
@@ -259,11 +260,11 @@ type MsgClientLeave struct {
// MsgClientPub is client's request to publish data to topic subscribers {pub}.
type MsgClientPub struct {
- Id string `json:"id,omitempty"`
- Topic string `json:"topic"`
- NoEcho bool `json:"noecho,omitempty"`
- Head map[string]interface{} `json:"head,omitempty"`
- Content interface{} `json:"content"`
+ Id string `json:"id,omitempty"`
+ Topic string `json:"topic"`
+ NoEcho bool `json:"noecho,omitempty"`
+ Head map[string]any `json:"head,omitempty"`
+ Content any `json:"content"`
}
// MsgClientGet is a query of topic state {get}.
@@ -446,11 +447,11 @@ type MsgTopicDesc struct {
ReadSeqId int `json:"read,omitempty"`
RecvSeqId int `json:"recv,omitempty"`
// Id of the last delete operation as seen by the requesting user
- DelId int `json:"clear,omitempty"`
- Public interface{} `json:"public,omitempty"`
- Trusted interface{} `json:"trusted,omitempty"`
+ DelId int `json:"clear,omitempty"`
+ Public any `json:"public,omitempty"`
+ Trusted any `json:"trusted,omitempty"`
// Per-subscription private data
- Private interface{} `json:"private,omitempty"`
+ Private any `json:"private,omitempty"`
}
func (src *MsgTopicDesc) describe() string {
@@ -508,11 +509,11 @@ type MsgTopicSub struct {
// ID of the message reported by the given user as received
RecvSeqId int `json:"recv,omitempty"`
// Topic's public data
- Public interface{} `json:"public,omitempty"`
+ Public any `json:"public,omitempty"`
// Topic's trusted public data
- Trusted interface{} `json:"trusted,omitempty"`
+ Trusted any `json:"trusted,omitempty"`
// User's own private data per topic
- Private interface{} `json:"private,omitempty"`
+ Private any `json:"private,omitempty"`
// Response to non-'me' topic
@@ -575,9 +576,9 @@ type MsgDelValues struct {
// MsgServerCtrl is a server control message {ctrl}.
type MsgServerCtrl struct {
- Id string `json:"id,omitempty"`
- Topic string `json:"topic,omitempty"`
- Params interface{} `json:"params,omitempty"`
+ Id string `json:"id,omitempty"`
+ Topic string `json:"topic,omitempty"`
+ Params any `json:"params,omitempty"`
Code int `json:"code"`
Text string `json:"text,omitempty"`
@@ -601,12 +602,12 @@ func (src *MsgServerCtrl) describe() string {
type MsgServerData struct {
Topic string `json:"topic"`
// ID of the user who originated the message as {pub}, could be empty if sent by the system
- From string `json:"from,omitempty"`
- Timestamp time.Time `json:"ts"`
- DeletedAt *time.Time `json:"deleted,omitempty"`
- SeqId int `json:"seq"`
- Head map[string]interface{} `json:"head,omitempty"`
- Content interface{} `json:"content"`
+ From string `json:"from,omitempty"`
+ Timestamp time.Time `json:"ts"`
+ DeletedAt *time.Time `json:"deleted,omitempty"`
+ SeqId int `json:"seq"`
+ Head map[string]any `json:"head,omitempty"`
+ Content any `json:"content"`
}
// Deep-shallow copy.
@@ -907,13 +908,13 @@ func NoErrReply(msg *ClientComMessage, ts time.Time) *ServerComMessage {
}
// NoErrParams indicates successful completion with additional parameters (200).
-func NoErrParams(id, topic string, ts time.Time, params interface{}) *ServerComMessage {
+func NoErrParams(id, topic string, ts time.Time, params any) *ServerComMessage {
return NoErrParamsExplicitTs(id, topic, ts, ts, params)
}
// NoErrParamsExplicitTs indicates successful completion with additional parameters
// and explicit server and incoming request timestamps (200).
-func NoErrParamsExplicitTs(id, topic string, serverTs, incomingReqTs time.Time, params interface{}) *ServerComMessage {
+func NoErrParamsExplicitTs(id, topic string, serverTs, incomingReqTs time.Time, params any) *ServerComMessage {
return &ServerComMessage{
Ctrl: &MsgServerCtrl{
Id: id,
@@ -930,7 +931,7 @@ func NoErrParamsExplicitTs(id, topic string, serverTs, incomingReqTs time.Time,
// NoErrParamsReply indicates successful completion with additional parameters
// and explicit server and incoming request timestamps (200).
-func NoErrParamsReply(msg *ClientComMessage, ts time.Time, params interface{}) *ServerComMessage {
+func NoErrParamsReply(msg *ClientComMessage, ts time.Time, params any) *ServerComMessage {
return NoErrParamsExplicitTs(msg.Id, msg.Original, ts, msg.Timestamp, params)
}
@@ -970,7 +971,7 @@ func NoErrAcceptedExplicitTs(id, topic string, serverTs, incomingReqTs time.Time
}
// NoContentParams indicates request was processed but resulted in no content (204).
-func NoContentParams(id, topic string, serverTs, incomingReqTs time.Time, params interface{}) *ServerComMessage {
+func NoContentParams(id, topic string, serverTs, incomingReqTs time.Time, params any) *ServerComMessage {
return &ServerComMessage{
Ctrl: &MsgServerCtrl{
Id: id,
@@ -987,7 +988,7 @@ func NoContentParams(id, topic string, serverTs, incomingReqTs time.Time, params
// NoContentParamsReply indicates request was processed but resulted in no content
// in response to a client request (204).
-func NoContentParamsReply(msg *ClientComMessage, ts time.Time, params interface{}) *ServerComMessage {
+func NoContentParamsReply(msg *ClientComMessage, ts time.Time, params any) *ServerComMessage {
return NoContentParams(msg.Id, msg.Original, ts, msg.Timestamp, params)
}
@@ -1016,7 +1017,7 @@ func NoErrShutdown(ts time.Time) *ServerComMessage {
}
// NoErrDeliveredParams means requested content has been delivered (208).
-func NoErrDeliveredParams(id, topic string, ts time.Time, params interface{}) *ServerComMessage {
+func NoErrDeliveredParams(id, topic string, ts time.Time, params any) *ServerComMessage {
return &ServerComMessage{
Ctrl: &MsgServerCtrl{
Id: id,
@@ -1059,7 +1060,7 @@ func InfoChallenge(id string, ts time.Time, challenge []byte) *ServerComMessage
Id: id,
Code: http.StatusMultipleChoices, // 300
Text: "challenge",
- Params: map[string]interface{}{"challenge": challenge},
+ Params: map[string]any{"challenge": challenge},
Timestamp: ts,
},
Id: id,
diff --git a/server/db/adapter.go b/server/db/adapter.go
index b381e4b5f..2a036b1bb 100644
--- a/server/db/adapter.go
+++ b/server/db/adapter.go
@@ -141,10 +141,10 @@ type Adapter interface {
// Search
- // FindUsers searches for new contacts given a list of tags
- FindUsers(user t.Uid, req [][]string, opt []string) ([]t.Subscription, error)
- // FindTopics searches for group topics given a list of tags
- FindTopics(req [][]string, opt []string) ([]t.Subscription, error)
+ // FindUsers searches for new contacts given a list of tags.
+ FindUsers(user t.Uid, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error)
+ // FindTopics searches for group topics given a list of tags.
+ FindTopics(req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error)
// Messages
@@ -181,4 +181,15 @@ type Adapter interface {
FileDeleteUnused(olderThan time.Time, limit int) ([]string, error)
// FileLinkAttachments connects given topic or message to the file record IDs from the list.
FileLinkAttachments(topic string, userId, msgId t.Uid, fids []string) error
+
+ // Persistent cache management.
+
+ // PCacheGet reads a persistent cache entry.
+ PCacheGet(key string) (string, error)
+ // PCacheUpsert creates or updates a persistent cache entry.
+ PCacheUpsert(key string, value string, failOnDuplicate bool) error
+ // PCacheDelete deletes a single persistent cache entry.
+ PCacheDelete(key string) error
+ // PCacheExpire expires older entries with the specified key prefix.
+ PCacheExpire(keyPrefix string, olderThan time.Time) error
}
diff --git a/server/db/mongodb/adapter.go b/server/db/mongodb/adapter.go
index 0c0d56149..e875fac86 100644
--- a/server/db/mongodb/adapter.go
+++ b/server/db/mongodb/adapter.go
@@ -42,7 +42,7 @@ const (
defaultHost = "localhost:27017"
defaultDatabase = "tinode"
- adpVersion = 112
+ adpVersion = 113
adapterName = "mongodb"
defaultMaxResults = 1024
@@ -324,6 +324,11 @@ func (a *adapter) CreateDb(reset bool) error {
SetPartialFilterExpression(b.M{"devices.deviceid": b.M{"$exists": true}}),
},
},
+ // Index on lastSeen and updatedat for deleting stale user accounts.
+ {
+ Collection: "users",
+ IndexOpts: mdb.IndexModel{Keys: b.D{{"lastseen", 1}, {"updatedat", 1}}},
+ },
// User authentication records {_id, userid, secret}
// Should be able to access user's auth records by user id
@@ -515,6 +520,18 @@ func (a *adapter) UpgradeDb() error {
}
}
+ if a.version == 112 {
+ // Create secondary index on Users(lastseen,updatedat) for deleting stale user accounts.
+ if _, err = a.db.Collection("users").Indexes().CreateOne(a.ctx,
+ mdb.IndexModel{Keys: b.D{{"lastseen", 1}, {"updatedat", 1}}}); err != nil {
+ return err
+ }
+
+ if err := bumpVersion(a, 113); err != nil {
+ return err
+ }
+ }
+
if a.version != adpVersion {
return errors.New("Failed to perform database upgrade to version " + strconv.Itoa(adpVersion) +
". DB is still at " + strconv.Itoa(a.version))
@@ -2068,7 +2085,7 @@ func (a *adapter) subsDelete(ctx context.Context, filter b.M, hard bool) error {
}
// Search
-func (a *adapter) getFindPipeline(req [][]string, opt []string) (map[string]struct{}, b.A) {
+func (a *adapter) getFindPipeline(req [][]string, opt []string, activeOnly bool) (map[string]struct{}, b.A) {
allReq := t.FlattenDoubleSlice(req)
index := make(map[string]struct{})
var allTags []interface{}
@@ -2077,11 +2094,12 @@ func (a *adapter) getFindPipeline(req [][]string, opt []string) (map[string]stru
index[tag] = struct{}{}
}
+ matchOn := b.M{"tags": b.M{"$in": allTags}}
+ if activeOnly {
+ matchOn["state"] = b.M{"$eq": t.StateOK}
+ }
pipeline := b.A{
- b.M{"$match": b.M{
- "tags": b.M{"$in": allTags},
- "state": b.M{"$ne": t.StateDeleted},
- }},
+ b.M{"$match": matchOn},
b.M{"$project": b.M{"_id": 1, "access": 1, "createdat": 1, "updatedat": 1, "public": 1, "trusted": 1, "tags": 1}},
@@ -2118,8 +2136,8 @@ func (a *adapter) getFindPipeline(req [][]string, opt []string) (map[string]stru
}
// FindUsers searches for new contacts given a list of tags
-func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string) ([]t.Subscription, error) {
- index, pipeline := a.getFindPipeline(req, opt)
+func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
+ index, pipeline := a.getFindPipeline(req, opt, activeOnly)
cur, err := a.db.Collection("users").Aggregate(a.ctx, pipeline)
if err != nil {
return nil, err
@@ -2157,8 +2175,8 @@ func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string) ([]t.Subscr
}
// FindTopics searches for group topics given a list of tags
-func (a *adapter) FindTopics(req [][]string, opt []string) ([]t.Subscription, error) {
- index, pipeline := a.getFindPipeline(req, opt)
+func (a *adapter) FindTopics(req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
+ index, pipeline := a.getFindPipeline(req, opt, activeOnly)
cur, err := a.db.Collection("topics").Aggregate(a.ctx, pipeline)
if err != nil {
return nil, err
@@ -2684,6 +2702,63 @@ func (a *adapter) FileLinkAttachments(topic string, userId, msgId t.Uid, fids []
return err
}
+// PCacheGet reads a persistet cache entry.
+func (a *adapter) PCacheGet(key string) (string, error) {
+ var value map[string]string
+ findOpts := mdbopts.FindOneOptions{Projection: b.M{"value": 1, "_id": 0}}
+ if err := a.db.Collection("kvmeta").FindOne(a.ctx, b.M{"_id": key}, &findOpts).Decode(&value); err != nil {
+ if err == mdb.ErrNoDocuments {
+ err = t.ErrNotFound
+ }
+ return "", err
+ }
+ return value["value"], nil
+}
+
+// PCacheUpsert creates or updates a persistent cache entry.
+func (a *adapter) PCacheUpsert(key string, value string, failOnDuplicate bool) error {
+ if strings.Contains(key, "^") {
+ // Do not allow ^ in keys: it interferes with $match query.
+ return t.ErrMalformed
+ }
+
+ collection := a.db.Collection("kvmeta")
+ doc := b.M{
+ "value": value,
+ }
+
+ if failOnDuplicate {
+ doc["_id"] = key
+ doc["createdat"] = t.TimeNow()
+ _, err := collection.InsertOne(a.ctx, doc)
+ if mdb.IsDuplicateKeyError(err) {
+ err = t.ErrDuplicate
+ }
+ return err
+ }
+
+ res := collection.FindOneAndUpdate(a.ctx, b.M{"_id": key}, b.M{"$set": doc},
+ mdbopts.FindOneAndUpdate().SetUpsert(true))
+ return res.Err()
+}
+
+// PCacheDelete deletes one persistent cache entry.
+func (a *adapter) PCacheDelete(key string) error {
+ _, err := a.db.Collection("kvmeta").DeleteOne(a.ctx, b.M{"_id": key})
+ return err
+}
+
+// PCacheExpire expires old entries with the given key prefix.
+func (a *adapter) PCacheExpire(keyPrefix string, olderThan time.Time) error {
+ if keyPrefix == "" {
+ return t.ErrMalformed
+ }
+
+ _, err := a.db.Collection("kvmeta").DeleteMany(a.ctx, b.M{"createdat": b.M{"$lt": olderThan},
+ "_id": primitive.Regex{Pattern: "^" + keyPrefix}})
+ return err
+}
+
func (a *adapter) isDbInitialized() bool {
var result map[string]int
diff --git a/server/db/mongodb/tests/mongo_test.go b/server/db/mongodb/tests/mongo_test.go
index daae34af0..869923e86 100644
--- a/server/db/mongodb/tests/mongo_test.go
+++ b/server/db/mongodb/tests/mongo_test.go
@@ -523,7 +523,7 @@ func TestSubsForTopic(t *testing.T) {
func TestFindUsers(t *testing.T) {
reqTags := [][]string{{"alice", "bob", "carol"}}
- gotSubs, err := adp.FindUsers(types.ParseUserId("usr"+users[2].Id), reqTags, nil)
+ gotSubs, err := adp.FindUsers(types.ParseUserId("usr"+users[2].Id), reqTags, nil, false)
if err != nil {
t.Error(err)
}
@@ -534,7 +534,7 @@ func TestFindUsers(t *testing.T) {
func TestFindTopics(t *testing.T) {
reqTags := [][]string{{"travel", "qwer", "asdf", "zxcv"}}
- gotSubs, err := adp.FindTopics(reqTags, nil)
+ gotSubs, err := adp.FindTopics(reqTags, nil, false)
if err != nil {
t.Error(err)
}
diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go
index af6f2e42a..c658940f0 100644
--- a/server/db/mysql/adapter.go
+++ b/server/db/mysql/adapter.go
@@ -43,7 +43,7 @@ const (
defaultDSN = "root:@tcp(localhost:3306)/tinode?parseTime=true"
defaultDatabase = "tinode"
- adpVersion = 112
+ adpVersion = 113
adapterName = "mysql"
@@ -328,7 +328,8 @@ func (a *adapter) CreateDb(reset bool) error {
trusted JSON,
tags JSON,
PRIMARY KEY(id),
- INDEX users_state_stateat(state, stateat)
+ INDEX users_state_stateat(state, stateat),
+ INDEX users_lastseen_updatedat(lastseen, updatedat)
)`); err != nil {
return err
}
@@ -549,13 +550,15 @@ func (a *adapter) CreateDb(reset bool) error {
if _, err = tx.Exec(
`CREATE TABLE kvmeta(` +
- "`key` CHAR(32)," +
- "`value` TEXT," +
- "PRIMARY KEY(`key`)" +
+ "`key` VARCHAR(64) NOT NULL," +
+ "createdat DATETIME(3)," +
+ "`value` TEXT," +
+ "PRIMARY KEY(`key`)," +
+ "INDEX kvmeta_createdat_key(createdat, `key`)" +
`)`); err != nil {
return err
}
- if _, err = tx.Exec("INSERT INTO kvmeta(`key`, `value`) VALUES('version', ?)", adpVersion); err != nil {
+ if _, err = tx.Exec("INSERT INTO kvmeta(`key`, `value`) VALUES('version',?)", adpVersion); err != nil {
return err
}
@@ -737,6 +740,34 @@ func (a *adapter) UpgradeDb() error {
}
}
+ if a.version == 112 {
+ // Perform database upgrade from version 112 to version 113.
+
+ // Index for deleting unvalidated accounts.
+ if _, err := a.db.Exec("ALTER TABLE users ADD INDEX users_lastseen_updatedat(lastseen,updatedat)"); err != nil {
+ return err
+ }
+
+ // Add timestamp to kvmeta.
+ if _, err := a.db.Exec("ALTER TABLE kvmeta MODIFY `key` VARCHAR(64) NOT NULL"); err != nil {
+ return err
+ }
+
+ // Add timestamp to kvmeta.
+ if _, err := a.db.Exec("ALTER TABLE kvmeta ADD createdat DATETIME(3) AFTER `key`"); err != nil {
+ return err
+ }
+
+ // Add compound index on the new field and key (could be searched by key prefix).
+ if _, err := a.db.Exec("ALTER TABLE kvmeta ADD INDEX kvmeta_createdat_key(createdat, `key`)"); err != nil {
+ return err
+ }
+
+ if err := bumpVersion(a, 113); err != nil {
+ return err
+ }
+ }
+
if a.version != adpVersion {
return errors.New("Failed to perform database upgrade to version " + strconv.Itoa(adpVersion) +
". DB is still at " + strconv.Itoa(a.version))
@@ -2310,10 +2341,14 @@ func (a *adapter) SubsDelForUser(user t.Uid, hard bool) error {
// Returns a list of users who match given tags, such as "email:jdoe@example.com" or "tel:+18003287448".
// Searching the 'users.Tags' for the given tags using respective index.
-func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string) ([]t.Subscription, error) {
+func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
index := make(map[string]struct{})
var args []interface{}
- args = append(args, t.StateOK)
+ stateConstraint := ""
+ if activeOnly {
+ args = append(args, t.StateOK)
+ stateConstraint = "u.state=? AND "
+ }
allReq := t.FlattenDoubleSlice(req)
for _, tag := range append(allReq, opt...) {
args = append(args, tag)
@@ -2322,7 +2357,7 @@ func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string) ([]t.Subscr
query := "SELECT u.id,u.createdat,u.updatedat,u.access,u.public,u.trusted,u.tags,COUNT(*) AS matches " +
"FROM users AS u LEFT JOIN usertags AS t ON t.userid=u.id " +
- "WHERE u.state=? AND t.tag IN (?" + strings.Repeat(",?", len(allReq)+len(opt)-1) + ") " +
+ "WHERE " + stateConstraint + "t.tag IN (?" + strings.Repeat(",?", len(allReq)+len(opt)-1) + ") " +
"GROUP BY u.id,u.createdat,u.updatedat,u.access,u.public,u.trusted,u.tags "
if len(allReq) > 0 {
query += "HAVING"
@@ -2398,10 +2433,14 @@ func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string) ([]t.Subscr
// Returns a list of topics with matching tags.
// Searching the 'topics.Tags' for the given tags using respective index.
-func (a *adapter) FindTopics(req [][]string, opt []string) ([]t.Subscription, error) {
+func (a *adapter) FindTopics(req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
index := make(map[string]struct{})
var args []interface{}
- args = append(args, t.StateOK)
+ stateConstraint := ""
+ if activeOnly {
+ args = append(args, t.StateOK)
+ stateConstraint = "t.state=? AND "
+ }
var allReq []string
for _, el := range req {
allReq = append(allReq, el...)
@@ -2413,7 +2452,7 @@ func (a *adapter) FindTopics(req [][]string, opt []string) ([]t.Subscription, er
query := "SELECT t.name AS topic,t.createdat,t.updatedat,t.usebt,t.access,t.public,t.trusted,t.tags,COUNT(*) AS matches " +
"FROM topics AS t LEFT JOIN topictags AS tt ON t.name=tt.topic " +
- "WHERE t.state=? AND tt.tag IN (?" + strings.Repeat(",?", len(allReq)+len(opt)-1) + ") " +
+ "WHERE " + stateConstraint + "tt.tag IN (?" + strings.Repeat(",?", len(allReq)+len(opt)-1) + ") " +
"GROUP BY t.name,t.createdat,t.updatedat,t.usebt,t.access,t.public,t.trusted,t.tags "
if len(allReq) > 0 {
query += "HAVING"
@@ -2623,7 +2662,7 @@ func (a *adapter) MessageGetDeleted(topic string, forUser t.Uid, opts *t.QueryOp
if dellog.Hi <= dellog.Low+1 {
dellog.Hi = 0
}
- dmsg.SeqIdRanges = append(dmsg.SeqIdRanges, t.Range{dellog.Low, dellog.Hi})
+ dmsg.SeqIdRanges = append(dmsg.SeqIdRanges, t.Range{Low: dellog.Low, Hi: dellog.Hi})
}
if err == nil {
err = rows.Err()
@@ -3348,6 +3387,75 @@ func (a *adapter) FileLinkAttachments(topic string, userId, msgId t.Uid, fids []
return tx.Commit()
}
+// PCacheGet reads a persistet cache entry.
+func (a *adapter) PCacheGet(key string) (string, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ var value string
+ if err := a.db.GetContext(ctx, &value, "SELECT `value` FROM kvmeta WHERE `key`=? LIMIT 1", key); err != nil {
+ if err == sql.ErrNoRows {
+ return "", t.ErrNotFound
+ }
+ return "", err
+ }
+ return value, nil
+}
+
+// PCacheUpsert creates or updates a persistent cache entry.
+func (a *adapter) PCacheUpsert(key string, value string, failOnDuplicate bool) error {
+ if strings.Contains(key, "%") {
+ // Do not allow % in keys: it interferes with LIKE query.
+ return t.ErrMalformed
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ var action string
+ if failOnDuplicate {
+ action = "INSERT"
+ } else {
+ action = "REPLACE"
+ }
+
+ _, err := a.db.ExecContext(ctx, action+" INTO kvmeta(`key`,createdat,`value`) VALUES(?,?,?)", key, t.TimeNow(), value)
+ if isDupe(err) {
+ return t.ErrDuplicate
+ }
+ return err
+}
+
+// PCacheDelete deletes one persistent cache entry.
+func (a *adapter) PCacheDelete(key string) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ _, err := a.db.ExecContext(ctx, "DELETE FROM kvmeta WHERE `key`=?")
+ return err
+}
+
+// PCacheExpire expires old entries with the given key prefix.
+func (a *adapter) PCacheExpire(keyPrefix string, olderThan time.Time) error {
+ if keyPrefix == "" {
+ return t.ErrMalformed
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ _, err := a.db.ExecContext(ctx, "DELETE FROM kvmeta WHERE `key` LIKE ? AND createdat", keyPrefix+"%", olderThan)
+ return err
+}
+
// Helper functions
// Check if MySQL error is a Error Code: 1062. Duplicate entry ... for key ...
diff --git a/server/db/mysql/schema.sql b/server/db/mysql/schema.sql
index aa5714478..2e014a4e5 100644
--- a/server/db/mysql/schema.sql
+++ b/server/db/mysql/schema.sql
@@ -16,9 +16,11 @@ USE tinode;
CREATE TABLE kvmeta(
- `key` CHAR(32),
+ `key` VARCHAR(64),
+ createdat DATETIME(3),
`value` TEXT,
- PRIMARY KEY(`key`)
+ PRIMARY KEY(`key`),
+ INDEX kvmeta_createdat_key(createdat, `key`)
);
INSERT INTO kvmeta(`key`, `value`) VALUES("version", "100");
@@ -34,9 +36,10 @@ CREATE TABLE users(
useragent VARCHAR(255) DEFAULT '',
public JSON,
tags JSON, -- Denormalized array of tags
-
+
PRIMARY KEY(id),
- INDEX users_state_stateat(state, stateat)
+ INDEX users_state_stateat(state, stateat),
+ INDEX users_lastseen_updatedat(lastseen, updatedat)
);
# Indexed user tags.
@@ -44,7 +47,7 @@ CREATE TABLE usertags(
id INT NOT NULL AUTO_INCREMENT,
userid BIGINT NOT NULL,
tag VARCHAR(96) NOT NULL,
-
+
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
INDEX usertags_tag(tag),
@@ -60,7 +63,7 @@ CREATE TABLE devices(
platform VARCHAR(32),
lastseen DATETIME NOT NULL,
lang VARCHAR(8),
-
+
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
UNIQUE INDEX devices_hash(hash)
@@ -75,7 +78,7 @@ CREATE TABLE auth(
authlvl SMALLINT NOT NULL,
secret VARCHAR(255) NOT NULL,
expires DATETIME,
-
+
PRIMARY KEY(id),
FOREIGN KEY(userid) REFERENCES users(id),
UNIQUE INDEX auth_userid_scheme(userid, scheme),
@@ -99,7 +102,7 @@ CREATE TABLE topics(
delid INT DEFAULT 0,
public JSON,
tags JSON, -- Denormalized array of tags
-
+
PRIMARY KEY(id),
UNIQUE INDEX topics_name (name),
INDEX topics_owner(owner),
@@ -111,7 +114,7 @@ CREATE TABLE topictags(
id INT NOT NULL AUTO_INCREMENT,
topic CHAR(25) NOT NULL,
tag VARCHAR(96) NOT NULL,
-
+
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
INDEX topictags_tag (tag),
@@ -132,7 +135,7 @@ CREATE TABLE subscriptions(
modewant CHAR(8),
modegiven CHAR(8),
private JSON,
-
+
PRIMARY KEY(id) ,
FOREIGN KEY(userid) REFERENCES users(id),
UNIQUE INDEX subscriptions_topic_userid(topic, userid),
@@ -152,7 +155,7 @@ CREATE TABLE messages(
`from` BIGINT NOT NULL,
head JSON,
content JSON,
-
+
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
UNIQUE INDEX messages_topic_seqid (topic, seqid)
@@ -166,13 +169,13 @@ CREATE TABLE dellog(
delid INT NOT NULL,
low INT NOT NULL,
hi INT NOT NULL,
-
+
PRIMARY KEY(id),
FOREIGN KEY(topic) REFERENCES topics(name),
# For getting the list of deleted message ranges
INDEX dellog_topic_delid_deletedfor(topic,delid,deletedfor),
# Used when getting not-yet-deleted messages(messages LEFT JOIN dellog)
- INDEX dellog_topic_deletedfor_low_hi(topic,deletedfor,low,hi),
+ INDEX dellog_topic_deletedfor_low_hi(topic,deletedfor,low,hi),
# Used when deleting a user
INDEX dellog_deletedfor(deletedfor)
);
@@ -190,7 +193,7 @@ CREATE TABLE credentials(
resp VARCHAR(255) NOT NULL,
done TINYINT NOT NULL DEFAULT 0,
retries INT NOT NULL DEFAULT 0,
-
+
PRIMARY KEY(id),
UNIQUE credentials_uniqueness(synthetic),
FOREIGN KEY(userid) REFERENCES users(id),
@@ -206,7 +209,7 @@ CREATE TABLE fileuploads(
mimetype VARCHAR(255) NOT NULL,
size BIGINT NOT NULL,
location VARCHAR(2048) NOT NULL,
-
+
PRIMARY KEY(id),
INDEX fileuploads_status(status)
);
@@ -225,4 +228,4 @@ CREATE TABLE filemsglinks(
FOREIGN KEY(msgid) REFERENCES messages(id) ON DELETE CASCADE,
FOREIGN KEY(topicid) REFERENCES topics(id) ON DELETE CASCADE,
FOREIGN KEY(userid) REFERENCES users(id) ON DELETE CASCADE
-);
\ No newline at end of file
+);
diff --git a/server/db/postgres/adapter.go b/server/db/postgres/adapter.go
new file mode 100644
index 000000000..40d5149e3
--- /dev/null
+++ b/server/db/postgres/adapter.go
@@ -0,0 +1,3541 @@
+//go:build postgres
+// +build postgres
+
+// Package postgres is a database adapter for PostgreSQL.
+package postgres
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "hash/fnv"
+ "log"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/jackc/pgconn"
+ "github.com/jackc/pgx/v4"
+ "github.com/jackc/pgx/v4/pgxpool"
+ "github.com/jmoiron/sqlx"
+ "github.com/tinode/chat/server/auth"
+ "github.com/tinode/chat/server/db/common"
+ "github.com/tinode/chat/server/store"
+ t "github.com/tinode/chat/server/store/types"
+)
+
+// adapter holds MySQL connection data.
+type adapter struct {
+ db *pgxpool.Pool
+ poolConfig *pgxpool.Config
+ dsn string
+ dbName string
+ // Maximum number of records to return
+ maxResults int
+ // Maximum number of message records to return
+ maxMessageResults int
+ version int
+
+ // Single query timeout.
+ sqlTimeout time.Duration
+ // DB transaction timeout.
+ txTimeout time.Duration
+}
+
+const (
+ defaultDSN = "postgresql://postgres:postgres@localhost:5432/tinode?sslmode=disable&connect_timeout=10"
+ defaultDatabase = "tinode"
+
+ adpVersion = 113
+ adapterName = "postgres"
+
+ defaultMaxResults = 1024
+ // This is capped by the Session's send queue limit (128).
+ defaultMaxMessageResults = 100
+
+ // If DB request timeout is specified,
+ // we allocate txTimeoutMultiplier times more time for transactions.
+ txTimeoutMultiplier = 1.5
+)
+
+type configType struct {
+ // DB connection settings:
+ // Using fields
+ User string `json:"user,omitempty"`
+ Passwd string `json:"passwd,omitempty"`
+ Host string `json:"host,omitempty"`
+ Port string `json:"port,omitempty"`
+ DBName string `json:"dbname,omitempty"`
+ // Deprecated.
+ DSN string `json:"dsn,omitempty"`
+ Database string `json:"database,omitempty"`
+
+ // Connection pool settings.
+ //
+ // Maximum number of open connections to the database.
+ MaxOpenConns int `json:"max_open_conns,omitempty"`
+ // Maximum number of connections in the idle connection pool.
+ MaxIdleConns int `json:"max_idle_conns,omitempty"`
+ // Maximum amount of time a connection may be reused (in seconds).
+ ConnMaxLifetime int `json:"conn_max_lifetime,omitempty"`
+
+ // DB request timeout (in seconds).
+ // If 0 (or negative), no timeout is applied.
+ SqlTimeout int `json:"sql_timeout,omitempty"`
+}
+
+func (a *adapter) getContext() (context.Context, context.CancelFunc) {
+ if a.sqlTimeout > 0 {
+ return context.WithTimeout(context.Background(), a.sqlTimeout)
+ }
+ return context.Background(), nil
+}
+
+func (a *adapter) getContextForTx() (context.Context, context.CancelFunc) {
+ if a.txTimeout > 0 {
+ return context.WithTimeout(context.Background(), a.txTimeout)
+ }
+ return context.Background(), nil
+}
+
+// Open initializes database session
+func (a *adapter) Open(jsonconfig json.RawMessage) error {
+ if a.db != nil {
+ return errors.New("postgres adapter is already connected")
+ }
+
+ if len(jsonconfig) < 2 {
+ return errors.New("adapter postgres missing config")
+ }
+
+ var err error
+ var config configType
+ ctx := context.Background()
+ if err = json.Unmarshal(jsonconfig, &config); err != nil {
+ return errors.New("postgres adapter failed to parse config: " + err.Error())
+ }
+
+ if config.DSN != "" {
+ a.dsn = config.DSN
+ a.dbName = config.Database
+ } else {
+ dsn, err := setConnStr(config)
+ if err != nil {
+ return err
+ }
+ a.dsn = dsn
+ a.dbName = config.DBName
+ }
+
+ if a.dsn == "" {
+ a.dsn = defaultDSN
+ }
+
+ if a.dbName == "" {
+ a.dbName = defaultDatabase
+ }
+
+ if a.maxResults <= 0 {
+ a.maxResults = defaultMaxResults
+ }
+
+ if a.maxMessageResults <= 0 {
+ a.maxMessageResults = defaultMaxMessageResults
+ }
+
+ a.poolConfig, err = pgxpool.ParseConfig(a.dsn)
+ if err != nil {
+ return errors.New("adapter postgres failed to parse config: " + err.Error())
+ }
+
+ // ConnectConfig creates a new Pool and immediately establishes one connection.
+ a.db, err = pgxpool.ConnectConfig(ctx, a.poolConfig)
+ if isMissingDb(err) {
+ // Missing DB is OK if we are initializing the database.
+ // Since tinode DB does not exist, connect without specifying the DB name.
+ a.poolConfig.ConnConfig.Database = ""
+ a.db, err = pgxpool.ConnectConfig(ctx, a.poolConfig)
+ }
+ if err != nil {
+ return err
+ }
+
+ // Actually opening the network connection if one was not opened earlier.
+ if a.poolConfig.LazyConnect {
+ err = a.db.Ping(ctx)
+ }
+
+ if err == nil {
+ if config.MaxOpenConns > 0 {
+ a.poolConfig.MaxConns = int32(config.MaxOpenConns)
+ }
+ if config.MaxIdleConns > 0 {
+ a.poolConfig.MinConns = int32(config.MaxIdleConns)
+ }
+ if config.ConnMaxLifetime > 0 {
+ a.poolConfig.MaxConnLifetime = time.Duration(config.ConnMaxLifetime) * time.Second
+ }
+ if config.SqlTimeout > 0 {
+ a.sqlTimeout = time.Duration(config.SqlTimeout) * time.Second
+ // We allocate txTimeoutMultiplier times sqlTimeout for transactions.
+ a.txTimeout = time.Duration(float64(config.SqlTimeout)*txTimeoutMultiplier) * time.Second
+ }
+ }
+ return err
+}
+
+// Close closes the underlying database connection
+func (a *adapter) Close() error {
+ if a.db != nil {
+ a.db.Close()
+ a.db = nil
+ a.version = -1
+ }
+ return nil
+}
+
+// IsOpen returns true if connection to database has been established. It does not check if
+// connection is actually live.
+func (a *adapter) IsOpen() bool {
+ return a.db != nil
+}
+
+// GetDbVersion returns current database version.
+func (a *adapter) GetDbVersion() (int, error) {
+ if a.version > 0 {
+ return a.version, nil
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ var vers string
+ err := a.db.QueryRow(ctx, "SELECT value FROM kvmeta WHERE key = $1", "version").Scan(&vers)
+
+ if err != nil {
+ if isMissingDb(err) || isMissingTable(err) || err == pgx.ErrNoRows {
+ err = errors.New("Database not initialized")
+ }
+ return -1, err
+ }
+
+ a.version, _ = strconv.Atoi(vers)
+
+ return a.version, nil
+}
+
+func (a *adapter) updateDbVersion(v int) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ a.version = -1
+ if _, err := a.db.Exec(ctx, "UPDATE kvmeta SET value = $1 WHERE key = $2", strconv.Itoa(v), "version"); err != nil {
+ return err
+ }
+ return nil
+}
+
+// CheckDbVersion checks whether the actual DB version matches the expected version of this adapter.
+func (a *adapter) CheckDbVersion() error {
+ version, err := a.GetDbVersion()
+ if err != nil {
+ return err
+ }
+
+ if version != adpVersion {
+ return errors.New("Invalid database version " + strconv.Itoa(version) +
+ ". Expected " + strconv.Itoa(adpVersion))
+ }
+
+ return nil
+}
+
+// Version returns adapter version.
+func (adapter) Version() int {
+ return adpVersion
+}
+
+// DB connection stats object.
+func (a *adapter) Stats() interface{} {
+ if a.db == nil {
+ return nil
+ }
+ return a.db.Stat()
+}
+
+// GetName returns string that adapter uses to register itself with store.
+func (a *adapter) GetName() string {
+ return adapterName
+}
+
+// SetMaxResults configures how many results can be returned in a single DB call.
+func (a *adapter) SetMaxResults(val int) error {
+ if val <= 0 {
+ a.maxResults = defaultMaxResults
+ } else {
+ a.maxResults = val
+ }
+
+ return nil
+}
+
+// CreateDb initializes the storage.
+func (a *adapter) CreateDb(reset bool) error {
+ var err error
+ var tx pgx.Tx
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ // Can't use an existing connection because it's configured with a database name which may not exist.
+ // Don't care if it does not close cleanly.
+ if a.db != nil {
+ a.db.Close()
+ }
+
+ // Create default database name
+ a.poolConfig.ConnConfig.Database = "postgres"
+
+ a.db, err = pgxpool.ConnectConfig(ctx, a.poolConfig)
+ if err != nil {
+ return err
+ }
+
+ if reset {
+ if _, err = a.db.Exec(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS %s;", a.dbName)); err != nil {
+ return err
+ }
+ }
+
+ if _, err = a.db.Exec(ctx, fmt.Sprintf("CREATE DATABASE %s WITH ENCODING utf8;", a.dbName)); err != nil {
+ return err
+ }
+
+ a.poolConfig.ConnConfig.Database = a.dbName
+ a.db, err = pgxpool.ConnectConfig(ctx, a.poolConfig)
+ if err != nil {
+ return err
+ }
+
+ if tx, err = a.db.Begin(ctx); err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ // FIXME: This is useless: MySQL auto-commits on every CREATE TABLE.
+ // Maybe DROP DATABASE instead.
+ tx.Rollback(ctx)
+ }
+ }()
+
+ // Indexed users.
+ if _, err := tx.Exec(ctx,
+ `CREATE TABLE users(
+ id BIGINT NOT NULL,
+ createdat TIMESTAMP(3) NOT NULL,
+ updatedat TIMESTAMP(3) NOT NULL,
+ state SMALLINT NOT NULL DEFAULT 0,
+ stateat TIMESTAMP(3),
+ access JSON,
+ lastseen TIMESTAMP,
+ useragent VARCHAR(255) DEFAULT '',
+ public JSON,
+ trusted JSON,
+ tags JSON,
+ PRIMARY KEY(id)
+ );
+ CREATE INDEX users_state_stateat ON users(state, stateat);
+ CREATE INDEX users_lastseen_updatedat ON users(lastseen, updatedat);`); err != nil {
+ return err
+ }
+
+ // Indexed user tags.
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE usertags(
+ id SERIAL NOT NULL,
+ userid BIGINT NOT NULL,
+ tag VARCHAR(96) NOT NULL,
+ PRIMARY KEY(id),
+ FOREIGN KEY(userid) REFERENCES users(id)
+ );
+ CREATE INDEX usertags_tag ON usertags(tag);
+ CREATE UNIQUE INDEX usertags_userid_tag ON usertags(userid, tag);`); err != nil {
+ return err
+ }
+
+ // Indexed devices. Normalized into a separate table.
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE devices(
+ id SERIAL NOT NULL,
+ userid BIGINT NOT NULL,
+ hash CHAR(16) NOT NULL,
+ deviceid TEXT NOT NULL,
+ platform VARCHAR(32),
+ lastseen TIMESTAMP NOT NULL,
+ lang VARCHAR(8),
+ PRIMARY KEY(id),
+ FOREIGN KEY(userid) REFERENCES users(id)
+ );
+ CREATE UNIQUE INDEX devices_hash ON devices(hash);`); err != nil {
+ return err
+ }
+
+ // Authentication records for the basic authentication scheme.
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE auth(
+ id SERIAL NOT NULL,
+ uname VARCHAR(32) NOT NULL,
+ userid BIGINT NOT NULL,
+ scheme VARCHAR(16) NOT NULL,
+ authlvl INT NOT NULL,
+ secret VARCHAR(255) NOT NULL,
+ expires TIMESTAMP,
+ PRIMARY KEY(id),
+ FOREIGN KEY(userid) REFERENCES users(id)
+ );
+ CREATE UNIQUE INDEX auth_userid_scheme ON auth(userid, scheme);
+ CREATE UNIQUE INDEX auth_uname ON auth(uname);`); err != nil {
+ return err
+ }
+
+ // Topics
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE topics(
+ id SERIAL NOT NULL,
+ createdat TIMESTAMP(3) NOT NULL,
+ updatedat TIMESTAMP(3) NOT NULL,
+ state SMALLINT NOT NULL DEFAULT 0,
+ stateat TIMESTAMP(3),
+ touchedat TIMESTAMP(3),
+ name VARCHAR(25) NOT NULL,
+ usebt BOOLEAN DEFAULT FALSE,
+ owner BIGINT NOT NULL DEFAULT 0,
+ access JSON,
+ seqid INT NOT NULL DEFAULT 0,
+ delid INT DEFAULT 0,
+ public JSON,
+ trusted JSON,
+ tags JSON,
+ PRIMARY KEY(id)
+ );
+ CREATE UNIQUE INDEX topics_name ON topics(name);
+ CREATE INDEX topics_owner ON topics(owner);
+ CREATE INDEX topics_state_stateat ON topics(state, stateat);`); err != nil {
+ return err
+ }
+
+ // Create system topic 'sys'.
+ if err = createSystemTopic(tx); err != nil {
+ return err
+ }
+
+ // Indexed topic tags.
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE topictags(
+ id SERIAL NOT NULL,
+ topic VARCHAR(25) NOT NULL,
+ tag VARCHAR(96) NOT NULL,
+ PRIMARY KEY(id),
+ FOREIGN KEY(topic) REFERENCES topics(name)
+ );
+ CREATE INDEX topictags_tag ON topictags(tag);
+ CREATE UNIQUE INDEX topictags_userid_tag ON topictags(topic, tag);`); err != nil {
+ return err
+ }
+
+ // Subscriptions
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE subscriptions(
+ id SERIAL NOT NULL,
+ createdat TIMESTAMP(3) NOT NULL,
+ updatedat TIMESTAMP(3) NOT NULL,
+ deletedat TIMESTAMP(3),
+ userid BIGINT NOT NULL,
+ topic VARCHAR(25) NOT NULL,
+ delid INT DEFAULT 0,
+ recvseqid INT DEFAULT 0,
+ readseqid INT DEFAULT 0,
+ modewant VARCHAR(8),
+ modegiven VARCHAR(8),
+ private JSON,
+ PRIMARY KEY(id),
+ FOREIGN KEY(userid) REFERENCES users(id)
+ );
+ CREATE UNIQUE INDEX subscriptions_topic_userid ON subscriptions(topic, userid);
+ CREATE INDEX subscriptions_topic ON subscriptions(topic);
+ CREATE INDEX subscriptions_deletedat ON subscriptions(deletedat);`); err != nil {
+ return err
+ }
+
+ // Messages
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE messages(
+ id SERIAL NOT NULL,
+ createdat TIMESTAMP(3) NOT NULL,
+ updatedat TIMESTAMP(3) NOT NULL,
+ deletedat TIMESTAMP(3),
+ delid INT DEFAULT 0,
+ seqid INT NOT NULL,
+ topic VARCHAR(25) NOT NULL,
+ "from" BIGINT NOT NULL,
+ head JSON,
+ content JSON,
+ PRIMARY KEY(id),
+ FOREIGN KEY(topic) REFERENCES topics(name)
+ );
+ CREATE UNIQUE INDEX messages_topic_seqid ON messages(topic, seqid);`); err != nil {
+ return err
+ }
+
+ // Deletion log
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE dellog(
+ id SERIAL NOT NULL,
+ topic VARCHAR(25) NOT NULL,
+ deletedfor BIGINT NOT NULL DEFAULT 0,
+ delid INT NOT NULL,
+ low INT NOT NULL,
+ hi INT NOT NULL,
+ PRIMARY KEY(id),
+ FOREIGN KEY(topic) REFERENCES topics(name)
+ );
+ CREATE INDEX dellog_topic_delid_deletedfor ON dellog(topic,delid,deletedfor);
+ CREATE INDEX dellog_topic_deletedfor_low_hi ON dellog(topic,deletedfor,low,hi);
+ CREATE INDEX dellog_deletedfor ON dellog(deletedfor);`); err != nil {
+ return err
+ }
+
+ // User credentials
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE credentials(
+ id SERIAL NOT NULL,
+ createdat TIMESTAMP(3) NOT NULL,
+ updatedat TIMESTAMP(3) NOT NULL,
+ deletedat TIMESTAMP(3),
+ method VARCHAR(16) NOT NULL,
+ value VARCHAR(128) NOT NULL,
+ synthetic VARCHAR(192) NOT NULL,
+ userid BIGINT NOT NULL,
+ resp VARCHAR(255),
+ done BOOLEAN NOT NULL DEFAULT FALSE,
+ retries INT NOT NULL DEFAULT 0,
+ PRIMARY KEY(id),
+ FOREIGN KEY(userid) REFERENCES users(id)
+ );
+ CREATE UNIQUE INDEX credentials_uniqueness ON credentials(synthetic);`); err != nil {
+ return err
+ }
+
+ // Records of uploaded files.
+ // Don't add FOREIGN KEY on userid. It's not needed and it will break user deletion.
+ // Using INDEX rather than FK on topic because it's either 'topics' or 'users' reference.
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE fileuploads(
+ id BIGINT NOT NULL,
+ createdat TIMESTAMP(3) NOT NULL,
+ updatedat TIMESTAMP(3) NOT NULL,
+ userid BIGINT,
+ status INT NOT NULL,
+ mimetype VARCHAR(255) NOT NULL,
+ size BIGINT NOT NULL,
+ location VARCHAR(2048) NOT NULL,
+ PRIMARY KEY(id)
+ );
+ CREATE INDEX fileuploads_status ON fileuploads(status);`); err != nil {
+ return err
+ }
+
+ // Links between uploaded files and the topics, users or messages they are attached to.
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE filemsglinks(
+ id SERIAL NOT NULL,
+ createdat TIMESTAMP(3) NOT NULL,
+ fileid BIGINT NOT NULL,
+ msgid INT,
+ topic VARCHAR(25),
+ userid BIGINT,
+ PRIMARY KEY(id),
+ FOREIGN KEY(fileid) REFERENCES fileuploads(id) ON DELETE CASCADE,
+ FOREIGN KEY(msgid) REFERENCES messages(id) ON DELETE CASCADE,
+ FOREIGN KEY(topic) REFERENCES topics(name) ON DELETE CASCADE,
+ FOREIGN KEY(userid) REFERENCES users(id) ON DELETE CASCADE
+ );`); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(ctx,
+ `CREATE TABLE kvmeta(
+ "key" VARCHAR(64) NOT NULL,
+ createdat TIMESTAMP(3),
+ "value" TEXT,
+ PRIMARY KEY("key")
+ );
+ CREATE INDEX kvmeta_createdat_key ON kvmeta(createdat, "key");`); err != nil {
+ return err
+ }
+ if _, err = tx.Exec(ctx, `INSERT INTO kvmeta("key", "value") VALUES($1, $2)`, "version", strconv.Itoa(adpVersion)); err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+// UpgradeDb upgrades the database, if necessary.
+func (a *adapter) UpgradeDb() error {
+ bumpVersion := func(a *adapter, x int) error {
+ if err := a.updateDbVersion(x); err != nil {
+ return err
+ }
+ _, err := a.GetDbVersion()
+ return err
+ }
+
+ if _, err := a.GetDbVersion(); err != nil {
+ return err
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ if a.version == 112 {
+ // Perform database upgrade from version 112 to version 113.
+
+ // Index for deleting unvalidated accounts.
+ if _, err := a.db.Exec(ctx, "CREATE INDEX users_lastseen_updatedat ON users(lastseen,updatedat)"); err != nil {
+ return err
+ }
+
+ // Allow lnger kvmeta keys.
+ if _, err := a.db.Exec(ctx, `ALTER TABLE kvmeta ALTER COLUMN "key" TYPE VARCHAR(64)`); err != nil {
+ return err
+ }
+
+ if _, err := a.db.Exec(ctx, `ALTER TABLE kvmeta ALTER COLUMN "key" SET NOT NULL`); err != nil {
+ return err
+ }
+
+ // Add timestamp to kvmeta.
+ if _, err := a.db.Exec(ctx, `ALTER TABLE kvmeta ADD COLUMN createdat TIMESTAMP(3)`); err != nil {
+ return err
+ }
+
+ // Add compound index on the new field and key (could be searched by key prefix).
+ if _, err := a.db.Exec(ctx, `CREATE INDEX kvmeta_createdat_key ON kvmeta(createdat, "key")`); err != nil {
+ return err
+ }
+
+ if err := bumpVersion(a, 113); err != nil {
+ return err
+ }
+ }
+
+ if a.version != adpVersion {
+ return errors.New("Failed to perform database upgrade to version " + strconv.Itoa(adpVersion) +
+ ". DB is still at " + strconv.Itoa(a.version))
+ }
+ return nil
+}
+
+func createSystemTopic(tx pgx.Tx) error {
+ now := t.TimeNow()
+ query := `INSERT INTO topics(createdat,updatedat,state,touchedat,name,access,public)
+ VALUES($1,$2,$3,$4,'sys','{"Auth": "N","Anon": "N"}','{"fn": "System"}')`
+ _, err := tx.Exec(context.Background(), query, now, now, t.StateOK, now)
+ return err
+}
+
+func addTags(ctx context.Context, tx pgx.Tx, table, keyName string, keyVal interface{}, tags []string, ignoreDups bool) error {
+ if len(tags) == 0 {
+ return nil
+ }
+
+ sql := fmt.Sprintf("INSERT INTO %s (%s, tag) VALUES($1,$2)", table, keyName)
+
+ for _, tag := range tags {
+ if _, err := tx.Exec(ctx, sql, keyVal, tag); err != nil {
+ if isDupe(err) {
+ if ignoreDups {
+ continue
+ }
+ return t.ErrDuplicate
+ }
+ return err
+ }
+ }
+
+ return nil
+}
+
+func removeTags(ctx context.Context, tx pgx.Tx, table, keyName string, keyVal interface{}, tags []string) error {
+ if len(tags) == 0 {
+ return nil
+ }
+
+ sql, args := expandQuery(fmt.Sprintf("DELETE FROM %s WHERE %s=? AND tag = ANY (?)", table, keyName), keyVal, tags)
+ _, err := tx.Exec(ctx, sql, args)
+
+ return err
+}
+
+// UserCreate creates a new user. Returns error and true if error is due to duplicate user name,
+// false for any other error
+func (a *adapter) UserCreate(user *t.User) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ decoded_uid := store.DecodeUid(user.Uid())
+ if _, err = tx.Exec(ctx,
+ "INSERT INTO users(id,createdat,updatedat,state,access,public,trusted,tags) VALUES($1,$2,$3,$4,$5,$6,$7,$8);",
+ decoded_uid,
+ user.CreatedAt,
+ user.UpdatedAt,
+ user.State,
+ user.Access,
+ toJSON(user.Public),
+ toJSON(user.Trusted),
+ user.Tags); err != nil {
+ return err
+ }
+
+ // Save user's tags to a separate table to make user findable.
+ if err = addTags(ctx, tx, "usertags", "userid", decoded_uid, user.Tags, false); err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+// Add user's authentication record
+func (a *adapter) AuthAddRecord(uid t.Uid, scheme, unique string, authLvl auth.Level,
+ secret []byte, expires time.Time) error {
+
+ var exp *time.Time
+ if !expires.IsZero() {
+ exp = &expires
+ }
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ if _, err := a.db.Exec(ctx, "INSERT INTO auth(uname,userid,scheme,authLvl,secret,expires) VALUES($1,$2,$3,$4,$5,$6)",
+ unique, store.DecodeUid(uid), scheme, authLvl, secret, exp); err != nil {
+ if isDupe(err) {
+ return t.ErrDuplicate
+ }
+ return err
+ }
+ return nil
+}
+
+// AuthDelScheme deletes an existing authentication scheme for the user.
+func (a *adapter) AuthDelScheme(user t.Uid, scheme string) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ _, err := a.db.Exec(ctx, "DELETE FROM auth WHERE userid=$1 AND scheme=$2", store.DecodeUid(user), scheme)
+ return err
+}
+
+// AuthDelAllRecords deletes all authentication records for the user.
+func (a *adapter) AuthDelAllRecords(user t.Uid) (int, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ res, err := a.db.Exec(ctx, "DELETE FROM auth WHERE userid=$1", store.DecodeUid(user))
+ if err != nil {
+ return 0, err
+ }
+ count := res.RowsAffected()
+
+ return int(count), nil
+}
+
+// Update user's authentication unique, secret, auth level.
+func (a *adapter) AuthUpdRecord(uid t.Uid, scheme, unique string, authLvl auth.Level,
+ secret []byte, expires time.Time) error {
+
+ parapg := []string{"authLvl=?"}
+ args := []interface{}{authLvl}
+ if unique != "" {
+ parapg = append(parapg, "uname=?")
+ args = append(args, unique)
+ }
+ if len(secret) > 0 {
+ parapg = append(parapg, "secret=?")
+ args = append(args, secret)
+ }
+ if !expires.IsZero() {
+ parapg = append(parapg, "expires=?")
+ args = append(args, expires)
+ }
+ args = append(args, store.DecodeUid(uid), scheme)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ sql, args := expandQuery("UPDATE auth SET "+strings.Join(parapg, ",")+" WHERE userid=? AND scheme=?", args...)
+ resp, err := a.db.Exec(ctx, sql, args...)
+ if isDupe(err) {
+ return t.ErrDuplicate
+ }
+
+ if count := resp.RowsAffected(); count <= 0 {
+ return t.ErrNotFound
+ }
+
+ return err
+}
+
+// Retrieve user's authentication record
+func (a *adapter) AuthGetRecord(uid t.Uid, scheme string) (string, auth.Level, []byte, time.Time, error) {
+ var expires time.Time
+
+ var record struct {
+ Uname string
+ Authlvl auth.Level
+ Secret []byte
+ Expires *time.Time
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ if err := a.db.QueryRow(ctx, "SELECT uname,secret,expires,authlvl FROM auth WHERE userid=$1 AND scheme=$2",
+ store.DecodeUid(uid), scheme).Scan(
+ &record.Uname, &record.Secret, &record.Expires, &record.Authlvl); err != nil {
+ if err == pgx.ErrNoRows {
+ // Nothing found - use standard error.
+ err = t.ErrNotFound
+ }
+ return "", 0, nil, expires, err
+ }
+
+ if record.Expires != nil {
+ expires = *record.Expires
+ }
+
+ return record.Uname, record.Authlvl, record.Secret, expires, nil
+}
+
+// Retrieve user's authentication record
+func (a *adapter) AuthGetUniqueRecord(unique string) (t.Uid, auth.Level, []byte, time.Time, error) {
+ var expires time.Time
+
+ var record struct {
+ Userid int64
+ Authlvl auth.Level
+ Secret []byte
+ Expires *time.Time
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ if err := a.db.QueryRow(ctx, "SELECT userid,secret,expires,authlvl FROM auth WHERE uname=$1", unique).Scan(
+ &record.Userid, &record.Secret, &record.Expires, &record.Authlvl); err != nil {
+ if err == pgx.ErrNoRows {
+ // Nothing found - clear the error
+ err = nil
+ }
+ return t.ZeroUid, 0, nil, expires, err
+ }
+
+ if record.Expires != nil {
+ expires = *record.Expires
+ }
+
+ return store.EncodeUid(record.Userid), record.Authlvl, record.Secret, expires, nil
+}
+
+// UserGet fetches a single user by user id. If user is not found it returns (nil, nil)
+func (a *adapter) UserGet(uid t.Uid) (*t.User, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ var user t.User
+ var id int64
+
+ row, err := a.db.Query(ctx, "SELECT * FROM users WHERE id=$1 AND state!=$2", store.DecodeUid(uid), t.StateDeleted)
+ if err != nil {
+ return nil, err
+ }
+ defer row.Close()
+
+ if !row.Next() {
+ // Nothing found: user does not exist or marked as soft-deleted
+ return nil, nil
+ }
+
+ err = row.Scan(&id, &user.CreatedAt, &user.UpdatedAt, &user.State, &user.StateAt, &user.Access, &user.LastSeen, &user.UserAgent, &user.Public, &user.Trusted, &user.Tags)
+ if err == nil {
+ user.SetUid(uid)
+ return &user, nil
+ }
+
+ return nil, err
+}
+
+func (a *adapter) UserGetAll(ids ...t.Uid) ([]t.User, error) {
+ uids := make([]interface{}, len(ids))
+ for i, id := range ids {
+ uids[i] = store.DecodeUid(id)
+ }
+
+ users := []t.User{}
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ rows, err := a.db.Query(ctx, "SELECT * FROM users WHERE id = ANY ($1) AND state!=$2", uids, t.StateDeleted)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var user t.User
+ var id int64
+ if err = rows.Scan(&id, &user.CreatedAt, &user.UpdatedAt, &user.State, &user.StateAt, &user.Access, &user.LastSeen, &user.UserAgent, &user.Public, &user.Trusted, &user.Tags); err != nil {
+ users = nil
+ break
+ }
+
+ if user.State == t.StateDeleted {
+ continue
+ }
+
+ user.SetUid(store.EncodeUid(id))
+ users = append(users, user)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return users, err
+}
+
+// UserDelete deletes specified user: wipes completely (hard-delete) or marks as deleted.
+// TODO: report when the user is not found.
+func (a *adapter) UserDelete(uid t.Uid, hard bool) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ now := t.TimeNow()
+ decoded_uid := store.DecodeUid(uid)
+
+ if hard {
+ // Delete user's devices
+ // t.ErrNotFound = user has no devices.
+ if err = deviceDelete(ctx, tx, uid, ""); err != nil && err != t.ErrNotFound {
+ return err
+ }
+
+ // Delete user's subscriptions in all topics.
+ if err = subsDelForUser(ctx, tx, uid, true); err != nil {
+ return err
+ }
+
+ // Delete records of messages soft-deleted for the user.
+ if _, err = tx.Exec(ctx, "DELETE FROM dellog WHERE deletedfor=$1", decoded_uid); err != nil {
+ return err
+ }
+
+ // Can't delete user's messages in all topics because we cannot notify topics of such deletion.
+ // Just leave the messages there marked as sent by "not found" user.
+
+ // Delete topics where the user is the owner.
+
+ // First delete all messages in those topics.
+ if _, err = tx.Exec(ctx, "DELETE FROM dellog USING topics WHERE topics.name=dellog.topic AND topics.owner=$1",
+ decoded_uid); err != nil {
+ return err
+ }
+ if _, err = tx.Exec(ctx, "DELETE FROM messages USING topics WHERE topics.name=messages.topic AND topics.owner=$1",
+ decoded_uid); err != nil {
+ return err
+ }
+
+ // Delete all subscriptions.
+ if _, err = tx.Exec(ctx, "DELETE FROM subscriptions USING topics WHERE topics.name=subscriptions.topic AND topics.owner=$1",
+ decoded_uid); err != nil {
+ return err
+ }
+
+ // Delete topic tags.
+ if _, err = tx.Exec(ctx, "DELETE FROM topictags USING topics WHERE topics.name=topictags.topic AND topics.owner=$1",
+ decoded_uid); err != nil {
+ return err
+ }
+
+ // And finally delete the topics.
+ if _, err = tx.Exec(ctx, "DELETE FROM topics WHERE owner=$1", decoded_uid); err != nil {
+ return err
+ }
+
+ // Delete user's authentication records.
+ if _, err = tx.Exec(ctx, "DELETE FROM auth WHERE userid=$1", decoded_uid); err != nil {
+ return err
+ }
+
+ // Delete all credentials.
+ if err = credDel(ctx, tx, uid, "", ""); err != nil && err != t.ErrNotFound {
+ return err
+ }
+
+ if _, err = tx.Exec(ctx, "DELETE FROM usertags WHERE userid=$1", decoded_uid); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(ctx, "DELETE FROM users WHERE id=$1", decoded_uid); err != nil {
+ return err
+ }
+ } else {
+ // Disable all user's subscriptions. That includes p2p subscriptions. No need to delete them.
+ if err = subsDelForUser(ctx, tx, uid, false); err != nil {
+ return err
+ }
+
+ // Disable all subscriptions to topics where the user is the owner.
+ if _, err = tx.Exec(ctx, "UPDATE subscriptions SET updatedat=$1, deletedat=$2 "+
+ "FROM topics WHERE subscriptions.topic=topics.name AND topics.owner=$3",
+ now, now, decoded_uid); err != nil {
+ return err
+ }
+ // Disable group topics where the user is the owner.
+ if _, err = tx.Exec(ctx, "UPDATE topics SET updatedat=$1, touchedat=$2, state=$3, stateat=$4 WHERE owner=$5",
+ now, now, t.StateDeleted, now, decoded_uid); err != nil {
+ return err
+ }
+ // Disable p2p topics with the user (p2p topic's owner is 0).
+ if _, err = tx.Exec(ctx, "UPDATE topics SET updatedat=$1, touchedat=$2, state=$3, stateat=$4 "+
+ "FROM subscriptions WHERE topics.name=subscriptions.topic "+
+ "AND topics.owner=0 AND subscriptions.userid=$5",
+ now, now, t.StateDeleted, now, decoded_uid); err != nil {
+ return err
+ }
+
+ // Disable the other user's subscription to a disabled p2p topic.
+ if _, err = tx.Exec(ctx, "UPDATE subscriptions AS s_one SET updatedat=$1, deletedat=$2 "+
+ "FROM subscriptions AS s_two WHERE s_one.topic=s_two.topic "+
+ "AND s_two.userid=$3 AND s_two.topic LIKE 'p2p%'",
+ now, now, decoded_uid); err != nil {
+ return err
+ }
+
+ // Disable user.
+ if _, err = tx.Exec(ctx, "UPDATE users SET updatedat=$1, state=$2, stateat=$3 WHERE id=$4",
+ now, t.StateDeleted, now, decoded_uid); err != nil {
+ return err
+ }
+ }
+
+ return tx.Commit(ctx)
+}
+
+// topicStateForUser is called by UserUpdate when the update contains state change.
+func (a *adapter) topicStateForUser(ctx context.Context, tx pgx.Tx, decoded_uid int64, now time.Time, update interface{}) error {
+ var err error
+
+ state, ok := update.(t.ObjState)
+ if !ok {
+ return t.ErrMalformed
+ }
+
+ if now.IsZero() {
+ now = t.TimeNow()
+ }
+
+ // Change state of all topics where the user is the owner.
+ if _, err = tx.Exec(ctx, "UPDATE topics SET state=$1, stateat=$2 WHERE owner=$3 AND state!=$4",
+ state, now, decoded_uid, t.StateDeleted); err != nil {
+ return err
+ }
+
+ // Change state of p2p topics with the user (p2p topic's owner is 0)
+ if _, err = tx.Exec(ctx, "UPDATE topics SET state=$1, stateat=$2 "+
+ "FROM subscriptions WHERE topics.name=subscriptions.topic AND "+
+ "topics.owner=0 AND subscriptions.userid=$3 AND topics.state!=$4",
+ state, now, decoded_uid, t.StateDeleted); err != nil {
+ return err
+ }
+
+ // Subscriptions don't need to be updated:
+ // subscriptions of a disabled user are not disabled and still can be manipulated.
+
+ return nil
+}
+
+// UserUpdate updates user object.
+func (a *adapter) UserUpdate(uid t.Uid, update map[string]interface{}) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ cols, args := updateByMap(update)
+ decoded_uid := store.DecodeUid(uid)
+ args = append(args, decoded_uid)
+ sql, args := expandQuery("UPDATE users SET "+strings.Join(cols, ",")+" WHERE id=?", args...)
+ _, err = tx.Exec(ctx, sql, args...)
+ if err != nil {
+ return err
+ }
+
+ if state, ok := update["State"]; ok {
+ now, _ := update["StateAt"].(time.Time)
+ err = a.topicStateForUser(ctx, tx, decoded_uid, now, state)
+ if err != nil {
+ return err
+ }
+ }
+
+ // Tags are also stored in a separate table
+ if tags := extractTags(update); tags != nil {
+ // First delete all user tags
+ _, err = tx.Exec(ctx, "DELETE FROM usertags WHERE userid=$1", decoded_uid)
+ if err != nil {
+ return err
+ }
+ // Now insert new tags
+ err = addTags(ctx, tx, "usertags", "userid", decoded_uid, tags, false)
+ if err != nil {
+ return err
+ }
+ }
+
+ return tx.Commit(ctx)
+}
+
+// UserUpdateTags adds or resets user's tags
+func (a *adapter) UserUpdateTags(uid t.Uid, add, remove, reset []string) ([]string, error) {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return nil, err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ decoded_uid := store.DecodeUid(uid)
+
+ if reset != nil {
+ // Delete all tags first if resetting.
+ _, err = tx.Exec(ctx, "DELETE FROM usertags WHERE userid=$1", decoded_uid)
+ if err != nil {
+ return nil, err
+ }
+ add = reset
+ remove = nil
+ }
+
+ // Now insert new tags. Ignore duplicates if resetting.
+ err = addTags(ctx, tx, "usertags", "userid", decoded_uid, add, reset == nil)
+ if err != nil {
+ return nil, err
+ }
+
+ // Delete tags.
+ err = removeTags(ctx, tx, "usertags", "userid", decoded_uid, remove)
+ if err != nil {
+ return nil, err
+ }
+
+ var allTags []string
+ rows, err := tx.Query(ctx, "SELECT tag FROM usertags WHERE userid=$1", decoded_uid)
+ if err != nil {
+ return nil, err
+ }
+
+ for rows.Next() {
+ var tag string
+ rows.Scan(&tag)
+ allTags = append(allTags, tag)
+ }
+ rows.Close()
+
+ _, err = tx.Exec(ctx, "UPDATE users SET tags=$1 WHERE id=$2", t.StringSlice(allTags), decoded_uid)
+ if err != nil {
+ return nil, err
+ }
+
+ return allTags, tx.Commit(ctx)
+}
+
+// UserGetByCred returns user ID for the given validated credential.
+func (a *adapter) UserGetByCred(method, value string) (t.Uid, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ var decoded_uid int64
+ err := a.db.QueryRow(ctx, "SELECT userid FROM credentials WHERE synthetic=$1", method+":"+value).Scan(&decoded_uid)
+ if err == nil {
+ return store.EncodeUid(decoded_uid), nil
+ }
+
+ if err == pgx.ErrNoRows {
+ // Clear the error if user does not exist
+ return t.ZeroUid, nil
+ }
+ return t.ZeroUid, err
+}
+
+// UserUnreadCount returns the total number of unread messages in all topics with
+// the R permission. If read fails, the counts are still returned with the original
+// user IDs but with the unread count undefined and non-nil error.
+func (a *adapter) UserUnreadCount(ids ...t.Uid) (map[t.Uid]int, error) {
+ uids := make([]interface{}, len(ids))
+ counts := make(map[t.Uid]int, len(ids))
+ for i, id := range ids {
+ uids[i] = store.DecodeUid(id)
+ // Ensure all original uids are always present.
+ counts[id] = 0
+ }
+
+ query, uids := expandQuery("SELECT s.userid, SUM(t.seqid)-SUM(s.readseqid) AS unreadcount FROM topics AS t, subscriptions AS s "+
+ "WHERE s.userid IN (?) AND t.name=s.topic AND s.deletedat IS NULL AND t.state!=? AND "+
+ "POSITION('R' IN s.modewant)>0 AND POSITION('R' IN s.modegiven)>0 GROUP BY s.userid", uids, t.StateDeleted)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ rows, err := a.db.Query(ctx, query, uids...)
+ if err != nil {
+ return counts, err
+ }
+ defer rows.Close()
+
+ var userId int64
+ var unreadCount int
+ for rows.Next() {
+ if err = rows.Scan(&userId, &unreadCount); err != nil {
+ break
+ }
+ counts[store.EncodeUid(userId)] = unreadCount
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return counts, err
+}
+
+// UserGetUnvalidated returns a list of uids which have never logged in, have no
+// validated credentials and haven't been updated since lastUpdatedBefore.
+func (a *adapter) UserGetUnvalidated(lastUpdatedBefore time.Time, limit int) ([]t.Uid, error) {
+ var uids []t.Uid
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ rows, err := a.db.Query(ctx,
+ "SELECT u.id, COALESCE(SUM(CASE WHEN c.done THEN 1 ELSE 0 END), 0) AS total "+
+ "FROM users u LEFT JOIN credentials c ON u.id = c.userid "+
+ "WHERE u.lastseen IS NULL AND u.updatedat < $1 GROUP BY u.id, u.updatedat "+
+ "HAVING COALESCE(SUM(CASE WHEN c.done THEN 1 ELSE 0 END), 0) = 0 ORDER BY u.updatedat ASC LIMIT $2",
+ lastUpdatedBefore, limit)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var userId int64
+ var unused int
+ if err = rows.Scan(&userId, &unused); err != nil {
+ break
+ }
+ uids = append(uids, store.EncodeUid(userId))
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return uids, err
+}
+
+// *****************************
+
+func (a *adapter) topicCreate(ctx context.Context, tx pgx.Tx, topic *t.Topic) error {
+ _, err := tx.Exec(ctx, "INSERT INTO topics(createdat,updatedat,touchedat,state,name,usebt,owner,access,public,trusted,tags) "+
+ "VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)",
+ topic.CreatedAt, topic.UpdatedAt, topic.TouchedAt, topic.State, topic.Id, topic.UseBt,
+ store.DecodeUid(t.ParseUid(topic.Owner)), topic.Access, toJSON(topic.Public), toJSON(topic.Trusted), topic.Tags)
+ if err != nil {
+ return err
+ }
+
+ // Save topic's tags to a separate table to make topic findable.
+ return addTags(ctx, tx, "topictags", "topic", topic.Id, topic.Tags, false)
+}
+
+// TopicCreate saves topic object to database.
+func (a *adapter) TopicCreate(topic *t.Topic) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ err = a.topicCreate(ctx, tx, topic)
+ if err != nil {
+ return err
+ }
+ return tx.Commit(ctx)
+}
+
+// If undelete = true - update subscription on duplicate key, otherwise ignore the duplicate.
+func createSubscription(ctx context.Context, tx pgx.Tx, sub *t.Subscription, undelete bool) error {
+
+ isOwner := (sub.ModeGiven & sub.ModeWant).IsOwner()
+
+ jpriv := toJSON(sub.Private)
+ decoded_uid := store.DecodeUid(t.ParseUid(sub.User))
+ _, err2 := tx.Exec(ctx, "SAVEPOINT createSub")
+ if err2 != nil {
+ log.Println("Error: Failed to create savepoint: ", err2.Error())
+ }
+ _, err := tx.Exec(ctx,
+ "INSERT INTO subscriptions(createdat,updatedat,deletedat,userid,topic,modeWant,modeGiven,private) "+
+ "VALUES($1,$2,NULL,$3,$4,$5,$6,$7)",
+ sub.CreatedAt, sub.UpdatedAt, decoded_uid, sub.Topic, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv)
+
+ if err != nil && isDupe(err) {
+ _, err2 = tx.Exec(ctx, "ROLLBACK TO SAVEPOINT createSub")
+ if err2 != nil {
+ log.Println("Error: Failed to rollback savepoint: ", err2.Error())
+ }
+ if undelete {
+ _, err = tx.Exec(ctx, "UPDATE subscriptions SET createdat=$1,updatedat=$2,deletedat=NULL,modeWant=$3,modeGiven=$4,"+
+ "delid=0,recvseqid=0,readseqid=0 WHERE topic=$5 AND userid=$6",
+ sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), sub.Topic, decoded_uid)
+ } else {
+ _, err = tx.Exec(ctx, "UPDATE subscriptions SET createdat=$1,updatedat=$2,deletedat=NULL,modeWant=$3,modeGiven=$4,"+
+ "delid=0,recvseqid=0,readseqid=0,private=$5 WHERE topic=$6 AND userid=$7",
+ sub.CreatedAt, sub.UpdatedAt, sub.ModeWant.String(), sub.ModeGiven.String(), jpriv,
+ sub.Topic, decoded_uid)
+ }
+ } else {
+ _, err2 = tx.Exec(ctx, "RELEASE SAVEPOINT createSub")
+ if err2 != nil {
+ log.Println("Error: Failed to release savepoint: ", err2.Error())
+ }
+ }
+ if err == nil && isOwner {
+ _, err = tx.Exec(ctx, "UPDATE topics SET owner=$1 WHERE name=$2", decoded_uid, sub.Topic)
+ }
+ return err
+}
+
+// TopicCreateP2P given two users creates a p2p topic
+func (a *adapter) TopicCreateP2P(initiator, invited *t.Subscription) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ err = createSubscription(ctx, tx, initiator, false)
+ if err != nil {
+ return err
+ }
+
+ err = createSubscription(ctx, tx, invited, true)
+ if err != nil {
+ return err
+ }
+
+ topic := &t.Topic{ObjHeader: t.ObjHeader{Id: initiator.Topic}}
+ topic.ObjHeader.MergeTimes(&initiator.ObjHeader)
+ topic.TouchedAt = initiator.GetTouchedAt()
+ err = a.topicCreate(ctx, tx, topic)
+ if err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+// TopicGet loads a single topic by name, if it exists. If the topic does not exist the call returns (nil, nil)
+func (a *adapter) TopicGet(topic string) (*t.Topic, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ // Fetch topic by name
+ var tt = new(t.Topic)
+ var owner int64
+ err := a.db.QueryRow(ctx,
+ "SELECT createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,owner,seqid,delid,public,trusted,tags "+
+ "FROM topics WHERE name=$1",
+ topic).Scan(&tt.CreatedAt, &tt.UpdatedAt, &tt.State, &tt.StateAt, &tt.TouchedAt, &tt.Id,
+ &tt.UseBt, &tt.Access, &owner, &tt.SeqId, &tt.DelId, &tt.Public, &tt.Trusted, &tt.Tags)
+ if err != nil {
+ if err == pgx.ErrNoRows {
+ // Nothing found - clear the error
+ err = nil
+ }
+ return nil, err
+ }
+
+ tt.Owner = store.EncodeUid(owner).String()
+
+ return tt, nil
+}
+
+// TopicsForUser loads user's contact list: p2p and grp topics, except for 'me' & 'fnd' subscriptions.
+// Reads and denormalizes Public value.
+func (a *adapter) TopicsForUser(uid t.Uid, keepDeleted bool, opts *t.QueryOpt) ([]t.Subscription, error) {
+ // Fetch ALL user's subscriptions, even those which has not been modified recently.
+ // We are going to use these subscriptions to fetch topics and users which may have been modified recently.
+ q := `SELECT createdat,updatedat,deletedat,topic,delid,recvseqid,
+ readseqid,modewant,modegiven,private FROM subscriptions WHERE userid=?`
+ args := []interface{}{store.DecodeUid(uid)}
+ if !keepDeleted {
+ // Filter out deleted rows.
+ q += " AND deletedat IS NULL"
+ }
+ limit := 0
+ ipg := time.Time{}
+ if opts != nil {
+ if opts.Topic != "" {
+ q += " AND topic=?"
+ args = append(args, opts.Topic)
+ }
+
+ // Apply the limit only when the client does not manage the cache (or cold start).
+ // Otherwise have to get all subscriptions and do a manual join with users/topics.
+ if opts.IfModifiedSince == nil {
+ if opts.Limit > 0 && opts.Limit < a.maxResults {
+ limit = opts.Limit
+ } else {
+ limit = a.maxResults
+ }
+ } else {
+ ipg = *opts.IfModifiedSince
+ }
+ } else {
+ limit = a.maxResults
+ }
+
+ if limit > 0 {
+ q += " LIMIT ?"
+ args = append(args, limit)
+ }
+
+ q, args = expandQuery(q, args...)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ rows, err := a.db.Query(ctx, q, args...)
+
+ if err != nil {
+ rows.Close()
+ return nil, err
+ }
+
+ // Fetch subscriptions. Two queries are needed: users table (p2p) and topics table (grp).
+ // Prepare a list of separate subscriptions to users vs topics
+ join := make(map[string]t.Subscription) // Keeping these to make a join with table for .private and .access
+ topq := make([]interface{}, 0, 16)
+ usrq := make([]interface{}, 0, 16)
+ for rows.Next() {
+ var sub t.Subscription
+ var modeWant, modeGiven []byte
+ if err = rows.Scan(&sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt, &sub.Topic, &sub.DelId,
+ &sub.RecvSeqId, &sub.ReadSeqId, &modeWant, &modeGiven, &sub.Private); err != nil {
+ break
+ }
+ sub.ModeWant.Scan(modeWant)
+ sub.ModeGiven.Scan(modeGiven)
+ tname := sub.Topic
+ sub.User = uid.String()
+ tcat := t.GetTopicCat(tname)
+
+ if tcat == t.TopicCatMe || tcat == t.TopicCatFnd {
+ // One of 'me', 'fnd' subscriptions, skip. Don't skip 'sys' subscription.
+ continue
+ } else if tcat == t.TopicCatP2P {
+ // P2P subscription, find the other user to get user.Public and user.Trusted.
+ uid1, uid2, _ := t.ParseP2P(tname)
+ if uid1 == uid {
+ usrq = append(usrq, store.DecodeUid(uid2))
+ sub.SetWith(uid2.UserId())
+ } else {
+ usrq = append(usrq, store.DecodeUid(uid1))
+ sub.SetWith(uid1.UserId())
+ }
+ topq = append(topq, tname)
+ } else {
+ // Group or 'sys' subscription.
+ if tcat == t.TopicCatGrp {
+ // Maybe convert channel name to topic name.
+ tname = t.ChnToGrp(tname)
+ }
+ topq = append(topq, tname)
+ }
+ sub.Private = fromJSON(sub.Private)
+ join[tname] = sub
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+ rows.Close()
+
+ if err != nil {
+ return nil, err
+ }
+
+ var subs []t.Subscription
+ if len(join) == 0 {
+ return subs, nil
+ }
+
+ // Fetch grp topics and join to subscriptions.
+ if len(topq) > 0 {
+ q = "SELECT createdat,updatedat,state,stateat,touchedat,name AS id,usebt,access,seqid,delid,public,trusted,tags " +
+ "FROM topics WHERE name IN (?)"
+ newargs := []interface{}{topq}
+
+ if !keepDeleted {
+ // Optionally skip deleted topics.
+ q += " AND state!=?"
+ newargs = append(newargs, t.StateDeleted)
+ }
+
+ if !ipg.IsZero() {
+ // Use cache timestamp if provided: get newer entries only.
+ q += " AND touchedat>?"
+ newargs = append(newargs, ipg)
+
+ if limit > 0 && limit < len(topq) {
+ // No point in fetching more than the requested limit.
+ q += " ORDER BY touchedat LIMIT ?"
+ newargs = append(newargs, limit)
+ }
+ }
+ q, newargs = expandQuery(q, newargs...)
+
+ ctx2, cancel2 := a.getContext()
+ if cancel2 != nil {
+ defer cancel2()
+ }
+ rows, err = a.db.Query(ctx2, q, newargs...)
+ if err != nil {
+ rows.Close()
+ return nil, err
+ }
+
+ var top t.Topic
+ for rows.Next() {
+ if err = rows.Scan(&top.CreatedAt, &top.UpdatedAt, &top.State, &top.StateAt, &top.TouchedAt, &top.Id, &top.UseBt,
+ &top.Access, &top.SeqId, &top.DelId, &top.Public, &top.Trusted, &top.Tags); err != nil {
+ break
+ }
+
+ sub := join[top.Id]
+ // Check if sub.UpdatedAt needs to be adjusted to earlier or later time.
+ sub.UpdatedAt = common.SelectLatestTime(sub.UpdatedAt, top.UpdatedAt)
+ sub.SetState(top.State)
+ sub.SetTouchedAt(top.TouchedAt)
+ sub.SetSeqId(top.SeqId)
+ if t.GetTopicCat(sub.Topic) == t.TopicCatGrp {
+ sub.SetPublic(top.Public)
+ sub.SetTrusted(top.Trusted)
+ }
+ // Put back the updated value of a subsription, will process further below
+ join[top.Id] = sub
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+ rows.Close()
+
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // Fetch p2p users and join to p2p subscriptions.
+ if len(usrq) > 0 {
+ q = "SELECT id,createdat,updatedat,state,stateat,access,lastseen,useragent,public,trusted,tags " +
+ "FROM users WHERE id IN (?)"
+ newargs := []interface{}{usrq}
+
+ if !keepDeleted {
+ // Optionally skip deleted users.
+ q += " AND state!=?"
+ newargs = append(newargs, t.StateDeleted)
+ }
+
+ // Ignoring ipg: we need all users to get LastSeen and UserAgent.
+
+ q, newargs = expandQuery(q, newargs...)
+
+ ctx3, cancel3 := a.getContext()
+ if cancel3 != nil {
+ defer cancel3()
+ }
+
+ rows, err = a.db.Query(ctx3, q, newargs...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var usr2 t.User
+ var id int64
+ if err = rows.Scan(&id, &usr2.CreatedAt, &usr2.UpdatedAt, &usr2.State, &usr2.StateAt, &usr2.Access,
+ &usr2.LastSeen, &usr2.UserAgent, &usr2.Public, &usr2.Trusted, &usr2.Tags); err != nil {
+ break
+ }
+
+ usr2.Id = store.EncodeUid(id).String()
+ joinOn := uid.P2PName(t.ParseUid(usr2.Id))
+ if sub, ok := join[joinOn]; ok {
+ sub.UpdatedAt = common.SelectLatestTime(sub.UpdatedAt, usr2.UpdatedAt)
+ sub.SetState(usr2.State)
+ sub.SetPublic(usr2.Public)
+ sub.SetTrusted(usr2.Trusted)
+ sub.SetDefaultAccess(usr2.Access.Auth, usr2.Access.Anon)
+ sub.SetLastSeenAndUA(usr2.LastSeen, usr2.UserAgent)
+ join[joinOn] = sub
+ }
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ subs = make([]t.Subscription, 0, len(join))
+ for _, sub := range join {
+ subs = append(subs, sub)
+ }
+
+ return common.SelectEarliestUpdatedSubs(subs, opts, a.maxResults), nil
+}
+
+// UsersForTopic loads users subscribed to the given topic.
+// The difference between UsersForTopic vs SubsForTopic is that the former loads user.Public,
+// the latter does not.
+func (a *adapter) UsersForTopic(topic string, keepDeleted bool, opts *t.QueryOpt) ([]t.Subscription, error) {
+ tcat := t.GetTopicCat(topic)
+
+ // Fetch all subscribed users. The number of users is not large
+ q := `SELECT s.createdat,s.updatedat,s.deletedat,s.userid,s.topic,s.delid,s.recvseqid,
+ s.readseqid,s.modewant,s.modegiven,u.public,u.trusted,u.lastseen,u.useragent,s.private
+ FROM subscriptions AS s JOIN users AS u ON s.userid=u.id
+ WHERE s.topic=?`
+ args := []interface{}{topic}
+ if !keepDeleted {
+ // Filter out rows with users deleted
+ q += " AND u.state!=?"
+ args = append(args, t.StateDeleted)
+
+ // For p2p topics we must load all subscriptions including deleted.
+ // Otherwise it will be impossible to swipe Public values.
+ if tcat != t.TopicCatP2P {
+ // Filter out deleted subscriptions.
+ q += " AND s.deletedat IS NULL"
+ }
+ }
+
+ limit := a.maxResults
+ var oneUser t.Uid
+ if opts != nil {
+ // Ignore IfModifiedSince: loading all entries because a topic cannot have too many subscribers.
+ // Those unmodified will be stripped of Public & Private.
+
+ if !opts.User.IsZero() {
+ // For p2p topics we have to fetch both users otherwise public cannot be swapped.
+ if tcat != t.TopicCatP2P {
+ q += " AND s.userid=?"
+ args = append(args, store.DecodeUid(opts.User))
+ }
+ oneUser = opts.User
+ }
+ if opts.Limit > 0 && opts.Limit < limit {
+ limit = opts.Limit
+ }
+ }
+ q += " LIMIT ?"
+ args = append(args, limit)
+ q, args = expandQuery(q, args...)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ rows, err := a.db.Query(ctx, q, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ // Fetch subscriptions
+ var sub t.Subscription
+ var subs []t.Subscription
+ var userId int64
+ var modeWant, modeGiven []byte
+ var lastSeen *time.Time = nil
+ var userAgent string
+ var public, trusted interface{}
+ for rows.Next() {
+ if err = rows.Scan(
+ &sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt,
+ &userId, &sub.Topic, &sub.DelId, &sub.RecvSeqId,
+ &sub.ReadSeqId, &modeWant, &modeGiven,
+ &public, &trusted, &lastSeen, &userAgent, &sub.Private); err != nil {
+ break
+ }
+
+ sub.User = store.EncodeUid(userId).String()
+ sub.SetPublic(public)
+ sub.SetTrusted(trusted)
+ sub.SetLastSeenAndUA(lastSeen, userAgent)
+ sub.ModeWant.Scan(modeWant)
+ sub.ModeGiven.Scan(modeGiven)
+ subs = append(subs, sub)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ if err == nil && tcat == t.TopicCatP2P && len(subs) > 0 {
+ // Swap public & lastSeen values of P2P topics as expected.
+ if len(subs) == 1 {
+ // The other user is deleted, nothing we can do.
+ subs[0].SetPublic(nil)
+ subs[0].SetTrusted(nil)
+ subs[0].SetLastSeenAndUA(nil, "")
+ } else {
+ tmp := subs[0].GetPublic()
+ subs[0].SetPublic(subs[1].GetPublic())
+ subs[1].SetPublic(tmp)
+
+ tmp = subs[0].GetTrusted()
+ subs[0].SetTrusted(subs[1].GetTrusted())
+ subs[1].SetTrusted(tmp)
+
+ lastSeen := subs[0].GetLastSeen()
+ userAgent = subs[0].GetUserAgent()
+ subs[0].SetLastSeenAndUA(subs[1].GetLastSeen(), subs[1].GetUserAgent())
+ subs[1].SetLastSeenAndUA(lastSeen, userAgent)
+ }
+
+ // Remove deleted and unneeded subscriptions
+ if !keepDeleted || !oneUser.IsZero() {
+ var xsubs []t.Subscription
+ for i := range subs {
+ if (subs[i].DeletedAt != nil && !keepDeleted) || (!oneUser.IsZero() && subs[i].Uid() != oneUser) {
+ continue
+ }
+ xsubs = append(xsubs, subs[i])
+ }
+ subs = xsubs
+ }
+ }
+
+ return subs, err
+}
+
+// topicNamesForUser reads a slice of strings using provided query.
+func (a *adapter) topicNamesForUser(uid t.Uid, sqlQuery string) ([]string, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ rows, err := a.db.Query(ctx, sqlQuery, store.DecodeUid(uid))
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var names []string
+ var name string
+ for rows.Next() {
+ if err = rows.Scan(&name); err != nil {
+ break
+ }
+ names = append(names, name)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return names, err
+}
+
+// OwnTopics loads a slice of topic names where the user is the owner.
+func (a *adapter) OwnTopics(uid t.Uid) ([]string, error) {
+ return a.topicNamesForUser(uid, "SELECT name FROM topics WHERE owner=$1")
+}
+
+// ChannelsForUser loads a slice of topic names where the user is a channel reader and notifications (P) are enabled.
+func (a *adapter) ChannelsForUser(uid t.Uid) ([]string, error) {
+ return a.topicNamesForUser(uid,
+ "SELECT topic FROM subscriptions WHERE userid=$1 AND topic LIKE 'chn%' "+
+ "AND POSITION('P' IN modewant)>0 AND POSITION('P' IN modegiven)>0 AND deletedat IS NULL")
+}
+
+func (a *adapter) TopicShare(shares []*t.Subscription) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ for _, sub := range shares {
+ err = createSubscription(ctx, tx, sub, true)
+ if err != nil {
+ return err
+ }
+ }
+
+ return tx.Commit(ctx)
+}
+
+// TopicDelete deletes specified topic.
+func (a *adapter) TopicDelete(topic string, isChan, hard bool) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ // If the topic is a channel, must try to delete subscriptions under both grpXXX and chnXXX names.
+ args := []interface{}{topic}
+ if isChan {
+ args = append(args, t.GrpToChn(topic))
+ }
+
+ if hard {
+ // Delete subscriptions. If this is a channel, delete both group subscriptions and channel subscriptions.
+ q, args := expandQuery("DELETE FROM subscriptions WHERE topic IN (?)", args)
+
+ if _, err = tx.Exec(ctx, q, args...); err != nil {
+ return err
+ }
+
+ if err = messageDeleteList(ctx, tx, topic, nil); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(ctx, "DELETE FROM topictags WHERE topic=$1", topic); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(ctx, "DELETE FROM topics WHERE name=$1", topic); err != nil {
+ return err
+ }
+ } else {
+ now := t.TimeNow()
+ q, args := expandQuery("UPDATE subscriptions SET updatedat=?,deletedat=? WHERE topic IN (?)", now, now, args)
+
+ if _, err = tx.Exec(ctx, q, args); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(ctx, "UPDATE topics SET updatedat=$1,touchedat=$2,state=$3,stateat=$4 WHERE name=$5",
+ now, now, t.StateDeleted, now, topic); err != nil {
+ return err
+ }
+ }
+ return tx.Commit(ctx)
+}
+
+func (a *adapter) TopicUpdateOnMessage(topic string, msg *t.Message) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ _, err := a.db.Exec(ctx, "UPDATE topics SET seqid=$1,touchedat=$2 WHERE name=$3", msg.SeqId, msg.CreatedAt, topic)
+
+ return err
+}
+
+func (a *adapter) TopicUpdate(topic string, update map[string]interface{}) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ if t, u := update["TouchedAt"], update["UpdatedAt"]; t == nil && u != nil {
+ update["TouchedAt"] = u
+ }
+ cols, args := updateByMap(update)
+ q, args := expandQuery("UPDATE topics SET "+strings.Join(cols, ",")+" WHERE name=?", args, topic)
+ _, err = tx.Exec(ctx, q, args...)
+ if err != nil {
+ return err
+ }
+
+ // Tags are also stored in a separate table
+ if tags := extractTags(update); tags != nil {
+ // First delete all user tags
+ _, err = tx.Exec(ctx, "DELETE FROM topictags WHERE topic=$1", topic)
+ if err != nil {
+ return err
+ }
+ // Now insert new tags
+ err = addTags(ctx, tx, "topictags", "topic", topic, tags, false)
+ if err != nil {
+ return err
+ }
+ }
+
+ return tx.Commit(ctx)
+}
+
+func (a *adapter) TopicOwnerChange(topic string, newOwner t.Uid) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ _, err := a.db.Exec(ctx, "UPDATE topics SET owner=$1 WHERE name=$2", store.DecodeUid(newOwner), topic)
+ return err
+}
+
+// Get a subscription of a user to a topic.
+func (a *adapter) SubscriptionGet(topic string, user t.Uid, keepDeleted bool) (*t.Subscription, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ var sub t.Subscription
+ var userId int64
+ var modeWant, modeGiven []byte
+ err := a.db.QueryRow(ctx, `SELECT createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
+ readseqid,modewant,modegiven,private FROM subscriptions WHERE topic=$1 AND userid=$2`,
+ topic, store.DecodeUid(user)).Scan(&sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt, &userId,
+ &sub.Topic, &sub.DelId, &sub.RecvSeqId, &sub.ReadSeqId, &modeWant, &modeGiven, &sub.Private)
+
+ if err != nil {
+ if err == pgx.ErrNoRows {
+ // Nothing found - clear the error
+ err = nil
+ }
+ return nil, err
+ }
+
+ if !keepDeleted && sub.DeletedAt != nil {
+ return nil, nil
+ }
+
+ sub.User = store.EncodeUid(userId).String()
+ sub.ModeWant.Scan(modeWant)
+ sub.ModeGiven.Scan(modeGiven)
+
+ return &sub, nil
+}
+
+// SubsForUser loads all user's subscriptions. Does NOT load Public or Private values and does
+// not load deleted subscriptions.
+func (a *adapter) SubsForUser(forUser t.Uid) ([]t.Subscription, error) {
+ q := `SELECT createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
+ readseqid,modewant,modegiven FROM subscriptions WHERE userid=$1 AND deletedat IS NULL`
+ args := []interface{}{store.DecodeUid(forUser)}
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ rows, err := a.db.Query(ctx, q, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var subs []t.Subscription
+ var sub t.Subscription
+ var userId int64
+ var modeWant, modeGiven []byte
+ for rows.Next() {
+ if err = rows.Scan(&sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt, &userId, &sub.Topic, &sub.DelId,
+ &sub.RecvSeqId, &sub.ReadSeqId, &modeWant, &modeGiven); err != nil {
+ break
+ }
+
+ sub.User = store.EncodeUid(userId).String()
+ sub.ModeWant.Scan(modeWant)
+ sub.ModeGiven.Scan(modeGiven)
+ subs = append(subs, sub)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return subs, err
+}
+
+// SubsForTopic fetches all subsciptions for a topic. Does NOT load Public value.
+// The difference between UsersForTopic vs SubsForTopic is that the former loads user.public+trusted,
+// the latter does not.
+func (a *adapter) SubsForTopic(topic string, keepDeleted bool, opts *t.QueryOpt) ([]t.Subscription, error) {
+ q := `SELECT createdat,updatedat,deletedat,userid AS user,topic,delid,recvseqid,
+ readseqid,modewant,modegiven,private FROM subscriptions WHERE topic=?`
+
+ args := []interface{}{topic}
+
+ if !keepDeleted {
+ // Filter out deleted rows.
+ q += " AND deletedat IS NULL"
+ }
+ limit := a.maxResults
+ if opts != nil {
+ // Ignore IfModifiedSince - we must return all entries
+ // Those unmodified will be stripped of Public & Private.
+
+ if !opts.User.IsZero() {
+ q += " AND userid=?"
+ args = append(args, store.DecodeUid(opts.User))
+ }
+ if opts.Limit > 0 && opts.Limit < limit {
+ limit = opts.Limit
+ }
+ }
+
+ q += " LIMIT ?"
+ args = append(args, limit)
+ q, args = expandQuery(q, args...)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ rows, err := a.db.Query(ctx, q, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var subs []t.Subscription
+ var sub t.Subscription
+ var userId int64
+ var modeWant, modeGiven []byte
+ for rows.Next() {
+ if err = rows.Scan(&sub.CreatedAt, &sub.UpdatedAt, &sub.DeletedAt, &userId, &sub.Topic, &sub.DelId,
+ &sub.RecvSeqId, &sub.ReadSeqId, &modeWant, &modeGiven, &sub.Private); err != nil {
+ break
+ }
+
+ sub.User = store.EncodeUid(userId).String()
+ sub.ModeWant.Scan(modeWant)
+ sub.ModeGiven.Scan(modeGiven)
+ subs = append(subs, sub)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return subs, err
+}
+
+// SubsUpdate updates one or multiple subscriptions to a topic.
+func (a *adapter) SubsUpdate(topic string, user t.Uid, update map[string]interface{}) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ cols, args := updateByMap(update)
+ args = append(args, topic)
+ q := "UPDATE subscriptions SET " + strings.Join(cols, ",") + " WHERE topic=?"
+ if !user.IsZero() {
+ // Update just one topic subscription
+ args = append(args, store.DecodeUid(user))
+ q += " AND userid=?"
+ }
+ q, args = expandQuery(q, args...)
+
+ if _, err = tx.Exec(ctx, q, args...); err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+// SubsDelete marks subscription as deleted.
+func (a *adapter) SubsDelete(topic string, user t.Uid) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ tx, err := a.db.Begin(ctx)
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ decoded_id := store.DecodeUid(user)
+ now := t.TimeNow()
+ res, err := tx.Exec(ctx,
+ "UPDATE subscriptions SET updatedat=$1,deletedat=$2 WHERE topic=$3 AND userid=$4 AND deletedat IS NULL",
+ now, now, topic, decoded_id)
+ if err != nil {
+ return err
+ }
+
+ affected := res.RowsAffected()
+ if affected == 0 {
+ // ensure tx.Rollback() above is ran
+ err = t.ErrNotFound
+ return err
+ }
+
+ // Remove records of messages soft-deleted by this user.
+ _, err = tx.Exec(ctx, "DELETE FROM dellog WHERE topic=$1 AND deletedfor=$2", topic, decoded_id)
+ if err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+// subsDelForUser marks user's subscriptions as deleted.
+func subsDelForUser(ctx context.Context, tx pgx.Tx, user t.Uid, hard bool) error {
+ var err error
+ if hard {
+ _, err = tx.Exec(ctx, "DELETE FROM subscriptions WHERE userid=$1;", store.DecodeUid(user))
+ } else {
+ now := t.TimeNow()
+ _, err = tx.Exec(ctx, "UPDATE subscriptions SET updatedat=$1,deletedat=$2 WHERE userid=$3 AND deletedat IS NULL;",
+ now, now, store.DecodeUid(user))
+ }
+ return err
+}
+
+// SubsDelForUser marks user's subscriptions as deleted.
+func (a *adapter) SubsDelForUser(user t.Uid, hard bool) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ if err = subsDelForUser(ctx, tx, user, hard); err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+
+}
+
+// Returns a list of users who match given tags, such as "email:jdoe@example.com" or "tel:+18003287448".
+// Searching the 'users.Tags' for the given tags using respective index.
+func (a *adapter) FindUsers(user t.Uid, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
+ index := make(map[string]struct{})
+ var args []interface{}
+ stateConstraint := ""
+ if activeOnly {
+ args = append(args, t.StateOK)
+ stateConstraint = "u.state=? AND "
+ }
+ allReq := t.FlattenDoubleSlice(req)
+ allTags := append(allReq, opt...)
+ for _, tag := range allTags {
+ index[tag] = struct{}{}
+ }
+ args = append(args, allTags)
+
+ query := "SELECT u.id,u.createdat,u.updatedat,u.access,u.public,u.trusted,u.tags,COUNT(*) AS matches " +
+ "FROM users AS u LEFT JOIN usertags AS t ON t.userid=u.id " +
+ "WHERE " + stateConstraint + "t.tag IN (?) GROUP BY u.id,u.createdat,u.updatedat"
+ if len(allReq) > 0 {
+ query += " HAVING"
+ first := true
+ for _, reqDisjunction := range req {
+ if len(reqDisjunction) > 0 {
+ if !first {
+ query += " AND"
+ } else {
+ first = false
+ }
+ // At least one of the tags must be present.
+ query += " COUNT(t.tag IN (?) OR NULL)>=1"
+ args = append(args, reqDisjunction)
+ }
+ }
+ }
+ query, args = expandQuery(query+" ORDER BY matches DESC LIMIT ?", args, a.maxResults)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ // Get users matched by tags, sort by number of matches from high to low.
+ rows, err := a.db.Query(ctx, query, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var userId int64
+ var public, trusted interface{}
+ var access t.DefaultAccess
+ var userTags t.StringSlice
+ var ignored int
+ var sub t.Subscription
+ var subs []t.Subscription
+ thisUser := store.DecodeUid(user)
+ for rows.Next() {
+ if err = rows.Scan(&userId, &sub.CreatedAt, &sub.UpdatedAt, &access,
+ &public, &trusted, &userTags, &ignored); err != nil {
+ subs = nil
+ break
+ }
+
+ if userId == thisUser {
+ // Skip the callee
+ continue
+ }
+ sub.User = store.EncodeUid(userId).String()
+ sub.SetPublic(public)
+ sub.SetTrusted(trusted)
+ sub.SetDefaultAccess(access.Auth, access.Anon)
+ foundTags := make([]string, 0, 1)
+ for _, tag := range userTags {
+ if _, ok := index[tag]; ok {
+ foundTags = append(foundTags, tag)
+ }
+ }
+ sub.Private = foundTags
+ subs = append(subs, sub)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return subs, err
+
+}
+
+// Returns a list of topics with matching tags.
+// Searching the 'topics.Tags' for the given tags using respective index.
+func (a *adapter) FindTopics(req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
+ index := make(map[string]struct{})
+ var args []interface{}
+ stateConstraint := ""
+ if activeOnly {
+ args = append(args, t.StateOK)
+ stateConstraint = "t.state=? AND "
+ }
+ allReq := t.FlattenDoubleSlice(req)
+ allTags := append(allReq, opt...)
+ for _, tag := range allTags {
+ index[tag] = struct{}{}
+ }
+ args = append(args, allTags)
+
+ query := "SELECT t.id,t.name AS topic,t.createdat,t.updatedat,t.usebt,t.access,t.public,t.trusted,t.tags,COUNT(*) AS matches " +
+ "FROM topics AS t LEFT JOIN topictags AS tt ON t.name=tt.topic " +
+ "WHERE " + stateConstraint + "tt.tag IN (?) GROUP BY t.id,t.name,t.createdat,t.updatedat,t.usebt"
+ if len(allReq) > 0 {
+ query += " HAVING"
+ first := true
+ for _, reqDisjunction := range req {
+ if len(reqDisjunction) > 0 {
+ if !first {
+ query += " AND"
+ } else {
+ first = false
+ }
+ // At least one of the tags must be present.
+ query += " COUNT(tt.tag IN (?) OR NULL)>=1"
+ args = append(args, reqDisjunction)
+ }
+ }
+ }
+ query, args = expandQuery(query+" ORDER BY matches DESC LIMIT ?", args, a.maxResults)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ rows, err := a.db.Query(ctx, query, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var access t.DefaultAccess
+ var public, trusted interface{}
+ var topicTags t.StringSlice
+ var id int
+ var ignored int
+ var isChan int
+ var sub t.Subscription
+ var subs []t.Subscription
+ for rows.Next() {
+ if err = rows.Scan(&id, &sub.Topic, &sub.CreatedAt, &sub.UpdatedAt, &isChan, &access,
+ &public, &trusted, &topicTags, &ignored); err != nil {
+ subs = nil
+ break
+ }
+
+ if isChan != 0 {
+ sub.Topic = t.GrpToChn(sub.Topic)
+ }
+ sub.SetPublic(public)
+ sub.SetTrusted(trusted)
+ sub.SetDefaultAccess(access.Auth, access.Anon)
+ foundTags := make([]string, 0, 1)
+ for _, tag := range topicTags {
+ if _, ok := index[tag]; ok {
+ foundTags = append(foundTags, tag)
+ }
+ }
+ sub.Private = foundTags
+ subs = append(subs, sub)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ if err != nil {
+ return nil, err
+ }
+ return subs, nil
+
+}
+
+// Messages
+func (a *adapter) MessageSave(msg *t.Message) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ // store assignes message ID, but we don't use it. Message IDs are not used anywhere.
+ // Using a sequential ID provided by the database.
+ var id int
+ err := a.db.QueryRow(ctx,
+ `INSERT INTO messages(createdAt,updatedAt,seqid,topic,"from",head,content) VALUES($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
+ msg.CreatedAt, msg.UpdatedAt, msg.SeqId, msg.Topic,
+ store.DecodeUid(t.ParseUid(msg.From)), msg.Head, toJSON(msg.Content)).Scan(&id)
+ if err == nil {
+ // Replacing ID given by store by ID given by the DB.
+ msg.SetUid(t.Uid(id))
+ }
+ return err
+}
+
+func (a *adapter) MessageGetAll(topic string, forUser t.Uid, opts *t.QueryOpt) ([]t.Message, error) {
+ var limit = a.maxMessageResults
+ var lower = 0
+ var upper = 1<<31 - 1
+
+ if opts != nil {
+ if opts.Since > 0 {
+ lower = opts.Since
+ }
+ if opts.Before > 0 {
+ // MySQL BETWEEN is inclusive-inclusive, Tinode API requires inclusive-exclusive, thus -1
+ upper = opts.Before - 1
+ }
+
+ if opts.Limit > 0 && opts.Limit < limit {
+ limit = opts.Limit
+ }
+ }
+
+ unum := store.DecodeUid(forUser)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ rows, err := a.db.Query(
+ ctx,
+ `SELECT m.createdat,m.updatedat,m.deletedat,m.delid,m.seqid,m.topic,m."from",m.head,m.content`+
+ " FROM messages AS m LEFT JOIN dellog AS d"+
+ " ON d.topic=m.topic AND m.seqid BETWEEN d.low AND d.hi-1 AND d.deletedfor=$1"+
+ " WHERE m.delid=0 AND m.topic=$2 AND m.seqid BETWEEN $3 AND $4 AND d.deletedfor IS NULL"+
+ " ORDER BY m.seqid DESC LIMIT $5",
+ unum, topic, lower, upper, limit)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ msgs := make([]t.Message, 0, limit)
+ for rows.Next() {
+ var msg t.Message
+ var from int64
+ if err = rows.Scan(&msg.CreatedAt, &msg.UpdatedAt, &msg.DeletedAt, &msg.DelId, &msg.SeqId,
+ &msg.Topic, &from, &msg.Head, &msg.Content); err != nil {
+ break
+ }
+ msg.From = store.EncodeUid(from).String()
+ msgs = append(msgs, msg)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return msgs, err
+}
+
+// Get ranges of deleted messages
+func (a *adapter) MessageGetDeleted(topic string, forUser t.Uid, opts *t.QueryOpt) ([]t.DelMessage, error) {
+ var limit = a.maxResults
+ var lower = 0
+ var upper = 1<<31 - 1
+
+ if opts != nil {
+ if opts.Since > 0 {
+ lower = opts.Since
+ }
+ if opts.Before > 1 {
+ // DelRange is inclusive-exclusive, while BETWEEN is inclusive-inclisive.
+ upper = opts.Before - 1
+ }
+
+ if opts.Limit > 0 && opts.Limit < limit {
+ limit = opts.Limit
+ }
+ }
+
+ // Fetch log of deletions
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ rows, err := a.db.Query(ctx, "SELECT topic,deletedfor,delid,low,hi FROM dellog WHERE topic=$1 AND delid BETWEEN $2 AND $3"+
+ " AND (deletedFor=0 OR deletedFor=$4) ORDER BY delid LIMIT $5",
+ topic, lower, upper, store.DecodeUid(forUser), limit)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var dellog struct {
+ Topic string
+ Deletedfor int64
+ Delid int
+ Low int
+ Hi int
+ }
+ var dmsgs []t.DelMessage
+ var dmsg t.DelMessage
+ for rows.Next() {
+ if err = rows.Scan(&dellog.Topic, &dellog.Deletedfor, &dellog.Delid, &dellog.Low, &dellog.Hi); err != nil {
+ dmsgs = nil
+ break
+ }
+
+ if dellog.Delid != dmsg.DelId {
+ if dmsg.DelId > 0 {
+ dmsgs = append(dmsgs, dmsg)
+ }
+ dmsg.DelId = dellog.Delid
+ dmsg.Topic = dellog.Topic
+ if dellog.Deletedfor > 0 {
+ dmsg.DeletedFor = store.EncodeUid(dellog.Deletedfor).String()
+ } else {
+ dmsg.DeletedFor = ""
+ }
+ dmsg.SeqIdRanges = nil
+ }
+ if dellog.Hi <= dellog.Low+1 {
+ dellog.Hi = 0
+ }
+ dmsg.SeqIdRanges = append(dmsg.SeqIdRanges, t.Range{Low: dellog.Low, Hi: dellog.Hi})
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ if err == nil {
+ if dmsg.DelId > 0 {
+ dmsgs = append(dmsgs, dmsg)
+ }
+ }
+
+ return dmsgs, err
+}
+
+func messageDeleteList(ctx context.Context, tx pgx.Tx, topic string, toDel *t.DelMessage) error {
+ var err error
+ if toDel == nil {
+ // Whole topic is being deleted, thus also deleting all messages.
+ _, err = tx.Exec(ctx, "DELETE FROM dellog WHERE topic=$1", topic)
+ if err == nil {
+ _, err = tx.Exec(ctx, "DELETE FROM messages WHERE topic=$1", topic)
+ }
+ // filemsglinks will be deleted because of ON DELETE CASCADE
+
+ } else {
+ // Only some messages are being deleted
+ // Start with making log entries
+ forUser := decodeUidString(toDel.DeletedFor)
+
+ // Counter of deleted messages
+ for _, rng := range toDel.SeqIdRanges {
+ if rng.Hi == 0 {
+ // Dellog must contain valid Low and *Hi*.
+ rng.Hi = rng.Low + 1
+ }
+ if _, err = tx.Exec(ctx,
+ "INSERT INTO dellog(topic,deletedfor,delid,low,hi) VALUES($1,$2,$3,$4,$5)",
+ topic, forUser, toDel.DelId, rng.Low, rng.Hi); err != nil {
+ break
+ }
+ }
+
+ if err == nil && toDel.DeletedFor == "" {
+ // Hard-deleting messages requires updates to the messages table
+ where := "m.topic=? AND "
+ args := []interface{}{topic}
+ if len(toDel.SeqIdRanges) > 1 || toDel.SeqIdRanges[0].Hi == 0 {
+ seqRange := []int{}
+ for _, r := range toDel.SeqIdRanges {
+ if r.Hi == 0 {
+ seqRange = append(seqRange, r.Low)
+ } else {
+ for i := r.Low; i < r.Hi; i++ {
+ seqRange = append(seqRange, i)
+ }
+ }
+ }
+ args = append(args, seqRange)
+ where += "m.seqid IN (?)"
+
+ } else {
+ // Optimizing for a special case of single range low..hi.
+ where += "m.seqid BETWEEN ? AND ?"
+ // MySQL's BETWEEN is inclusive-inclusive thus decrement Hi by 1.
+ args = append(args, toDel.SeqIdRanges[0].Low, toDel.SeqIdRanges[0].Hi-1)
+ }
+ where += " AND m.deletedAt IS NULL"
+ query, newargs := expandQuery("DELETE FROM filemsglinks AS fml USING messages AS m WHERE m.id=fml.msgid AND "+
+ where, args...)
+
+ _, err = tx.Exec(ctx, query, newargs...)
+ if err != nil {
+ return err
+ }
+
+ query, newargs = expandQuery("UPDATE messages AS m SET deletedat=?,delid=?,head=NULL,content=NULL WHERE "+
+ where, t.TimeNow(), toDel.DelId, args)
+
+ _, err = tx.Exec(ctx, query, newargs...)
+ }
+ }
+
+ return err
+}
+
+// MessageDeleteList deletes messages in the given topic with seqIds from the list
+func (a *adapter) MessageDeleteList(topic string, toDel *t.DelMessage) (err error) {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ if err = messageDeleteList(ctx, tx, topic, toDel); err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+func deviceHasher(deviceID string) string {
+ // Generate custom key as [64-bit hash of device id] to ensure predictable
+ // length of the key
+ hasher := fnv.New64()
+ hasher.Write([]byte(deviceID))
+ return strconv.FormatUint(uint64(hasher.Sum64()), 16)
+}
+
+// Device management for push notifications
+func (a *adapter) DeviceUpsert(uid t.Uid, def *t.DeviceDef) error {
+ hash := deviceHasher(def.DeviceId)
+
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ // Ensure uniqueness of the device ID: delete all records of the device ID
+ _, err = tx.Exec(ctx, "DELETE FROM devices WHERE hash=$1", hash)
+ if err != nil {
+ return err
+ }
+
+ // Actually add/update DeviceId for the new user
+ _, err = tx.Exec(ctx, "INSERT INTO devices(userid, hash, deviceId, platform, lastseen, lang) VALUES($1,$2,$3,$4,$5,$6)",
+ store.DecodeUid(uid), hash, def.DeviceId, def.Platform, def.LastSeen, def.Lang)
+ if err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+func (a *adapter) DeviceGetAll(uids ...t.Uid) (map[t.Uid][]t.DeviceDef, int, error) {
+ var unupg []interface{}
+ for _, uid := range uids {
+ unupg = append(unupg, store.DecodeUid(uid))
+ }
+
+ query, unupg := expandQuery("SELECT userid,deviceid,platform,lastseen,lang FROM devices WHERE userid IN (?)", unupg)
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ rows, err := a.db.Query(ctx, query, unupg...)
+ if err != nil {
+ return nil, 0, err
+ }
+ defer rows.Close()
+
+ var device struct {
+ Userid int64
+ Deviceid string
+ Platform string
+ Lastseen time.Time
+ Lang string
+ }
+
+ result := make(map[t.Uid][]t.DeviceDef)
+ count := 0
+ for rows.Next() {
+ if err = rows.Scan(&device.Userid, &device.Deviceid, &device.Platform, &device.Lastseen, &device.Lang); err != nil {
+ break
+ }
+ uid := store.EncodeUid(device.Userid)
+ udev := result[uid]
+ udev = append(udev, t.DeviceDef{
+ DeviceId: device.Deviceid,
+ Platform: device.Platform,
+ LastSeen: device.Lastseen,
+ Lang: device.Lang,
+ })
+ result[uid] = udev
+ count++
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+
+ return result, count, err
+}
+
+func deviceDelete(ctx context.Context, tx pgx.Tx, uid t.Uid, deviceID string) error {
+ var err error
+ var res pgconn.CommandTag
+ if deviceID == "" {
+ res, err = tx.Exec(ctx, "DELETE FROM devices WHERE userid=$1", store.DecodeUid(uid))
+ } else {
+ res, err = tx.Exec(ctx, "DELETE FROM devices WHERE userid=$1 AND hash=$2", store.DecodeUid(uid), deviceHasher(deviceID))
+ }
+
+ if err == nil {
+ if count := res.RowsAffected(); count == 0 {
+ err = t.ErrNotFound
+ }
+ }
+
+ return err
+}
+
+func (a *adapter) DeviceDelete(uid t.Uid, deviceID string) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ err = deviceDelete(ctx, tx, uid, deviceID)
+ if err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+// Credential management
+
+// CredUpsert adds or updates a validation record. Returns true if inserted, false if updated.
+// 1. if credential is validated:
+// 1.1 Hard-delete unconfirmed equivalent record, if exists.
+// 1.2 Insert new. Report error if duplicate.
+// 2. if credential is not validated:
+// 2.1 Check if validated equivalent exist. If so, report an error.
+// 2.2 Soft-delete all unvalidated records of the same method.
+// 2.3 Undelete existing credential. Return if successful.
+// 2.4 Insert new credential record.
+func (a *adapter) CredUpsert(cred *t.Credential) (bool, error) {
+ var err error
+
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return false, err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ now := t.TimeNow()
+ userId := decodeUidString(cred.User)
+
+ // Enforce uniqueness: if credential is confirmed, "method:value" must be unique.
+ // if credential is not yet confirmed, "userid:method:value" is unique.
+ synth := cred.Method + ":" + cred.Value
+
+ if !cred.Done {
+ // Check if this credential is already validated.
+ var done bool
+ err = tx.QueryRow(ctx, "SELECT done FROM credentials WHERE synthetic=$1", synth).Scan(&done)
+ if err == nil {
+ // Assign err to ensure closing of a transaction.
+ err = t.ErrDuplicate
+ return false, err
+ }
+ if err != pgx.ErrNoRows {
+ return false, err
+ }
+ // We are going to insert new record.
+ synth = cred.User + ":" + synth
+
+ // Adding new unvalidated credential. Deactivate all unvalidated records of this user and method.
+ _, err = tx.Exec(ctx, "UPDATE credentials SET deletedat=$1 WHERE userid=$2 AND method=$3 AND done=FALSE",
+ now, userId, cred.Method)
+ if err != nil {
+ return false, err
+ }
+ // Assume that the record exists and try to update it: undelete, update timestamp and response value.
+ res, err := tx.Exec(ctx, "UPDATE credentials SET updatedat=$1,deletedat=NULL,resp=$2,done=FALSE WHERE synthetic=$3",
+ cred.UpdatedAt, cred.Resp, synth)
+ if err != nil {
+ return false, err
+ }
+ // If record was updated, then all is fine.
+ if numrows := res.RowsAffected(); numrows > 0 {
+ return false, tx.Commit(ctx)
+ }
+ } else {
+ // Hard-deleting unconformed record if it exists.
+ _, err = tx.Exec(ctx, "DELETE FROM credentials WHERE synthetic=$1", cred.User+":"+synth)
+ if err != nil {
+ return false, err
+ }
+ }
+
+ _, err = tx.Exec(ctx, "INSERT INTO credentials(createdat,updatedat,method,value,synthetic,userid,resp,done) "+
+ "VALUES($1,$2,$3,$4,$5,$6,$7,$8)",
+ cred.CreatedAt, cred.UpdatedAt, cred.Method, cred.Value, synth, userId, cred.Resp, cred.Done)
+ if err != nil {
+ if isDupe(err) {
+ return true, t.ErrDuplicate
+ }
+ return true, err
+ }
+ return true, tx.Commit(ctx)
+}
+
+// credDel deletes given validation method or all methods of the given user.
+// 1. If user is being deleted, hard-delete all records (method == "")
+// 2. If one value is being deleted:
+// 2.1 Delete it if it's valiated or if there were no attempts at validation
+// (otherwise it could be used to circumvent the limit on validation attempts).
+// 2.2 In that case mark it as soft-deleted.
+func credDel(ctx context.Context, tx pgx.Tx, uid t.Uid, method, value string) error {
+ constraints := " WHERE userid=?"
+ args := []interface{}{store.DecodeUid(uid)}
+
+ if method != "" {
+ constraints += " AND method=?"
+ args = append(args, method)
+
+ if value != "" {
+ constraints += " AND value=?"
+ args = append(args, value)
+ }
+ }
+ where, _ := expandQuery(constraints, args...)
+
+ var err error
+ var res pgconn.CommandTag
+ if method == "" {
+ // Case 1
+ res, err = tx.Exec(ctx, "DELETE FROM credentials"+where, args...)
+ if err == nil {
+ if count := res.RowsAffected(); count == 0 {
+ err = t.ErrNotFound
+ }
+ }
+ return err
+ }
+
+ // Case 2.1
+ res, err = tx.Exec(ctx, "DELETE FROM credentials"+where+" AND (done=true OR retries=0)", args...)
+ if err != nil {
+ return err
+ }
+ if count := res.RowsAffected(); count > 0 {
+ return nil
+ }
+
+ // Case 2.2
+ query, args := expandQuery("UPDATE credentials SET deletedat=?"+constraints, t.TimeNow(), args)
+ res, err = tx.Exec(ctx, query, args...)
+ if err == nil {
+ if count := res.RowsAffected(); count >= 0 {
+ err = t.ErrNotFound
+ }
+ }
+
+ return err
+}
+
+// CredDel deletes either credentials of the given user. If method is blank all
+// credentials are removed. If value is blank all credentials of the given the
+// method are removed.
+func (a *adapter) CredDel(uid t.Uid, method, value string) error {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ err = credDel(ctx, tx, uid, method, value)
+ if err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+// CredConfirm marks given credential method as confirmed.
+func (a *adapter) CredConfirm(uid t.Uid, method string) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ res, err := a.db.Exec(
+ ctx,
+ "UPDATE credentials SET updatedat=$1,done=true,synthetic=CONCAT(method,':',value) "+
+ "WHERE userid=$2 AND method=$3 AND deletedat IS NULL AND done=FALSE",
+ t.TimeNow(), store.DecodeUid(uid), method)
+ if err != nil {
+ if isDupe(err) {
+ return t.ErrDuplicate
+ }
+ return err
+ }
+ if numrows := res.RowsAffected(); numrows < 1 {
+ return t.ErrNotFound
+ }
+ return nil
+}
+
+// CredFail increments failure count of the given validation method.
+func (a *adapter) CredFail(uid t.Uid, method string) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ _, err := a.db.Exec(ctx, "UPDATE credentials SET updatedat=$1,retries=retries+1 WHERE userid=$2 AND method=$3 AND done=FALSE",
+ t.TimeNow(), store.DecodeUid(uid), method)
+ return err
+}
+
+// CredGetActive returns currently active unvalidated credential of the given user and method.
+func (a *adapter) CredGetActive(uid t.Uid, method string) (*t.Credential, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ var cred t.Credential
+
+ err := a.db.QueryRow(ctx, "SELECT createdat,updatedat,method,value,resp,done,retries "+
+ "FROM credentials WHERE userid=$1 AND deletedat IS NULL AND method=$2 AND done=FALSE",
+ store.DecodeUid(uid), method).Scan(&cred.CreatedAt, &cred.UpdatedAt, &cred.Method, &cred.Value, &cred.Resp, &cred.Done, &cred.Retries)
+
+ if err != nil {
+ if err == pgx.ErrNoRows {
+ err = nil
+ }
+ return nil, err
+ }
+
+ cred.User = uid.String()
+
+ return &cred, nil
+}
+
+// CredGetAll returns credential records for the given user and method, all or validated only.
+func (a *adapter) CredGetAll(uid t.Uid, method string, validatedOnly bool) ([]t.Credential, error) {
+ query := "SELECT createdat,updatedat,method,value,resp,done,retries FROM credentials WHERE userid=$1 AND deletedat IS NULL"
+ args := []interface{}{store.DecodeUid(uid)}
+ if method != "" {
+ query += " AND method=$2"
+ args = append(args, method)
+ }
+ if validatedOnly {
+ query += " AND done=TRUE"
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ var credentials []t.Credential
+ rows, err := a.db.Query(ctx, query, args...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ var cred t.Credential
+ if err = rows.Scan(&cred.CreatedAt, &cred.UpdatedAt, &cred.Method, &cred.Value, &cred.Resp, &cred.Done, &cred.Retries); err != nil {
+ credentials = nil
+ break
+ }
+
+ credentials = append(credentials, cred)
+ }
+
+ user := uid.String()
+ for i := range credentials {
+ credentials[i].User = user
+ }
+
+ return credentials, err
+}
+
+// FileUploads
+
+// FileStartUpload initializes a file upload
+func (a *adapter) FileStartUpload(fd *t.FileDef) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ var user interface{}
+ if fd.User != "" {
+ user = store.DecodeUid(t.ParseUid(fd.User))
+ }
+ _, err := a.db.Exec(ctx,
+ "INSERT INTO fileuploads(id,createdat,updatedat,userid,status,mimetype,size,location) "+
+ "VALUES($1,$2,$3,$4,$5,$6,$7,$8)",
+ store.DecodeUid(fd.Uid()), fd.CreatedAt, fd.UpdatedAt, user,
+ fd.Status, fd.MimeType, fd.Size, fd.Location)
+ return err
+}
+
+// FileFinishUpload marks file upload as completed, successfully or otherwise
+func (a *adapter) FileFinishUpload(fd *t.FileDef, success bool, size int64) (*t.FileDef, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ now := t.TimeNow()
+ if success {
+ _, err = tx.Exec(ctx, "UPDATE fileuploads SET updatedat=$1,status=$2,size=$3 WHERE id=$4",
+ now, t.UploadCompleted, size, store.DecodeUid(fd.Uid()))
+ if err != nil {
+ return nil, err
+ }
+
+ fd.Status = t.UploadCompleted
+ fd.Size = size
+ } else {
+ // Deleting the record: there is no value in keeping it in the DB.
+ _, err = tx.Exec(ctx, "DELETE FROM fileuploads WHERE id=$1", store.DecodeUid(fd.Uid()))
+ if err != nil {
+ return nil, err
+ }
+
+ fd.Status = t.UploadFailed
+ fd.Size = 0
+ }
+ fd.UpdatedAt = now
+
+ return fd, tx.Commit(ctx)
+}
+
+// FileGet fetches a record of a specific file
+func (a *adapter) FileGet(fid string) (*t.FileDef, error) {
+ id := t.ParseUid(fid)
+ if id.IsZero() {
+ return nil, t.ErrMalformed
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+ var fd t.FileDef
+ var ID int64
+ var userId int64
+ err := a.db.QueryRow(ctx, "SELECT id,createdat,updatedat,userid AS user,status,mimetype,size,location "+
+ "FROM fileuploads WHERE id=$1", store.DecodeUid(id)).Scan(&ID, &fd.CreatedAt, &fd.UpdatedAt, &userId, &fd.Status, &fd.MimeType, &fd.Size, &fd.Location)
+ if err == pgx.ErrNoRows {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ fd.SetUid(store.EncodeUid(ID))
+ fd.User = store.EncodeUid(userId).String()
+
+ return &fd, nil
+
+}
+
+// FileDeleteUnused deletes file upload records.
+func (a *adapter) FileDeleteUnused(olderThan time.Time, limit int) ([]string, error) {
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ // Garbage collecting entries which as either marked as deleted, or lack message references, or have no user assigned.
+ query := "SELECT fu.id,fu.location FROM fileuploads AS fu LEFT JOIN filemsglinks AS fml ON fml.fileid=fu.id " +
+ "WHERE fml.id IS NULL"
+ var args []interface{}
+
+ if !olderThan.IsZero() {
+ query += " AND fu.updatedat"
+ args = append(args, olderThan)
+ }
+ if limit > 0 {
+ query += " LIMIT ?"
+ args = append(args, limit)
+ }
+ query, _ = expandQuery(query, args...)
+
+ rows, err := tx.Query(ctx, query, args...)
+ if err != nil {
+ return nil, err
+ }
+
+ var locations []string
+ var ids []interface{}
+ for rows.Next() {
+ var id int
+ var loc string
+ if err = rows.Scan(&id, &loc); err != nil {
+ break
+ }
+ if loc != "" {
+ locations = append(locations, loc)
+ }
+ ids = append(ids, id)
+ }
+ if err == nil {
+ err = rows.Err()
+ }
+ rows.Close()
+
+ if err != nil {
+ return nil, err
+ }
+
+ if len(ids) > 0 {
+ query, ids = expandQuery("DELETE FROM fileuploads WHERE id IN (?)", ids)
+ _, err = tx.Exec(ctx, query, ids...)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return locations, tx.Commit(ctx)
+}
+
+// FileLinkAttachments connects given topic or message to the file record IDs from the list.
+func (a *adapter) FileLinkAttachments(topic string, userId, msgId t.Uid, fids []string) error {
+ if len(fids) == 0 || (topic == "" && msgId.IsZero() && userId.IsZero()) {
+ return t.ErrMalformed
+ }
+ now := t.TimeNow()
+
+ var args []interface{}
+ var linkId interface{}
+ var linkBy string
+ if !msgId.IsZero() {
+ linkBy = "msgid"
+ linkId = int64(msgId)
+ } else if topic != "" {
+ linkBy = "topic"
+ linkId = topic
+ // Only one attachment per topic is permitted at this time.
+ fids = fids[0:1]
+ } else {
+ linkBy = "userid"
+ linkId = store.DecodeUid(userId)
+ // Only one attachment per user is permitted at this time.
+ fids = fids[0:1]
+ }
+
+ // Decoded ids
+ var dids []interface{}
+ for _, fid := range fids {
+ id := t.ParseUid(fid)
+ if id.IsZero() {
+ return t.ErrMalformed
+ }
+ dids = append(dids, store.DecodeUid(id))
+ }
+
+ for _, id := range dids {
+ // createdat,fileid,[msgid|topic|userid]
+ args = append(args, now, id, linkId)
+ }
+
+ ctx, cancel := a.getContextForTx()
+ if cancel != nil {
+ defer cancel()
+ }
+ tx, err := a.db.BeginTx(ctx, pgx.TxOptions{})
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback(ctx)
+ }
+ }()
+
+ // Unlink earlier uploads on the same topic or user allowing them to be garbage-collected.
+ if msgId.IsZero() {
+ sql := "DELETE FROM filemsglinks WHERE " + linkBy + "=$1"
+ _, err = tx.Exec(ctx, sql, linkId)
+ if err != nil {
+ return err
+ }
+ }
+
+ query, args := expandQuery("INSERT INTO filemsglinks(createdat,fileid,"+linkBy+") VALUES (?,?,?)"+
+ strings.Repeat(",(?,?,?)", len(dids)-1), args...)
+
+ _, err = tx.Exec(ctx, query, args...)
+ if err != nil {
+ return err
+ }
+
+ return tx.Commit(ctx)
+}
+
+// PCacheGet reads a persistet cache entry.
+func (a *adapter) PCacheGet(key string) (string, error) {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ var value string
+ if err := a.db.QueryRow(ctx, `SELECT "value" FROM kvmeta WHERE "key"=$1 LIMIT 1`, key).Scan(&value); err != nil {
+ if err == pgx.ErrNoRows {
+ return "", t.ErrNotFound
+ }
+ return "", err
+ }
+ return value, nil
+}
+
+// PCacheUpsert creates or updates a persistent cache entry.
+func (a *adapter) PCacheUpsert(key string, value string, failOnDuplicate bool) error {
+ if strings.Contains(key, "%") {
+ // Do not allow % in keys: it interferes with LIKE query.
+ return t.ErrMalformed
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ var action string
+ if failOnDuplicate {
+ action = "INSERT"
+ } else {
+ action = "REPLACE"
+ }
+
+ _, err := a.db.Exec(ctx, action+` INTO kvmeta("key",createdat,"value") VALUES($1,$2,$3)`, key, t.TimeNow(), value)
+ if isDupe(err) {
+ return t.ErrDuplicate
+ }
+ return err
+}
+
+// PCacheDelete deletes one persistent cache entry.
+func (a *adapter) PCacheDelete(key string) error {
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ _, err := a.db.Exec(ctx, `DELETE FROM kvmeta WHERE "key"=$1`, key)
+ return err
+}
+
+// PCacheExpire expires old entries with the given key prefix.
+func (a *adapter) PCacheExpire(keyPrefix string, olderThan time.Time) error {
+ if keyPrefix == "" {
+ return t.ErrMalformed
+ }
+
+ ctx, cancel := a.getContext()
+ if cancel != nil {
+ defer cancel()
+ }
+
+ _, err := a.db.Exec(ctx, `DELETE FROM kvmeta WHERE "key" LIKE $1 AND createdat<$2`, keyPrefix+"%", olderThan)
+ return err
+}
+
+// Helper functions
+
+// Check if MySQL error is a Error Code: 1062. Duplicate entry ... for key ...
+func isDupe(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ msg := err.Error()
+ return strings.Contains(msg, "SQLSTATE 23505")
+}
+
+func isMissingTable(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ msg := err.Error()
+ return strings.Contains(msg, "SQLSTATE 42P01")
+}
+
+func isMissingDb(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ msg := err.Error()
+ return strings.Contains(msg, "SQLSTATE 3D000")
+}
+
+// Convert to JSON before storing to JSON field.
+func toJSON(src interface{}) []byte {
+ if src == nil {
+ return nil
+ }
+
+ jval, _ := json.Marshal(src)
+ return jval
+}
+
+// Deserialize JSON data from DB.
+func fromJSON(src interface{}) interface{} {
+ if src == nil {
+ return nil
+ }
+ if bb, ok := src.([]byte); ok {
+ var out interface{}
+ json.Unmarshal(bb, &out)
+ return out
+ }
+ return nil
+}
+
+// UIDs are stored as decoded int64 values. Take decoded string representation of int64, produce UID.
+func encodeUidString(str string) t.Uid {
+ unum, _ := strconv.ParseInt(str, 10, 64)
+ return store.EncodeUid(unum)
+}
+
+func decodeUidString(str string) int64 {
+ uid := t.ParseUid(str)
+ return store.DecodeUid(uid)
+}
+
+// Convert update to a list of columns and arguments.
+func updateByMap(update map[string]interface{}) (cols []string, args []interface{}) {
+ for col, arg := range update {
+ col = strings.ToLower(col)
+ if col == "public" || col == "trusted" || col == "private" {
+ arg = toJSON(arg)
+ }
+ cols = append(cols, col+"=?")
+ args = append(args, arg)
+ }
+ return
+}
+
+// If Tags field is updated, get the tags so tags table cab be updated too.
+func extractTags(update map[string]interface{}) []string {
+ var tags []string
+
+ if val := update["Tags"]; val != nil {
+ tags, _ = val.(t.StringSlice)
+ }
+
+ return []string(tags)
+}
+
+// Converting a structure with data to enter a connection string
+func setConnStr(c configType) (string, error) {
+ if c.User == "" || c.Passwd == "" || c.Host == "" || c.Port == "" || c.DBName == "" {
+ return "", errors.New("adapter postgres invalid config value")
+ }
+ connStr := fmt.Sprintf("%s://%s:%s@%s:%s/%s?sslmode=disable&connect_timeout=%d",
+ "postgres",
+ c.User,
+ c.Passwd,
+ c.Host,
+ c.Port,
+ c.DBName,
+ c.SqlTimeout)
+
+ return connStr, nil
+}
+
+func expandQuery(query string, args ...interface{}) (string, []interface{}) {
+ var expandedArgs []interface{}
+ var expandedQuery string
+
+ if len(args) != strings.Count(query, "?") {
+ args = flatMap(args)
+ }
+
+ expandedQuery, expandedArgs, _ = sqlx.In(query, args...)
+
+ placeholders := make([]string, len(expandedArgs))
+ for i := range expandedArgs {
+ placeholders[i] = "$" + strconv.Itoa(i+1)
+ expandedQuery = strings.Replace(expandedQuery, "?", placeholders[i], 1)
+ }
+
+ return expandedQuery, expandedArgs
+}
+
+func flatMap(slice []interface{}) []interface{} {
+ var result []interface{}
+ for _, v := range slice {
+ switch reflect.TypeOf(v).Kind() {
+ case reflect.Slice:
+ s := reflect.ValueOf(v)
+ for i := 0; i < s.Len(); i++ {
+ result = append(result, s.Index(i).Interface())
+ }
+ default:
+ result = append(result, v)
+ }
+ }
+ return result
+}
+
+func init() {
+ store.RegisterAdapter(&adapter{})
+}
+
+func (a *adapter) GetTopicsLastMsgWriter(subs []t.Subscription) []t.Subscription {
+ return subs
+}
diff --git a/server/db/postgres/blank.go b/server/db/postgres/blank.go
new file mode 100644
index 000000000..a805fb286
--- /dev/null
+++ b/server/db/postgres/blank.go
@@ -0,0 +1,8 @@
+//go:build !postgres
+// +build !postgres
+
+// This file is needed for conditional compilation. It's used when
+// the build tag 'postgres' is not defined. Otherwise the adapter.go
+// is compiled.
+
+package postgres
diff --git a/server/db/postgres/schema.sql b/server/db/postgres/schema.sql
new file mode 100644
index 000000000..3a1df0b85
--- /dev/null
+++ b/server/db/postgres/schema.sql
@@ -0,0 +1,2 @@
+# The MySQL and PostrgreSQL schemas are identical save for differences in SQL flavors.
+# SEE ../mysql/schema.sql.
diff --git a/server/db/rethinkdb/adapter.go b/server/db/rethinkdb/adapter.go
index 74bf0a1ce..dd5d34967 100644
--- a/server/db/rethinkdb/adapter.go
+++ b/server/db/rethinkdb/adapter.go
@@ -34,7 +34,7 @@ const (
defaultHost = "localhost:28015"
defaultDatabase = "tinode"
- adpVersion = 112
+ adpVersion = 113
adapterName = "rethinkdb"
@@ -549,6 +549,14 @@ func (a *adapter) UpgradeDb() error {
}
}
+ if a.version == 112 {
+ // Secondary indexes cannot store NULLs, consequently no useful indexes can be created.
+ // Just bump the version.
+ if err := bumpVersion(a, 113); err != nil {
+ return err
+ }
+ }
+
if a.version != adpVersion {
return errors.New("Failed to perform database upgrade to version " + strconv.Itoa(adpVersion) +
". DB is still at " + strconv.Itoa(a.version))
@@ -1884,7 +1892,7 @@ func (a *adapter) subsDelForUser(user t.Uid, hard bool) error {
// FindUsers returns a list of users who match given tags, such as "email:jdoe@example.com" or "tel:+18003287448".
// Searching the 'users.Tags' for the given tags using respective index.
-func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string) ([]t.Subscription, error) {
+func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
index := make(map[string]struct{})
allReq := t.FlattenDoubleSlice(req)
var allTags []interface{}
@@ -1909,9 +1917,11 @@ func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string) ([]t.Subscr
// Get users matched by tags, sort by number of matches from high to low.
query := rdb.DB(a.dbName).
Table("users").
- GetAllByIndex("Tags", allTags...).
- Filter(rdb.Row.Field("State").Eq(t.StateOK)).
- Pluck("Id", "Access", "CreatedAt", "UpdatedAt", "Public", "Trusted", "Tags").
+ GetAllByIndex("Tags", allTags...)
+ if activeOnly {
+ query = query.Filter(rdb.Row.Field("State").Eq(t.StateOK))
+ }
+ query = query.Pluck("Id", "Access", "CreatedAt", "UpdatedAt", "Public", "Trusted", "Tags").
Group("Id").
Ungroup().
Map(func(row rdb.Term) rdb.Term {
@@ -1968,7 +1978,7 @@ func (a *adapter) FindUsers(uid t.Uid, req [][]string, opt []string) ([]t.Subscr
// FindTopics returns a list of topics with matching tags.
// Searching the 'topics.Tags' for the given tags using respective index.
-func (a *adapter) FindTopics(req [][]string, opt []string) ([]t.Subscription, error) {
+func (a *adapter) FindTopics(req [][]string, opt []string, activeOnly bool) ([]t.Subscription, error) {
index := make(map[string]struct{})
var allReq []string
for _, el := range req {
@@ -1981,9 +1991,11 @@ func (a *adapter) FindTopics(req [][]string, opt []string) ([]t.Subscription, er
}
query := rdb.DB(a.dbName).
Table("topics").
- GetAllByIndex("Tags", allTags...).
- Filter(rdb.Row.Field("State").Eq(t.StateOK)).
- Pluck("Id", "Access", "CreatedAt", "UpdatedAt", "UseBt", "Public", "Trusted", "Tags").
+ GetAllByIndex("Tags", allTags...)
+ if activeOnly {
+ query = query.Filter(rdb.Row.Field("State").Eq(t.StateOK))
+ }
+ query = query.Pluck("Id", "Access", "CreatedAt", "UpdatedAt", "UseBt", "Public", "Trusted", "Tags").
Group("Id").
Ungroup().
Map(func(row rdb.Term) rdb.Term {
@@ -2778,6 +2790,74 @@ func (a *adapter) decFileUseCounter(msgQuery rdb.Term) error {
return err
}
+// PCacheGet reads a persistet cache entry.
+func (a *adapter) PCacheGet(key string) (string, error) {
+ cursor, err := rdb.DB(a.dbName).Table("kvmeta").Get(key).Field("value").Run(a.conn)
+ if err != nil {
+ return "", err
+ }
+ defer cursor.Close()
+
+ if cursor.IsNil() {
+ return "", t.ErrNotFound
+ }
+
+ var value string
+ if err = cursor.One(&value); err != nil {
+ return "", err
+ }
+
+ return value, nil
+}
+
+// PCacheUpsert creates or updates a persistent cache entry.
+func (a *adapter) PCacheUpsert(key string, value string, failOnDuplicate bool) error {
+ if strings.Contains(key, "^") {
+ // Do not allow ^ in keys: it interferes with Match() query.
+ return t.ErrMalformed
+ }
+
+ doc := map[string]interface{}{
+ "key": key,
+ "value": value,
+ }
+
+ var action string
+ if failOnDuplicate {
+ action = "error"
+ doc["CreatedAt"] = t.TimeNow()
+ } else {
+ action = "update"
+ }
+
+ _, err := rdb.DB(a.dbName).Table("kvmeta").Insert(doc, rdb.InsertOpts{Conflict: action}).RunWrite(a.conn)
+ if rdb.IsConflictErr(err) {
+ return t.ErrDuplicate
+ }
+
+ return err
+}
+
+// PCacheDelete deletes one persistent cache entry.
+func (a *adapter) PCacheDelete(key string) error {
+ _, err := rdb.DB(a.dbName).Table("kvmeta").Get(key).Delete().RunWrite(a.conn)
+ return err
+}
+
+// PCacheExpire expires old entries with the given key prefix.
+func (a *adapter) PCacheExpire(keyPrefix string, olderThan time.Time) error {
+ if keyPrefix == "" {
+ return t.ErrMalformed
+ }
+
+ _, err := rdb.DB(a.dbName).Table("kvmeta").
+ Filter(rdb.Row.Field("CreatedAt").Lt(olderThan).And(rdb.Row.Field("key").Match("^" + keyPrefix))).
+ Delete().
+ RunWrite(a.conn)
+
+ return err
+}
+
// Checks if the given error is 'Database not found'.
func isMissingDb(err error) bool {
if err == nil {
diff --git a/server/drafty/drafty.go b/server/drafty/drafty.go
index fe8a253c6..79c534ea1 100644
--- a/server/drafty/drafty.go
+++ b/server/drafty/drafty.go
@@ -103,7 +103,7 @@ type plainTextState struct {
}
// PlainText converts drafty document to plain text with some basic markdown-like formatting.
-// Deprecated. Use Preview for new development.
+// Deprecated: use Preview for new development.
func PlainText(content interface{}) (string, error) {
doc, err := decodeAsDrafty(content)
if err != nil {
diff --git a/server/hdl_files.go b/server/hdl_files.go
index e3ab0f20a..b9bd1186c 100644
--- a/server/hdl_files.go
+++ b/server/hdl_files.go
@@ -15,6 +15,7 @@ import (
"io"
"math/rand"
"net/http"
+ "strconv"
"strings"
"time"
@@ -39,6 +40,32 @@ func largeFileServe(wrt http.ResponseWriter, req *http.Request) {
}
}
+ // Preflight request: process before any security checks.
+ if req.Method == http.MethodOptions {
+ headers, statusCode, err := mh.Headers(req, true)
+ if err != nil {
+ writeHttpResponse(decodeStoreError(err, "", now, nil), err)
+ return
+ }
+ for name, values := range headers {
+ for _, value := range values {
+ wrt.Header().Add(name, value)
+ }
+ }
+ if statusCode <= 0 {
+ statusCode = http.StatusNoContent
+ }
+ wrt.WriteHeader(statusCode)
+ logs.Info.Println("media serve: preflight completed")
+ return
+ }
+
+ // Check if this is a GET/HEAD request.
+ if req.Method != http.MethodGet && req.Method != http.MethodHead {
+ writeHttpResponse(ErrOperationNotAllowed("", "", now), errors.New("method '"+req.Method+"' not allowed"))
+ return
+ }
+
// Check for API key presence
if isValid, _ := checkAPIKey(getAPIKey(req)); !isValid {
writeHttpResponse(ErrAPIKeyRequired(now), errors.New("invalid or missing API key"))
@@ -48,7 +75,7 @@ func largeFileServe(wrt http.ResponseWriter, req *http.Request) {
// Check authorization: either auth information or SID must be present
uid, challenge, err := authHttpRequest(req)
if err != nil {
- writeHttpResponse(decodeStoreError(err, "", "", now, nil), err)
+ writeHttpResponse(decodeStoreError(err, "", now, nil), err)
return
}
@@ -72,7 +99,7 @@ func largeFileServe(wrt http.ResponseWriter, req *http.Request) {
// Check if media handler redirects or adds headers.
headers, statusCode, err := mh.Headers(req, true)
if err != nil {
- writeHttpResponse(decodeStoreError(err, "", "", now, nil), err)
+ writeHttpResponse(decodeStoreError(err, "", now, nil), err)
return
}
@@ -98,7 +125,7 @@ func largeFileServe(wrt http.ResponseWriter, req *http.Request) {
return
}
- if req.Method == http.MethodHead || req.Method == http.MethodOptions {
+ if req.Method == http.MethodHead {
wrt.WriteHeader(http.StatusOK)
logs.Info.Println("media serve: completed", req.Method, "uid=", uid)
return
@@ -106,14 +133,16 @@ func largeFileServe(wrt http.ResponseWriter, req *http.Request) {
fd, rsc, err := mh.Download(req.URL.String())
if err != nil {
- writeHttpResponse(decodeStoreError(err, "", "", now, nil), err)
+ writeHttpResponse(decodeStoreError(err, "", now, nil), err)
return
}
defer rsc.Close()
wrt.Header().Set("Content-Type", fd.MimeType)
- wrt.Header().Set("Content-Disposition", "attachment")
+ if isAttachment, _ := strconv.ParseBool(req.URL.Query().Get("asatt")); isAttachment {
+ wrt.Header().Set("Content-Disposition", "attachment")
+ }
http.ServeContent(wrt, req, "", fd.UpdatedAt, rsc)
logs.Info.Println("media serve: OK, uid=", uid)
@@ -137,9 +166,28 @@ func largeFileReceive(wrt http.ResponseWriter, req *http.Request) {
}
}
- // Check if this is a POST/PUT/OPTIONS/HEAD request.
- if req.Method != http.MethodPost && req.Method != http.MethodPut &&
- req.Method != http.MethodHead && req.Method != http.MethodOptions {
+ // Preflight request: process before any security checks.
+ if req.Method == http.MethodOptions {
+ headers, statusCode, err := mh.Headers(req, false)
+ if err != nil {
+ writeHttpResponse(decodeStoreError(err, "", now, nil), err)
+ return
+ }
+ for name, values := range headers {
+ for _, value := range values {
+ wrt.Header().Add(name, value)
+ }
+ }
+ if statusCode <= 0 {
+ statusCode = http.StatusNoContent
+ }
+ wrt.WriteHeader(statusCode)
+ logs.Info.Println("media upload: preflight completed")
+ return
+ }
+
+ // Check if this is a POST/PUT/HEAD request.
+ if req.Method != http.MethodPost && req.Method != http.MethodPut && req.Method != http.MethodHead {
writeHttpResponse(ErrOperationNotAllowed("", "", now), errors.New("method '"+req.Method+"' not allowed"))
return
}
@@ -159,7 +207,7 @@ func largeFileReceive(wrt http.ResponseWriter, req *http.Request) {
// Check authorization: either auth information or SID must be present
uid, challenge, err := authHttpRequest(req)
if err != nil {
- writeHttpResponse(decodeStoreError(err, msgID, "", now, nil), err)
+ writeHttpResponse(decodeStoreError(err, msgID, now, nil), err)
return
}
if challenge != nil {
@@ -175,8 +223,8 @@ func largeFileReceive(wrt http.ResponseWriter, req *http.Request) {
// Check if uploads are handled elsewhere.
headers, statusCode, err := mh.Headers(req, false)
if err != nil {
- logs.Info.Println("Headers check failed", err)
- writeHttpResponse(decodeStoreError(err, "", "", now, nil), err)
+ logs.Info.Println("media upload: headers check failed", err)
+ writeHttpResponse(decodeStoreError(err, "", now, nil), err)
return
}
@@ -208,9 +256,9 @@ func largeFileReceive(wrt http.ResponseWriter, req *http.Request) {
return
}
- file, _, err := req.FormFile("file")
+ file, header, err := req.FormFile("file")
if err != nil {
- logs.Info.Println("Invalid multipart form", err)
+ logs.Info.Println("media upload: invalid multipart form", err)
if strings.Contains(err.Error(), "request body too large") {
writeHttpResponse(ErrTooLarge(msgID, "", now), err)
} else {
@@ -224,12 +272,21 @@ func largeFileReceive(wrt http.ResponseWriter, req *http.Request) {
writeHttpResponse(ErrUnknown(msgID, "", now), err)
return
}
+
+ mimeType := http.DetectContentType(buff)
+ // If DetectContentType fails, use client-provided content type.
+ if mimeType == "application/octet-stream" {
+ if contentType := header.Header.Get("Content-Type"); contentType != "" {
+ mimeType = contentType
+ }
+ }
+
fdef := &types.FileDef{
ObjHeader: types.ObjHeader{
Id: store.Store.GetUidString(),
},
User: uid.String(),
- MimeType: http.DetectContentType(buff),
+ MimeType: mimeType,
}
fdef.InitTimes()
@@ -240,18 +297,18 @@ func largeFileReceive(wrt http.ResponseWriter, req *http.Request) {
url, size, err := mh.Upload(fdef, file)
if err != nil {
- logs.Info.Println("Upload failed", file, "key", fdef.Location, err)
+ logs.Info.Println("media upload: failed", file, "key", fdef.Location, err)
store.Files.FinishUpload(fdef, false, 0)
- writeHttpResponse(decodeStoreError(err, msgID, "", now, nil), err)
+ writeHttpResponse(decodeStoreError(err, msgID, now, nil), err)
return
}
fdef, err = store.Files.FinishUpload(fdef, true, size)
if err != nil {
- logs.Info.Println("Failed to finalize upload", file, "key", fdef.Location, err)
+ logs.Info.Println("media upload: failed to finalize", file, "key", fdef.Location, err)
// Best effort cleanup.
mh.Delete([]string{fdef.Location})
- writeHttpResponse(decodeStoreError(err, msgID, "", now, nil), err)
+ writeHttpResponse(decodeStoreError(err, msgID, now, nil), err)
return
}
diff --git a/server/hdl_grpc.go b/server/hdl_grpc.go
index 825c65800..c11e92d1b 100644
--- a/server/hdl_grpc.go
+++ b/server/hdl_grpc.go
@@ -76,7 +76,7 @@ func (*grpcNodeServer) MessageLoop(stream pbx.Node_MessageLoopServer) error {
return nil
}
-func (sess *Session) sendMessageGrpc(msg interface{}) bool {
+func (sess *Session) sendMessageGrpc(msg any) bool {
if len(sess.send) > sendQueueLimit {
logs.Err.Println("grpc: outbound queue limit exceeded", sess.sid)
return false
@@ -139,7 +139,7 @@ func (sess *Session) writeGrpcLoop() {
}
}
-func grpcWrite(sess *Session, msg interface{}) error {
+func grpcWrite(sess *Session, msg any) error {
if out := sess.grpcnode; out != nil {
// handle panic runtime error: invalid memory address or nil pointer dereference
defer func() {
@@ -170,10 +170,6 @@ func serveGrpc(addr string, kaEnabled bool, tlsConf *tls.Config) (*grpc.Server,
secure := ""
var opts []grpc.ServerOption
opts = append(opts, grpc.MaxRecvMsgSize(int(globals.maxMessageSize)))
- if tlsConf != nil {
- opts = append(opts, grpc.Creds(credentials.NewTLS(tlsConf)))
- secure = " secure"
- }
if kaEnabled {
logs.Info.Printf("gRPC server kaEnabled")
@@ -192,6 +188,11 @@ func serveGrpc(addr string, kaEnabled bool, tlsConf *tls.Config) (*grpc.Server,
opts = append(opts, grpc.KeepaliveParams(kpConfig))
}
+ if tlsConf != nil {
+ opts = append(opts, grpc.Creds(credentials.NewTLS(tlsConf)))
+ secure = " secure"
+ }
+
srv := grpc.NewServer(opts...)
pbx.RegisterNodeServer(srv, &grpcNodeServer{})
logs.Info.Printf("gRPC/%s%s server is registered at [%s]", grpc.Version, secure, addr)
diff --git a/server/hdl_longpoll.go b/server/hdl_longpoll.go
index 5a061f63f..d0ffff3f1 100644
--- a/server/hdl_longpoll.go
+++ b/server/hdl_longpoll.go
@@ -12,14 +12,14 @@ package main
import (
"encoding/json"
"errors"
- "io/ioutil"
+ "io"
"net/http"
"time"
"github.com/tinode/chat/server/logs"
)
-func (sess *Session) sendMessageLp(wrt http.ResponseWriter, msg interface{}) bool {
+func (sess *Session) sendMessageLp(wrt http.ResponseWriter, msg any) bool {
if len(sess.send) > sendQueueLimit {
logs.Err.Println("longPoll: outbound queue limit exceeded", sess.sid)
return false
@@ -88,7 +88,7 @@ func (sess *Session) writeOnce(wrt http.ResponseWriter, req *http.Request) {
}
}
-func lpWrite(wrt http.ResponseWriter, msg interface{}) error {
+func lpWrite(wrt http.ResponseWriter, msg any) error {
// This will panic if msg is not []byte. This is intentional.
wrt.Write(msg.([]byte))
return nil
@@ -100,7 +100,7 @@ func (sess *Session) readOnce(wrt http.ResponseWriter, req *http.Request) (int,
}
req.Body = http.MaxBytesReader(wrt, req.Body, globals.maxMessageSize)
- raw, err := ioutil.ReadAll(req.Body)
+ raw, err := io.ReadAll(req.Body)
if err == nil {
// Locking-unlocking is needed because the client may issue multiple requests in parallel.
// Should not affect performance
diff --git a/server/hdl_websock.go b/server/hdl_websock.go
index 83eb58a6a..347b42544 100644
--- a/server/hdl_websock.go
+++ b/server/hdl_websock.go
@@ -63,7 +63,7 @@ func (sess *Session) readLoop() {
}
}
-func (sess *Session) sendMessage(msg interface{}) bool {
+func (sess *Session) sendMessage(msg any) bool {
if len(sess.send) > sendQueueLimit {
logs.Err.Println("ws: outbound queue limit exceeded", sess.sid)
return false
@@ -144,7 +144,7 @@ func (sess *Session) writeLoop() {
}
// Writes a message with the given message type (mt) and payload.
-func wsWrite(ws *websocket.Conn, mt int, msg interface{}) error {
+func wsWrite(ws *websocket.Conn, mt int, msg any) error {
var bits []byte
if msg != nil {
bits = msg.([]byte)
@@ -157,8 +157,9 @@ func wsWrite(ws *websocket.Conn, mt int, msg interface{}) error {
// Handles websocket requests from peers.
var upgrader = websocket.Upgrader{
- ReadBufferSize: 1024,
- WriteBufferSize: 1024,
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ EnableCompression: globals.wsCompression,
// Allow connections from any Origin
CheckOrigin: func(r *http.Request) bool { return true },
}
diff --git a/server/http.go b/server/http.go
index b6375c459..a16dddc26 100644
--- a/server/http.go
+++ b/server/http.go
@@ -36,7 +36,11 @@ func listenAndServe(addr string, mux *http.ServeMux, tlfConf *tls.Config, stop <
httpdone := make(chan bool)
server := &http.Server{
- Handler: mux,
+ Handler: mux,
+ ReadHeaderTimeout: 10 * time.Second,
+ IdleTimeout: 30 * time.Second,
+ WriteTimeout: 90 * time.Second,
+ MaxHeaderBytes: 1 << 14,
}
server.TLSConfig = tlfConf
@@ -398,13 +402,21 @@ type debugTopic struct {
Sessions []string `json:"sessions,omitempty"`
}
+// debugCachedUser is a user cache entry debug info.
+type debugCachedUser struct {
+ Uid string `json:"uid,omitempty"`
+ Unread int `json:"unread,omitempty"`
+ Topics int `json:"topics,omitempty"`
+}
+
// debugDump is server internal state dump for debugging.
type debugDump struct {
- Version string `json:"server_version,omitempty"`
- Build string `json:"build_id,omitempty"`
- Timestamp time.Time `json:"ts,omitempty"`
- Sessions []debugSession `json:"sessions,omitempty"`
- Topics []debugTopic `json:"topics,omitempty"`
+ Version string `json:"server_version,omitempty"`
+ Build string `json:"build_id,omitempty"`
+ Timestamp time.Time `json:"ts,omitempty"`
+ Sessions []debugSession `json:"sessions,omitempty"`
+ Topics []debugTopic `json:"topics,omitempty"`
+ UserCache []debugCachedUser `json:"user_cache,omitempty"`
}
func serveStatus(wrt http.ResponseWriter, req *http.Request) {
@@ -416,6 +428,7 @@ func serveStatus(wrt http.ResponseWriter, req *http.Request) {
Timestamp: types.TimeNow(),
Sessions: make([]debugSession, 0, len(globals.sessionStore.sessCache)),
Topics: make([]debugTopic, 0, 10),
+ UserCache: make([]debugCachedUser, 0, 10),
}
// Sessions.
globals.sessionStore.Range(func(sid string, s *Session) bool {
@@ -439,7 +452,7 @@ func serveStatus(wrt http.ResponseWriter, req *http.Request) {
return true
})
// Topics.
- globals.hub.topics.Range(func(_, t interface{}) bool {
+ globals.hub.topics.Range(func(_, t any) bool {
topic := t.(*Topic)
psd := make([]string, 0, len(topic.sessions))
for s := range topic.sessions {
@@ -463,6 +476,13 @@ func serveStatus(wrt http.ResponseWriter, req *http.Request) {
})
return true
})
+ for k, v := range usersCache {
+ result.UserCache = append(result.UserCache, debugCachedUser{
+ Uid: k.UserId(),
+ Unread: v.unread,
+ Topics: v.topics,
+ })
+ }
json.NewEncoder(wrt).Encode(result)
}
diff --git a/server/hub.go b/server/hub.go
index 2f89c5c67..e5b6e8749 100644
--- a/server/hub.go
+++ b/server/hub.go
@@ -209,7 +209,6 @@ func (h *Hub) run() {
join.sess.queueOut(ErrLockedReply(join, join.Timestamp))
continue
}
-
// Topic will check access rights and send appropriate {ctrl}
select {
case t.reg <- join:
@@ -301,7 +300,7 @@ func (h *Hub) run() {
// Cluster rehashing. Some previously local topics became remote,
// and the other way round.
// Such topics must be shut down at this node.
- h.topics.Range(func(_, t interface{}) bool {
+ h.topics.Range(func(_, t any) bool {
topic := t.(*Topic)
// Handle two cases:
// 1. Master topic has moved out to another node.
@@ -326,7 +325,7 @@ func (h *Hub) run() {
// start cleanup process
topicsdone := make(chan bool)
topicCount := 0
- h.topics.Range(func(_, topic interface{}) bool {
+ h.topics.Range(func(_, topic any) bool {
topic.(*Topic).exit <- &shutDown{done: topicsdone}
topicCount++
return true
@@ -353,7 +352,7 @@ func (h *Hub) run() {
// * group topics where the given user is the owner.
// 'me' and fnd' are ignored here because they are direcly tied to the user object.
func (h *Hub) topicsStateForUser(uid types.Uid, suspended bool) {
- h.topics.Range(func(name interface{}, t interface{}) bool {
+ h.topics.Range(func(name any, t any) bool {
topic := t.(*Topic)
if topic.cat == types.TopicCatMe || topic.cat == types.TopicCatFnd {
return true
@@ -576,7 +575,7 @@ func (h *Hub) stopTopicsForUser(uid types.Uid, reason int, alldone chan<- bool)
}
count := 0
- h.topics.Range(func(name interface{}, t interface{}) bool {
+ h.topics.Range(func(name any, t any) bool {
topic := t.(*Topic)
if _, isMember := topic.perUser[uid]; (topic.cat != types.TopicCatGrp && isMember) ||
topic.owner == uid {
@@ -809,13 +808,13 @@ func replyOfflineTopicSetSub(sess *Session, msg *ClientComMessage) {
return
}
- update := make(map[string]interface{})
+ update := make(map[string]any)
if msg.Set.Desc != nil && msg.Set.Desc.Private != nil {
- private, ok := msg.Set.Desc.Private.(map[string]interface{})
+ private, ok := msg.Set.Desc.Private.(map[string]any)
if !ok {
- update = map[string]interface{}{"Private": msg.Set.Desc.Private}
+ update = map[string]any{"Private": msg.Set.Desc.Private}
} else if private, changed := mergeInterfaces(sub.Private, private); changed {
- update = map[string]interface{}{"Private": private}
+ update = map[string]any{"Private": private}
}
}
@@ -852,9 +851,9 @@ func replyOfflineTopicSetSub(sess *Session, msg *ClientComMessage) {
logs.Warn.Println("replyOfflineTopicSetSub update:", err)
sess.queueOut(decodeStoreErrorExplicitTs(err, msg.Id, msg.Original, now, msg.Timestamp, nil))
} else {
- var params interface{}
+ var params any
if update["ModeWant"] != nil {
- params = map[string]interface{}{
+ params = map[string]any{
"acs": MsgAccessMode{
Given: sub.ModeGiven.String(),
Want: sub.ModeWant.String(),
diff --git a/server/init_topic.go b/server/init_topic.go
index e3e96f732..f63b540b2 100644
--- a/server/init_topic.go
+++ b/server/init_topic.go
@@ -277,7 +277,6 @@ func initTopicP2P(t *Topic, sreg *ClientComMessage) error {
if stopic != nil && len(subs) == 2 {
// Case 4.
for i := 0; i < 2; i++ {
-
uid := types.ParseUid(subs[i].User)
t.perUser[uid] = perUserData{
// Adapter has already swapped the state, public, defaultAccess, lastSeen values.
@@ -375,7 +374,6 @@ func initTopicP2P(t *Topic, sreg *ClientComMessage) error {
// a. requester is starting a new topic
// b. requester's subscription is missing: deleted or creation failed
if sub1 == nil {
-
// Set user1's ModeGiven from user2's default values
userData.modeGiven = selectAccessMode(auth.Level(sreg.AuthLvl),
users[u2].Access.Anon,
diff --git a/server/main.go b/server/main.go
index a72a6eae4..a00f66298 100644
--- a/server/main.go
+++ b/server/main.go
@@ -30,12 +30,14 @@ import (
"github.com/tinode/chat/server/auth"
_ "github.com/tinode/chat/server/auth/anon"
_ "github.com/tinode/chat/server/auth/basic"
+ _ "github.com/tinode/chat/server/auth/code"
_ "github.com/tinode/chat/server/auth/rest"
_ "github.com/tinode/chat/server/auth/token"
// Database backends
_ "github.com/tinode/chat/server/db/mongodb"
_ "github.com/tinode/chat/server/db/mysql"
+ _ "github.com/tinode/chat/server/db/postgres"
_ "github.com/tinode/chat/server/db/rethinkdb"
"github.com/tinode/chat/server/logs"
@@ -61,9 +63,9 @@ import (
const (
// currentVersion is the current API/protocol version
- currentVersion = "0.21"
+ currentVersion = "0.22"
// minSupportedVersion is the minimum supported API version
- minSupportedVersion = "0.18"
+ minSupportedVersion = "0.19"
// idleSessionTimeout defines duration of being idle before terminating a session.
idleSessionTimeout = time.Second * 55
@@ -176,6 +178,8 @@ var globals struct {
maxSubscriberCount int
// Maximum number of indexable tags.
maxTagCount int
+ // If true, ordinary users cannot delete their accounts.
+ permanentAccounts bool
// Maximum allowed upload size.
maxFileUploadSize int64
@@ -193,6 +197,13 @@ var globals struct {
// ICE servers config (video calling)
iceServers []iceServer
+
+ // Websocket per-message compression negotiation is enabled.
+ wsCompression bool
+
+ // URL of the main endpoint.
+ // TODO: implement file-serving API for gRPC and remove this feature.
+ servingAt string
}
// Credential validator config.
@@ -243,6 +254,9 @@ type configType struct {
ApiPath string `json:"api_path"`
// Cache-Control value for static content.
CacheControl int `json:"cache_control"`
+ // If true, do not attempt to negotiate websocket per message compression (RFC 7692.4).
+ // It should be disabled (set to true) if you are using MSFT IIS as a reverse proxy.
+ WSCompressionDisabled bool `json:"ws_compression_disabled"`
// Address:port to listen for gRPC clients. If blank gRPC support will not be initialized.
// Could be overridden from the command line with --grpc_listen.
GrpcListen string `json:"grpc_listen"`
@@ -262,8 +276,10 @@ type configType struct {
MaxSubscriberCount int `json:"max_subscriber_count"`
// Masked tags: tags immutable on User (mask), mutable on Topic only within the mask.
MaskedTagNamespaces []string `json:"masked_tags"`
- // Maximum number of indexable tags
+ // Maximum number of indexable tags.
MaxTagCount int `json:"max_tag_count"`
+ // If true, ordinary users cannot delete their accounts.
+ PermanentAccounts bool `json:"permanent_accounts"`
// URL path for exposing runtime stats. Disabled if the path is blank.
ExpvarPath string `json:"expvar"`
// URL path for internal server status. Disabled if the path is blank.
@@ -462,11 +478,7 @@ func main() {
for _, req := range vconf.Required {
lvl := auth.ParseAuthLevel(req)
if lvl == auth.LevelNone {
- if req != "" {
- logs.Err.Fatalf("Invalid required AuthLevel '%s' in validator '%s'", req, name)
- }
- // Skip empty string
- continue
+ logs.Err.Fatalf("Invalid required AuthLevel '%s' in validator '%s'", req, name)
}
reqLevels = append(reqLevels, lvl)
if globals.authValidators == nil {
@@ -475,11 +487,6 @@ func main() {
globals.authValidators[lvl] = append(globals.authValidators[lvl], name)
}
- if len(reqLevels) == 0 {
- // Ignore validator with empty levels.
- continue
- }
-
if val := store.Store.GetValidator(name); val == nil {
logs.Err.Fatal("Config provided for an unknown validator '" + name + "'")
} else if err = val.Init(string(vconf.Config)); err != nil {
@@ -541,6 +548,8 @@ func main() {
if globals.maxTagCount <= 0 {
globals.maxTagCount = defaultMaxTagCount
}
+ // If account deletion is disabled.
+ globals.permanentAccounts = config.PermanentAccounts
globals.useXForwardedFor = config.UseXForwardedFor
globals.defaultCountryCode = config.DefaultCountryCode
@@ -548,6 +557,9 @@ func main() {
globals.defaultCountryCode = defaultCountryCode
}
+ // Websocket compression.
+ globals.wsCompression = !config.WSCompressionDisabled
+
if config.Media != nil {
if config.Media.UseHandler == "" {
config.Media = nil
@@ -687,6 +699,16 @@ func main() {
}
logs.Info.Printf("API served from root URL path '%s'", config.ApiPath)
+ // Best guess location of the main endpoint.
+ // TODO: provide fix for the case when the serving is over unix sockets.
+ // TODO: implement serving large files over gRPC, then remove globals.servingAt.
+ globals.servingAt = config.Listen + config.ApiPath
+ if tlsConfig != nil {
+ globals.servingAt = "https://" + globals.servingAt
+ } else {
+ globals.servingAt = "http://" + globals.servingAt
+ }
+
sspath := *serverStatusPath
if sspath == "" || sspath == "-" {
sspath = config.ServerStatusPath
diff --git a/server/media/media.go b/server/media/media.go
index a3f69a965..068926465 100644
--- a/server/media/media.go
+++ b/server/media/media.go
@@ -70,8 +70,9 @@ func matchCORSOrigin(allowed []string, origin string) string {
return "*"
}
+ origin = strings.ToLower(origin)
for _, val := range allowed {
- if val == origin {
+ if strings.ToLower(val) == origin {
return origin
}
}
@@ -87,7 +88,7 @@ func matchCORSMethod(allowMethods []string, method string) bool {
method = strings.ToUpper(method)
for _, mm := range allowMethods {
- if mm == method {
+ if strings.ToUpper(mm) == method {
return true
}
}
@@ -102,17 +103,6 @@ func CORSHandler(req *http.Request, allowedOrigins []string, serve bool) (http.H
return nil, 0
}
- headers := map[string][]string{
- // Always add Vary because of possible intermediate caches.
- "Vary": {"Origin", "Access-Control-Request-Method"},
- }
-
- allowedOrigin := matchCORSOrigin(allowedOrigins, req.Header.Get("Origin"))
- if allowedOrigin == "" {
- // CORS policy does not match the origin.
- return headers, http.StatusOK
- }
-
var allowMethods []string
if serve {
allowMethods = []string{http.MethodGet, http.MethodHead, http.MethodOptions}
@@ -120,16 +110,27 @@ func CORSHandler(req *http.Request, allowedOrigins []string, serve bool) (http.H
allowMethods = []string{http.MethodPost, http.MethodPut, http.MethodHead, http.MethodOptions}
}
+ headers := map[string][]string{
+ // Always add Vary because of possible intermediate caches.
+ "Vary": {"Origin", "Access-Control-Request-Method"},
+ "Access-Control-Allow-Headers": {"*"},
+ "Access-Control-Max-Age": {"86400"},
+ "Access-Control-Allow-Credentials": {"true"},
+ "Access-Control-Allow-Methods": {strings.Join(allowMethods, ", ")},
+ }
+
if !matchCORSMethod(allowMethods, req.Header.Get("Access-Control-Request-Method")) {
// CORS policy does not allow this method.
- return headers, http.StatusOK
+ return headers, http.StatusNoContent
+ }
+
+ allowedOrigin := matchCORSOrigin(allowedOrigins, req.Header.Get("Origin"))
+ if allowedOrigin == "" {
+ // CORS policy does not match the origin.
+ return headers, http.StatusNoContent
}
headers["Access-Control-Allow-Origin"] = []string{allowedOrigin}
- headers["Access-Control-Allow-Headers"] = []string{"*"}
- headers["Access-Control-Allow-Methods"] = []string{strings.Join(allowMethods, ",")}
- headers["Access-Control-Max-Age"] = []string{"86400"}
- headers["Access-Control-Allow-Credentials"] = []string{"true"}
- return headers, http.StatusOK
+ return headers, http.StatusNoContent
}
diff --git a/server/media/s3/s3.go b/server/media/s3/s3.go
index ae61962f0..b99377445 100644
--- a/server/media/s3/s3.go
+++ b/server/media/s3/s3.go
@@ -7,6 +7,7 @@ import (
"io"
"mime"
"net/http"
+ "strconv"
"sync/atomic"
"time"
@@ -172,10 +173,15 @@ func (ah *awshandler) Headers(req *http.Request, serve bool) (http.Header, int,
var awsReq *request.Request
if req.Method == http.MethodGet {
+ var contentDisposition *string
+ if isAttachment, _ := strconv.ParseBool(req.URL.Query().Get("asatt")); isAttachment {
+ contentDisposition = aws.String("attachment")
+ }
awsReq, _ = ah.svc.GetObjectRequest(&s3.GetObjectInput{
- Bucket: aws.String(ah.conf.BucketName),
- Key: aws.String(fid.String32()),
- ResponseContentType: aws.String(fd.MimeType),
+ Bucket: aws.String(ah.conf.BucketName),
+ Key: aws.String(fid.String32()),
+ ResponseContentType: aws.String(fd.MimeType),
+ ResponseContentDisposition: contentDisposition,
})
} else if req.Method == http.MethodHead {
awsReq, _ = ah.svc.HeadObjectRequest(&s3.HeadObjectInput{
diff --git a/server/pbconverter.go b/server/pbconverter.go
index 667ed1234..7743c5a25 100644
--- a/server/pbconverter.go
+++ b/server/pbconverter.go
@@ -14,7 +14,7 @@ import (
func pbServCtrlSerialize(ctrl *MsgServerCtrl) *pbx.ServerMsg_Ctrl {
var params map[string][]byte
if ctrl.Params != nil {
- if in, ok := ctrl.Params.(map[string]interface{}); ok {
+ if in, ok := ctrl.Params.(map[string]any); ok {
params = interfaceMapToByteMap(in)
}
}
@@ -247,18 +247,32 @@ func pbCliSerialize(msg *ClientComMessage) *pbx.ClientMsg {
},
}
case msg.Acc != nil:
+ var authLevel pbx.AuthLevel
+ switch msg.Acc.AuthLevel {
+ case "NONE", "none", "":
+ authLevel = pbx.AuthLevel_NONE
+ case "ANON", "anon":
+ authLevel = pbx.AuthLevel_ANON
+ case "AUTH", "auth":
+ authLevel = pbx.AuthLevel_AUTH
+ case "ROOT", "root":
+ // No support for ROOT here.
+ authLevel = pbx.AuthLevel_NONE
+ }
pkt.Message = &pbx.ClientMsg_Acc{
Acc: &pbx.ClientAcc{
- Id: msg.Acc.Id,
- UserId: msg.Acc.User,
- State: msg.Acc.State,
- Token: msg.Acc.Token,
- Scheme: msg.Acc.Scheme,
- Secret: msg.Acc.Secret,
- Login: msg.Acc.Login,
- Tags: msg.Acc.Tags,
- Cred: pbClientCredsSerialize(msg.Acc.Cred),
- Desc: pbSetDescSerialize(msg.Acc.Desc),
+ Id: msg.Acc.Id,
+ UserId: msg.Acc.User,
+ State: msg.Acc.State,
+ TmpScheme: msg.Acc.TmpScheme,
+ TmpSecret: msg.Acc.TmpSecret,
+ AuthLevel: authLevel,
+ Scheme: msg.Acc.Scheme,
+ Secret: msg.Acc.Secret,
+ Login: msg.Acc.Login,
+ Tags: msg.Acc.Tags,
+ Cred: pbClientCredsSerialize(msg.Acc.Cred),
+ Desc: pbSetDescSerialize(msg.Acc.Desc),
},
}
case msg.Login != nil:
@@ -380,15 +394,18 @@ func pbCliDeserialize(pkt *pbx.ClientMsg) *ClientComMessage {
}
} else if acc := pkt.GetAcc(); acc != nil {
msg.Acc = &MsgClientAcc{
- Id: acc.GetId(),
- User: acc.GetUserId(),
- State: acc.GetState(),
- Scheme: acc.GetScheme(),
- Secret: acc.GetSecret(),
- Login: acc.GetLogin(),
- Tags: acc.GetTags(),
- Desc: pbSetDescDeserialize(acc.GetDesc()),
- Cred: pbClientCredsDeserialize(acc.GetCred()),
+ Id: acc.GetId(),
+ User: acc.GetUserId(),
+ State: acc.GetState(),
+ TmpScheme: acc.GetTmpScheme(),
+ TmpSecret: acc.GetTmpSecret(),
+ AuthLevel: acc.GetAuthLevel().String(),
+ Scheme: acc.GetScheme(),
+ Secret: acc.GetSecret(),
+ Login: acc.GetLogin(),
+ Tags: acc.GetTags(),
+ Desc: pbSetDescDeserialize(acc.GetDesc()),
+ Cred: pbClientCredsDeserialize(acc.GetCred()),
}
} else if login := pkt.GetLogin(); login != nil {
msg.Login = &MsgClientLogin{
@@ -479,7 +496,7 @@ func pbCliDeserialize(pkt *pbx.ClientMsg) *ClientComMessage {
return &msg
}
-func interfaceMapToByteMap(in map[string]interface{}) map[string][]byte {
+func interfaceMapToByteMap(in map[string]any) map[string][]byte {
out := make(map[string][]byte, len(in))
for key, val := range in {
if val != nil {
@@ -489,8 +506,8 @@ func interfaceMapToByteMap(in map[string]interface{}) map[string][]byte {
return out
}
-func byteMapToInterfaceMap(in map[string][]byte) map[string]interface{} {
- out := make(map[string]interface{}, len(in))
+func byteMapToInterfaceMap(in map[string][]byte) map[string]any {
+ out := make(map[string]any, len(in))
for key, raw := range in {
if val := bytesToInterface(raw); val != nil {
out[key] = val
@@ -499,7 +516,7 @@ func byteMapToInterfaceMap(in map[string][]byte) map[string]interface{} {
return out
}
-func interfaceToBytes(in interface{}) []byte {
+func interfaceToBytes(in any) []byte {
if in != nil {
out, _ := json.Marshal(in)
return out
@@ -507,8 +524,8 @@ func interfaceToBytes(in interface{}) []byte {
return nil
}
-func bytesToInterface(in []byte) interface{} {
- var out interface{}
+func bytesToInterface(in []byte) any {
+ var out any
if len(in) > 0 {
err := json.Unmarshal(in, &out)
if err != nil {
@@ -861,6 +878,7 @@ func pbTopicDescSerialize(desc *MsgTopicDesc) *pbx.TopicDesc {
UpdatedAt: timeToInt64(desc.UpdatedAt),
TouchedAt: timeToInt64(desc.TouchedAt),
State: desc.State,
+ Online: desc.Online,
IsChan: desc.IsChan,
Defacs: pbDefaultAcsSerialize(desc.DefaultAcs),
Acs: pbAccessModeSerialize(desc.Acs),
@@ -888,6 +906,7 @@ func pbTopicDescDeserialize(desc *pbx.TopicDesc) *MsgTopicDesc {
UpdatedAt: int64ToTime(desc.GetUpdatedAt()),
TouchedAt: int64ToTime(desc.GetTouchedAt()),
State: desc.GetState(),
+ Online: desc.GetOnline(),
IsChan: desc.GetIsChan(),
DefaultAcs: pbDefaultAcsDeserialize(desc.GetDefacs()),
Acs: pbAccessModeDeserialize(desc.GetAcs()),
diff --git a/server/plugins.go b/server/plugins.go
index 229e6c8f3..7d269bbc2 100644
--- a/server/plugins.go
+++ b/server/plugins.go
@@ -2,6 +2,7 @@
package main
import (
+ "context"
"encoding/json"
"errors"
"strings"
@@ -10,8 +11,8 @@ import (
"github.com/tinode/chat/pbx"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store/types"
- "golang.org/x/net/context"
"google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
)
const (
@@ -246,7 +247,7 @@ type Plugin struct {
func pluginsInit(configString json.RawMessage) {
// Check if any plugins are defined
- if configString == nil || len(configString) == 0 {
+ if len(configString) == 0 {
return
}
@@ -286,7 +287,7 @@ func pluginsInit(configString json.RawMessage) {
}
if globals.plugins[count].filterTopic, err =
ParsePluginFilter(conf.Filters.Topic, plgFilterByTopicType|plgFilterByAction); err != nil {
- logs.Err.Fatal("plugins: bad FireHose filter", err)
+ logs.Err.Fatal("plugins: bad Topic filter", err)
}
if globals.plugins[count].filterSubscription, err =
ParsePluginFilter(conf.Filters.Subscription, plgFilterByTopicType|plgFilterByAction); err != nil {
@@ -306,7 +307,7 @@ func pluginsInit(configString json.RawMessage) {
globals.plugins[count].addr = parts[1]
}
- globals.plugins[count].conn, err = grpc.Dial(globals.plugins[count].addr, grpc.WithInsecure())
+ globals.plugins[count].conn, err = grpc.Dial(globals.plugins[count].addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
logs.Err.Fatalf("plugins: connection failure %v", err)
}
diff --git a/server/pres.go b/server/pres.go
index f91ff08f1..19a6bc011 100644
--- a/server/pres.go
+++ b/server/pres.go
@@ -287,7 +287,7 @@ func (t *Topic) presUsersOfInterest(what, ua string) {
}
// if notify available, send push to user friends
- if uaPayload["isPrivate"] == false && uaPayload["type"] == "watch_party" && wpNotified != true {
+ if uaPayload["isPrivate"] == false && uaPayload["type"] == "watch_party" && !wpNotified {
organizationId := ""
// search owner session to resolve organization id
for session := range t.sessions {
diff --git a/server/push.go b/server/push.go
index 25d7f53d4..bbb1959df 100644
--- a/server/push.go
+++ b/server/push.go
@@ -25,7 +25,7 @@ func (t *Topic) channelSubUnsub(uid types.Uid, sub bool) {
}
// Prepares a payload to be delivered to a mobile device as a push notification in response to a {data} message.
-func (t *Topic) pushForData(fromUid types.Uid, data *MsgServerData, organizationId string) *push.Receipt {
+func (t *Topic) pushForData(fromUid types.Uid, data *MsgServerData, msgMarkedAsReadBySender bool, organizationId string) *push.Receipt {
// Passing `Topic` as `t.name` for group topics and P2P topics. The p2p topic name is later rewritten for
// each recipient then the payload is created: p2p recipient sees the topic as the ID of the other user.
@@ -78,6 +78,9 @@ func (t *Topic) pushForData(fromUid types.Uid, data *MsgServerData, organization
// Number of attached sessions the data message will be delivered to.
// Push notifications sent to users with non-zero online sessions will be marked silent.
Delivered: online,
+ // Unread counts are incremented for all recipients,
+ // and for sender only if the message wasnt't marked 'read' by the sender
+ ShouldIncrementUnreadCountInCache: uid != fromUid || !msgMarkedAsReadBySender,
}
}
}
diff --git a/server/push/push.go b/server/push/push.go
index 8ebafa4e5..220319e0d 100644
--- a/server/push/push.go
+++ b/server/push/push.go
@@ -30,6 +30,8 @@ type Recipient struct {
Devices []string `json:"devices,omitempty"`
// Unread count to include in the push
Unread int `json:"unread"`
+ // Indicates whether unread counter in the cache should be incremented before sending the push.
+ ShouldIncrementUnreadCountInCache bool `json:"-"`
}
// Receipt is the push payload with a list of recipients.
diff --git a/server/push/tnpg/README.md b/server/push/tnpg/README.md
index bbd5f9823..191eda33f 100644
--- a/server/push/tnpg/README.md
+++ b/server/push/tnpg/README.md
@@ -12,7 +12,7 @@ TNPG solves this problem by allowing you to send push notifications on behalf of
### Obtain TNPG token
1. Register at https://console.tinode.co and create an organization.
-2. Get the TPNG token from the _Self hosting_ section by following the instructions there.
+2. Get the TPNG token from the _Self hosting_ → _Push Gateway_ section by following the instructions there.
### Configure the server
Update the server config [`tinode.conf`](../../tinode.conf#L413), section `"push"` -> `"name": "tnpg"`:
diff --git a/server/session.go b/server/session.go
index f150d16b7..91a75a4e6 100644
--- a/server/session.go
+++ b/server/session.go
@@ -135,11 +135,11 @@ type Session struct {
// Outbound mesages, buffered.
// The content must be serialized in format suitable for the session.
- send chan interface{}
+ send chan any
// Channel for shutting down the session, buffer 1.
// Content in the same format as for 'send'
- stop chan interface{}
+ stop chan any
// detach - channel for detaching session from topic, buffered.
// Content is topic name to detach from.
@@ -394,7 +394,7 @@ func (s *Session) detachSession(fromTopic string) {
}
}
-func (s *Session) stopSession(data interface{}) {
+func (s *Session) stopSession(data any) {
s.stop <- data
s.maybeScheduleClusterWriteLoop()
}
@@ -589,6 +589,7 @@ func (s *Session) dispatch(msg *ClientComMessage) {
msg.Id = msg.Acc.Id
case msg.Note != nil:
+ // If user is not authenticated or version not set the {note} is silently ignored.
handler = s.note
msg.Original = msg.Note.Topic
uaRefresh = true
@@ -697,7 +698,7 @@ func (s *Session) publish(msg *ClientComMessage) {
// Add "sender" header if the message is sent on behalf of another user.
if msg.AsUser != s.uid.UserId() {
if msg.Pub.Head == nil {
- msg.Pub.Head = make(map[string]interface{})
+ msg.Pub.Head = make(map[string]any)
}
msg.Pub.Head["sender"] = s.uid.UserId()
} else if msg.Pub.Head != nil {
@@ -735,7 +736,7 @@ func (s *Session) publish(msg *ClientComMessage) {
// Client metadata
func (s *Session) hello(msg *ClientComMessage) {
- var params map[string]interface{}
+ var params map[string]any
var deviceIDUpdate bool
if s.ver == 0 {
@@ -753,7 +754,7 @@ func (s *Session) hello(msg *ClientComMessage) {
return
}
- params = map[string]interface{}{
+ params = map[string]any{
"ver": currentVersion,
"build": store.Store.GetAdapterName() + ":" + buildstamp,
"maxMessageSize": globals.maxMessageSize,
@@ -771,6 +772,18 @@ func (s *Session) hello(msg *ClientComMessage) {
params["callTimeout"] = globals.callEstablishmentTimeout
}
+ if s.proto == GRPC {
+ // gRPC client may need server address to be able to fetch large files over http(s).
+ // TODO: add support for fetching files over gRPC, then remove this parameter.
+ params["servingAt"] = globals.servingAt
+ // Report cluster size.
+ if globals.cluster != nil {
+ params["clusterSize"] = len(globals.cluster.nodes) + 1
+ } else {
+ params["clusterSize"] = 1
+ }
+ }
+
// Set ua & platform in the beginning of the session.
// Don't change them later.
s.userAgent = msg.Hi.UserAgent
@@ -827,11 +840,14 @@ func (s *Session) hello(msg *ClientComMessage) {
s.deviceID = msg.Hi.DeviceID
s.lang = msg.Hi.Lang
// Try to deduce the country from the locale.
- if tag, err := language.Parse(s.lang); err == nil {
+ // Tag may be well-defined even if err != nil. For example, for 'zh_CN_#Hans'
+ // the tag is 'zh-CN' exact but the err is 'tag is not well-formed'.
+ if tag, _ := language.Parse(s.lang); tag != language.Und {
if region, conf := tag.Region(); region.IsCountry() && conf >= language.High {
s.countryCode = region.String()
}
}
+
if s.countryCode == "" {
if len(s.lang) > 2 {
// Logging strings longer than 2 b/c language.Parse(XX) always succeeds
@@ -863,26 +879,34 @@ func (s *Session) hello(msg *ClientComMessage) {
// Account creation
func (s *Session) acc(msg *ClientComMessage) {
- // If token is provided, get the user ID from it.
+ newAcc := strings.HasPrefix(msg.Acc.User, "new")
+
+ // If temporary auth parameters are provided, get the user ID from them.
var rec *auth.Rec
- if msg.Acc.Token != nil {
+ if !newAcc && msg.Acc.TmpScheme != "" {
if !s.uid.IsZero() {
s.queueOut(ErrAlreadyAuthenticated(msg.Acc.Id, "", msg.Timestamp))
logs.Warn.Println("s.acc: got token while already authenticated", s.sid)
return
}
+ authHdl := store.Store.GetLogicalAuthHandler(msg.Acc.TmpScheme)
+ if authHdl == nil {
+ logs.Warn.Println("s.acc: unknown authentication scheme", msg.Acc.TmpScheme, s.sid)
+ s.queueOut(ErrAuthUnknownScheme(msg.Id, "", msg.Timestamp))
+ }
+
var err error
- rec, _, err = store.Store.GetLogicalAuthHandler("token").Authenticate(msg.Acc.Token, s.remoteAddr, msg.Acc.SdkKey)
+ rec, _, err = authHdl.Authenticate(msg.Acc.TmpSecret, s.remoteAddr, msg.Acc.SdkKey)
if err != nil {
- s.queueOut(decodeStoreError(err, msg.Acc.Id, "", msg.Timestamp,
- map[string]interface{}{"what": "auth"}))
- logs.Warn.Println("s.acc: invalid token", err, s.sid)
+ s.queueOut(decodeStoreError(err, msg.Acc.Id, msg.Timestamp,
+ map[string]any{"what": "auth"}))
+ logs.Warn.Println("s.acc: invalid temp auth", err, s.sid)
return
}
}
- if strings.HasPrefix(msg.Acc.User, "new") {
+ if newAcc {
// New account
replyCreateUser(s, msg, rec)
} else {
@@ -897,7 +921,7 @@ func (s *Session) login(msg *ClientComMessage) {
if msg.Login.Scheme == "reset" {
if err := s.authSecretReset(msg.Login.Secret); err != nil {
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp, nil))
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
} else {
s.queueOut(InfoAuthReset(msg.Id, msg.Timestamp))
}
@@ -920,7 +944,7 @@ func (s *Session) login(msg *ClientComMessage) {
rec, challenge, err := handler.Authenticate(msg.Login.Secret, s.remoteAddr, msg.Login.SdkKey)
if err != nil {
- resp := decodeStoreError(err, msg.Id, "", msg.Timestamp, nil)
+ resp := decodeStoreError(err, msg.Id, msg.Timestamp, nil)
if resp.Ctrl.Code >= 500 {
// Log internal errors
logs.Warn.Println("s.login: internal", err, s.sid)
@@ -939,7 +963,7 @@ func (s *Session) login(msg *ClientComMessage) {
if err != nil {
logs.Warn.Println("s.login: user state check failed", rec.Uid, err, s.sid)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp, nil))
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
@@ -960,7 +984,7 @@ func (s *Session) login(msg *ClientComMessage) {
}
if err != nil {
logs.Warn.Println("s.login: failed to validate credentials:", err, s.sid)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp, nil))
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
} else {
s.queueOut(s.onLogin(msg.Id, msg.Timestamp, rec, missing))
}
@@ -992,7 +1016,8 @@ func (s *Session) authSecretReset(params []byte) error {
return err
}
if uid.IsZero() {
- return types.ErrNotFound
+ // Prevent discovery of existing contacts: report "no error" if contact is not found.
+ return nil
}
resetParams, err := hdl.GetResetParams(uid)
@@ -1000,27 +1025,27 @@ func (s *Session) authSecretReset(params []byte) error {
return err
}
- token, _, err := store.Store.GetLogicalAuthHandler("token").GenSecret(&auth.Rec{
- Uid: uid,
- AuthLevel: auth.LevelAuth,
- Lifetime: auth.Duration(time.Hour * 24),
- Features: auth.FeatureNoLogin,
+ code, _, err := store.Store.GetLogicalAuthHandler("code").GenSecret(&auth.Rec{
+ Uid: uid,
+ AuthLevel: auth.LevelAuth,
+ Features: auth.FeatureNoLogin,
+ Credential: credMethod + ":" + credValue,
})
if err != nil {
return err
}
- return validator.ResetSecret(credValue, authScheme, s.lang, token, resetParams)
+ return validator.ResetSecret(credValue, authScheme, s.lang, code, resetParams)
}
// onLogin performs steps after successful authentication.
func (s *Session) onLogin(msgID string, timestamp time.Time, rec *auth.Rec, missing []string) *ServerComMessage {
var reply *ServerComMessage
- var params map[string]interface{}
+ var params map[string]any
features := rec.Features
- params = map[string]interface{}{
+ params = map[string]any{
"user": rec.Uid.UserId(),
"authlvl": rec.AuthLevel.String(),
}
@@ -1328,7 +1353,7 @@ func (s *Session) expandTopicName(msg *ClientComMessage) (string, *ServerComMess
return routeTo, nil
}
-func (s *Session) serializeAndUpdateStats(msg *ServerComMessage) interface{} {
+func (s *Session) serializeAndUpdateStats(msg *ServerComMessage) any {
dataSize, data := s.serialize(msg)
if dataSize >= 0 {
statsAddHistSample("OutgoingMessageSize", float64(dataSize))
@@ -1336,7 +1361,7 @@ func (s *Session) serializeAndUpdateStats(msg *ServerComMessage) interface{} {
return data
}
-func (s *Session) serialize(msg *ServerComMessage) (int, interface{}) {
+func (s *Session) serialize(msg *ServerComMessage) (int, any) {
if s.proto == GRPC {
msg := pbServSerialize(msg)
// TODO: calculate and return the size of `msg`.
diff --git a/server/session_test.go b/server/session_test.go
index b6d58e87c..dcd3dab47 100644
--- a/server/session_test.go
+++ b/server/session_test.go
@@ -16,7 +16,7 @@ import (
func TestDispatchHello(t *testing.T) {
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: types.Uid(1),
authLvl: auth.LevelAuth,
}
@@ -87,7 +87,7 @@ func verifyResponseCodes(r *responses, codes []int, t *testing.T) {
func TestDispatchInvalidVersion(t *testing.T) {
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: types.Uid(1),
authLvl: auth.LevelAuth,
}
@@ -110,7 +110,7 @@ func TestDispatchInvalidVersion(t *testing.T) {
func TestDispatchUnsupportedVersion(t *testing.T) {
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: types.Uid(1),
authLvl: auth.LevelAuth,
}
@@ -159,7 +159,7 @@ func TestDispatchLogin(t *testing.T) {
aa.EXPECT().GenSecret(authRec).Return([]byte(token), expires, nil)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
authLvl: auth.LevelAuth,
ver: 16,
}
@@ -197,7 +197,7 @@ func TestDispatchLogin(t *testing.T) {
if resp.Ctrl.Params == nil {
t.Error("Response is expected to contain params dict.")
}
- p := resp.Ctrl.Params.(map[string]interface{})
+ p := resp.Ctrl.Params.(map[string]any)
if authToken := string(p["token"].([]byte)); authToken != token {
t.Errorf("Auth token: expected '%s', found '%s'.", token, authToken)
}
@@ -212,7 +212,7 @@ func TestDispatchLogin(t *testing.T) {
func TestDispatchSubscribe(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -267,7 +267,7 @@ func TestDispatchSubscribe(t *testing.T) {
func TestDispatchAlreadySubscribed(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -301,7 +301,7 @@ func TestDispatchAlreadySubscribed(t *testing.T) {
func TestDispatchSubscribeJoinChannelFull(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -342,7 +342,7 @@ func TestDispatchSubscribeJoinChannelFull(t *testing.T) {
func TestDispatchLeave(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -396,7 +396,7 @@ func TestDispatchLeave(t *testing.T) {
func TestDispatchLeaveUnsubMe(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -429,7 +429,7 @@ func TestDispatchLeaveUnsubMe(t *testing.T) {
func TestDispatchLeaveUnknownTopic(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -461,7 +461,7 @@ func TestDispatchLeaveUnknownTopic(t *testing.T) {
func TestDispatchLeaveUnsubFromUnknownTopic(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -494,7 +494,7 @@ func TestDispatchLeaveUnsubFromUnknownTopic(t *testing.T) {
func TestDispatchPublish(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -550,7 +550,7 @@ func TestDispatchPublish(t *testing.T) {
func TestDispatchPublishBroadcastChannelFull(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -591,7 +591,7 @@ func TestDispatchPublishBroadcastChannelFull(t *testing.T) {
func TestDispatchPublishMissingSubcription(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -626,7 +626,7 @@ func TestDispatchPublishMissingSubcription(t *testing.T) {
func TestDispatchGet(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -677,7 +677,7 @@ func TestDispatchGet(t *testing.T) {
func TestDispatchGetMalformedWhat(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -710,7 +710,7 @@ func TestDispatchGetMalformedWhat(t *testing.T) {
func TestDispatchGetMetaChannelFull(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -751,7 +751,7 @@ func TestDispatchGetMetaChannelFull(t *testing.T) {
func TestDispatchSet(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -809,7 +809,7 @@ func TestDispatchSet(t *testing.T) {
func TestDispatchSetMalformedWhat(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -841,7 +841,7 @@ func TestDispatchSetMalformedWhat(t *testing.T) {
func TestDispatchSetMetaChannelFull(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -886,7 +886,7 @@ func TestDispatchSetMetaChannelFull(t *testing.T) {
func TestDispatchDelMsg(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -937,7 +937,7 @@ func TestDispatchDelMsg(t *testing.T) {
func TestDispatchDelMalformedWhat(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -968,7 +968,7 @@ func TestDispatchDelMalformedWhat(t *testing.T) {
func TestDispatchDelMetaChanFull(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -1009,7 +1009,7 @@ func TestDispatchDelMetaChanFull(t *testing.T) {
func TestDispatchDelUnsubscribedSession(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -1043,7 +1043,7 @@ func TestDispatchDelUnsubscribedSession(t *testing.T) {
func TestDispatchNote(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -1101,7 +1101,7 @@ func TestDispatchNote(t *testing.T) {
func TestDispatchNoteBroadcastChanFull(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -1140,7 +1140,7 @@ func TestDispatchNoteBroadcastChanFull(t *testing.T) {
func TestDispatchNoteOnNonSubscribedTopic(t *testing.T) {
uid := types.Uid(1)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
uid: uid,
authLvl: auth.LevelAuth,
inflightReqs: &sync.WaitGroup{},
@@ -1197,7 +1197,7 @@ func TestDispatchAccNew(t *testing.T) {
// This login is available.
aa.EXPECT().IsUnique([]byte(secret), remoteAddr).Return(true, nil)
uu.EXPECT().Create(gomock.Any(), gomock.Any()).DoAndReturn(
- func(user *types.User, private interface{}) (*types.User, error) {
+ func(user *types.User, private any) (*types.User, error) {
user.SetUid(uid)
return user, nil
})
@@ -1210,7 +1210,7 @@ func TestDispatchAccNew(t *testing.T) {
uu.EXPECT().UpdateTags(uid, tags, nil, nil).Return(tags, nil)
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
authLvl: auth.LevelAuth,
ver: 16,
remoteAddr: remoteAddr,
@@ -1253,7 +1253,7 @@ func TestDispatchAccNew(t *testing.T) {
if resp.Ctrl.Params == nil {
t.Error("Response is expected to contain params dict.")
}
- p := resp.Ctrl.Params.(map[string]interface{})
+ p := resp.Ctrl.Params.(map[string]any)
if respUid := string(p["user"].(string)); respUid != uid.UserId() {
t.Errorf("Response uid: expected '%s', found '%s'.", uid.UserId(), respUid)
}
@@ -1271,7 +1271,7 @@ func TestDispatchAccNew(t *testing.T) {
func TestDispatchNoMessage(t *testing.T) {
remoteAddr := "192.168.0.1"
s := &Session{
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
authLvl: auth.LevelAuth,
ver: 16,
remoteAddr: remoteAddr,
diff --git a/server/sessionstore.go b/server/sessionstore.go
index aee23a14e..c05291857 100644
--- a/server/sessionstore.go
+++ b/server/sessionstore.go
@@ -36,7 +36,7 @@ type SessionStore struct {
}
// NewSession creates a new session and saves it to the session store.
-func (ss *SessionStore) NewSession(conn interface{}, sid string) (*Session, int) {
+func (ss *SessionStore) NewSession(conn any, sid string) (*Session, int) {
var s Session
if sid == "" {
@@ -69,9 +69,9 @@ func (ss *SessionStore) NewSession(conn interface{}, sid string) (*Session, int)
}
s.subs = make(map[string]*Subscription)
- s.send = make(chan interface{}, sendQueueLimit+32) // buffered
- s.stop = make(chan interface{}, 1) // Buffered by 1 just to make it non-blocking
- s.detach = make(chan string, 64) // buffered
+ s.send = make(chan any, sendQueueLimit+32) // buffered
+ s.stop = make(chan any, 1) // Buffered by 1 just to make it non-blocking
+ s.detach = make(chan string, 64) // buffered
s.bkgTimer = time.NewTimer(time.Hour)
s.bkgTimer.Stop()
diff --git a/server/stats.go b/server/stats.go
index 222e5872f..aa59c7fd8 100644
--- a/server/stats.go
+++ b/server/stats.go
@@ -48,7 +48,7 @@ type varUpdate struct {
// Name of the variable to update
varname string
// Value to publish (int, float, etc.)
- value interface{}
+ value any
// Treat the count as an increment as opposite to the final value.
inc bool
}
@@ -63,10 +63,10 @@ func statsInit(mux *http.ServeMux, path string) {
globals.statsUpdate = make(chan *varUpdate, 1024)
start := time.Now()
- expvar.Publish("Uptime", expvar.Func(func() interface{} {
+ expvar.Publish("Uptime", expvar.Func(func() any {
return time.Since(start).Seconds()
}))
- expvar.Publish("NumGoroutines", expvar.Func(func() interface{} {
+ expvar.Publish("NumGoroutines", expvar.Func(func() any {
return runtime.NumGoroutine()
}))
diff --git a/server/store/mock_store/mock_store.go b/server/store/mock_store/mock_store.go
index 8f2f53bbc..14945d18d 100644
--- a/server/store/mock_store/mock_store.go
+++ b/server/store/mock_store/mock_store.go
@@ -415,18 +415,18 @@ func (mr *MockUsersPersistenceInterfaceMockRecorder) FailCred(id, method interfa
}
// FindSubs mocks base method.
-func (m *MockUsersPersistenceInterface) FindSubs(id types.Uid, required [][]string, optional []string) ([]types.Subscription, error) {
+func (m *MockUsersPersistenceInterface) FindSubs(id types.Uid, required [][]string, optional []string, activeOnly bool) ([]types.Subscription, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "FindSubs", id, required, optional)
+ ret := m.ctrl.Call(m, "FindSubs", id, required, optional, activeOnly)
ret0, _ := ret[0].([]types.Subscription)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindSubs indicates an expected call of FindSubs.
-func (mr *MockUsersPersistenceInterfaceMockRecorder) FindSubs(id, required, optional interface{}) *gomock.Call {
+func (mr *MockUsersPersistenceInterfaceMockRecorder) FindSubs(id, required, optional, activeOnly interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindSubs", reflect.TypeOf((*MockUsersPersistenceInterface)(nil).FindSubs), id, required, optional)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindSubs", reflect.TypeOf((*MockUsersPersistenceInterface)(nil).FindSubs), id, required, optional, activeOnly)
}
// Get mocks base method.
@@ -1060,11 +1060,12 @@ func (mr *MockMessagesPersistenceInterfaceMockRecorder) GetDeleted(topic, forUse
}
// Save mocks base method.
-func (m *MockMessagesPersistenceInterface) Save(msg *types.Message, attachmentURLs []string, readBySender bool) error {
+func (m *MockMessagesPersistenceInterface) Save(msg *types.Message, attachmentURLs []string, readBySender bool) (error, bool) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Save", msg, attachmentURLs, readBySender)
ret0, _ := ret[0].(error)
- return ret0
+ ret1, _ := ret[1].(bool)
+ return ret0, ret1
}
// Save indicates an expected call of Save.
@@ -1238,3 +1239,83 @@ func (mr *MockFilePersistenceInterfaceMockRecorder) StartUpload(fd interface{})
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartUpload", reflect.TypeOf((*MockFilePersistenceInterface)(nil).StartUpload), fd)
}
+
+// MockPersistentCacheInterface is a mock of PersistentCacheInterface interface.
+type MockPersistentCacheInterface struct {
+ ctrl *gomock.Controller
+ recorder *MockPersistentCacheInterfaceMockRecorder
+}
+
+// MockPersistentCacheInterfaceMockRecorder is the mock recorder for MockPersistentCacheInterface.
+type MockPersistentCacheInterfaceMockRecorder struct {
+ mock *MockPersistentCacheInterface
+}
+
+// NewMockPersistentCacheInterface creates a new mock instance.
+func NewMockPersistentCacheInterface(ctrl *gomock.Controller) *MockPersistentCacheInterface {
+ mock := &MockPersistentCacheInterface{ctrl: ctrl}
+ mock.recorder = &MockPersistentCacheInterfaceMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockPersistentCacheInterface) EXPECT() *MockPersistentCacheInterfaceMockRecorder {
+ return m.recorder
+}
+
+// Delete mocks base method.
+func (m *MockPersistentCacheInterface) Delete(key string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Delete", key)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Delete indicates an expected call of Delete.
+func (mr *MockPersistentCacheInterfaceMockRecorder) Delete(key interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPersistentCacheInterface)(nil).Delete), key)
+}
+
+// Expire mocks base method.
+func (m *MockPersistentCacheInterface) Expire(keyPrefix string, olderThan time.Time) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Expire", keyPrefix, olderThan)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Expire indicates an expected call of Expire.
+func (mr *MockPersistentCacheInterfaceMockRecorder) Expire(keyPrefix, olderThan interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Expire", reflect.TypeOf((*MockPersistentCacheInterface)(nil).Expire), keyPrefix, olderThan)
+}
+
+// Get mocks base method.
+func (m *MockPersistentCacheInterface) Get(key string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Get", key)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockPersistentCacheInterfaceMockRecorder) Get(key interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPersistentCacheInterface)(nil).Get), key)
+}
+
+// Upsert mocks base method.
+func (m *MockPersistentCacheInterface) Upsert(key, value string, failOnDuplicate bool) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Upsert", key, value, failOnDuplicate)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Upsert indicates an expected call of Upsert.
+func (mr *MockPersistentCacheInterfaceMockRecorder) Upsert(key, value, failOnDuplicate interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockPersistentCacheInterface)(nil).Upsert), key, value, failOnDuplicate)
+}
diff --git a/server/store/store.go b/server/store/store.go
index 0084fa90e..7cb25a06d 100644
--- a/server/store/store.go
+++ b/server/store/store.go
@@ -266,7 +266,7 @@ type UsersPersistenceInterface interface {
UpdateTags(uid types.Uid, add, remove, reset []string) ([]string, error)
UpdateState(uid types.Uid, state types.ObjState) error
GetSubs(id types.Uid) ([]types.Subscription, error)
- FindSubs(id types.Uid, required [][]string, optional []string) ([]types.Subscription, error)
+ FindSubs(id types.Uid, required [][]string, optional []string, activeOnly bool) ([]types.Subscription, error)
GetTopics(id types.Uid, opts *types.QueryOpt) ([]types.Subscription, error)
GetTopicsAny(id types.Uid, opts *types.QueryOpt) ([]types.Subscription, error)
GetOwnTopics(id types.Uid) ([]string, error)
@@ -426,12 +426,12 @@ func (usersMapper) GetSubs(id types.Uid) ([]types.Subscription, error) {
// `required` specifies an AND of ORs for required terms:
// at least one element of every sublist in `required` must be present in the object's tags list.
// `optional` specifies a list of optional terms.
-func (usersMapper) FindSubs(id types.Uid, required [][]string, optional []string) ([]types.Subscription, error) {
- usubs, err := adp.FindUsers(id, required, optional)
+func (usersMapper) FindSubs(id types.Uid, required [][]string, optional []string, activeOnly bool) ([]types.Subscription, error) {
+ usubs, err := adp.FindUsers(id, required, optional, activeOnly)
if err != nil {
return nil, err
}
- tsubs, err := adp.FindTopics(required, optional)
+ tsubs, err := adp.FindTopics(required, optional, activeOnly)
if err != nil {
return nil, err
}
@@ -653,7 +653,7 @@ func (subsMapper) Delete(topic string, user types.Uid) error {
// MessagesPersistenceInterface is an interface which defines methods for persistent storage of messages.
type MessagesPersistenceInterface interface {
- Save(msg *types.Message, attachmentURLs []string, readBySender bool) error
+ Save(msg *types.Message, attachmentURLs []string, readBySender bool) (error, bool)
DeleteList(topic string, delID int, forUser types.Uid, ranges []types.Range) error
GetAll(topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Message, error)
GetDeleted(topic string, forUser types.Uid, opt *types.QueryOpt) ([]types.Range, int, error)
@@ -666,30 +666,35 @@ type messagesMapper struct{}
var Messages MessagesPersistenceInterface
// Save message
-func (messagesMapper) Save(msg *types.Message, attachmentURLs []string, readBySender bool) error {
+func (messagesMapper) Save(msg *types.Message, attachmentURLs []string, readBySender bool) (error, bool) {
msg.InitTimes()
msg.SetUid(Store.GetUid())
// Increment topic's or user's SeqId
err := adp.TopicUpdateOnMessage(msg.Topic, msg)
if err != nil {
- return err
+ return err, false
}
err = adp.MessageSave(msg)
if err != nil {
- return err
+ return err, false
}
+ markedReadBySender := false
// Mark message as read by the sender.
if readBySender {
// Make sure From is valid, otherwise we will reset values for all subscribers.
fromUid := types.ParseUid(msg.From)
if !fromUid.IsZero() {
// Ignore the error here. It's not a big deal if it fails.
- adp.SubsUpdate(msg.Topic, fromUid,
+ if subErr := adp.SubsUpdate(msg.Topic, fromUid,
map[string]interface{}{
"RecvSeqId": msg.SeqId,
- "ReadSeqId": msg.SeqId})
+ "ReadSeqId": msg.SeqId}); subErr != nil {
+ logs.Warn.Printf("topic[%s]: failed to mark message (seq: %d) read by sender - err: %+v", msg.Topic, msg.SeqId, subErr)
+ } else {
+ markedReadBySender = true
+ }
}
}
@@ -702,11 +707,11 @@ func (messagesMapper) Save(msg *types.Message, attachmentURLs []string, readBySe
}
}
if len(attachments) > 0 {
- return adp.FileLinkAttachments("", types.ZeroUid, msg.Uid(), attachments)
+ return adp.FileLinkAttachments("", types.ZeroUid, msg.Uid(), attachments), markedReadBySender
}
}
- return nil
+ return nil, markedReadBySender
}
// DeleteList deletes multiple messages defined by a list of ranges.
@@ -1056,6 +1061,43 @@ func (fileMapper) LinkAttachments(topic string, msgId types.Uid, attachments []s
return nil
}
+// PersistentCacheInterface is an interface which defines methods used for accessing persistent key-value cache.
+type PersistentCacheInterface interface {
+ // Get reads a persistent cache entry.
+ Get(key string) (string, error)
+ // Upsert creates or updates a persistent cache entry.
+ Upsert(key string, value string, failOnDuplicate bool) error
+ // Delete deletes a single persistent cache entry.
+ Delete(key string) error
+ // Expire expires older entries with the specified key prefix.
+ Expire(keyPrefix string, olderThan time.Time) error
+}
+
+// pcacheMapper is concrete type which implements PersistentCacheInterface.
+type pcacheMapper struct{}
+
+var PCache PersistentCacheInterface
+
+// Get reads a persistent cache entry.
+func (pcacheMapper) Get(key string) (string, error) {
+ return adp.PCacheGet(key)
+}
+
+// Upsert creates or updates a persistent cache entry.
+func (pcacheMapper) Upsert(key string, value string, failOnDuplicate bool) error {
+ return adp.PCacheUpsert(key, value, failOnDuplicate)
+}
+
+// Delete deletes a single persistent cache entry.
+func (pcacheMapper) Delete(key string) error {
+ return adp.PCacheDelete(key)
+}
+
+// Expire expires older entries with the specified key prefix.
+func (pcacheMapper) Expire(keyPrefix string, olderThan time.Time) error {
+ return adp.PCacheExpire(keyPrefix, olderThan)
+}
+
func init() {
Store = storeObj{}
Users = usersMapper{}
@@ -1064,4 +1106,5 @@ func init() {
Messages = messagesMapper{}
Devices = deviceMapper{}
Files = fileMapper{}
+ PCache = pcacheMapper{}
}
diff --git a/server/store/types/types.go b/server/store/types/types.go
index 6d1e23f59..bd84a557f 100644
--- a/server/store/types/types.go
+++ b/server/store/types/types.go
@@ -524,7 +524,7 @@ type User struct {
// AccessMode is a definition of access mode bits.
type AccessMode uint
-// Various access mode constants
+// Various access mode constants.
const (
ModeJoin AccessMode = 1 << iota // user can join, i.e. {sub} (J:1)
ModeRead // user can receive broadcasts ({data}, {info}) (R:2)
@@ -534,39 +534,38 @@ const (
ModeShare // user can invite new members (S:0x20, 32)
ModeDelete // user can hard-delete messages (D:0x40, 64)
ModeOwner // user is the owner (O:0x80, 128) - full access
- ModeUnset // Non-zero value to indicate unknown or undefined mode (:0x100, 256),
- // to make it different from ModeNone
+ ModeUnset // Non-zero value to indicate unknown or undefined mode (:0x100, 256), to make it different from ModeNone.
ModeNone AccessMode = 0 // No access, requests to gain access are processed normally (N:0)
- // Normal user's access to a topic ("JRWPS", 47, 0x2F)
+ // Normal user's access to a topic ("JRWPS", 47, 0x2F).
ModeCPublic AccessMode = ModeJoin | ModeRead | ModeWrite | ModePres | ModeShare
- // User's subscription to 'me' and 'fnd' ("JPS", 41, 0x29)
+ // User's subscription to 'me' and 'fnd' ("JPS", 41, 0x29).
ModeCSelf AccessMode = ModeJoin | ModePres | ModeShare
- // Owner's subscription to a generic topic ("JRWPASDO", 255, 0xFF)
+ // Owner's subscription to a generic topic ("JRWPASDO", 255, 0xFF).
ModeCFull AccessMode = ModeJoin | ModeRead | ModeWrite | ModePres | ModeApprove | ModeShare | ModeDelete | ModeOwner
- // Default P2P access mode ("JRWPA", 31, 0x1F)
+ // Default P2P access mode ("JRWPA", 31, 0x1F).
ModeCP2P AccessMode = ModeJoin | ModeRead | ModeWrite | ModePres | ModeApprove
// Default Auth access mode for a user ("JRWPAS", 63, 0x3F).
ModeCAuth AccessMode = ModeCP2P | ModeCPublic
- // Read-only access to topic ("JR", 3)
+ // Read-only access to topic ("JR", 3).
ModeCReadOnly = ModeJoin | ModeRead
- // Access to 'sys' topic by a root user ("JRWPD", 79, 0x4F)
+ // Access to 'sys' topic by a root user ("JRWPD", 79, 0x4F).
ModeCSys = ModeJoin | ModeRead | ModeWrite | ModePres | ModeDelete
- // Channel publisher: person authorized to publish content; no J: by invitation only ("RWPD", 78, 0x4E)
+ // Channel publisher: person authorized to publish content; no J: by invitation only ("RWPD", 78, 0x4E).
ModeCChnWriter = ModeRead | ModeWrite | ModePres | ModeShare
// Reader's access mode to a channel (JRP, 11, 0xB).
ModeCChnReader = ModeJoin | ModeRead | ModePres
- // Admin: user who can modify access mode ("OA", dec: 144, hex: 0x90)
+ // Admin: user who can modify access mode ("OA", dec: 144, hex: 0x90).
ModeCAdmin = ModeOwner | ModeApprove
- // Sharer: flags which define user who can be notified of access mode changes ("OAS", dec: 176, hex: 0xB0)
+ // Sharer: flags which define user who can be notified of access mode changes ("OAS", dec: 176, hex: 0xB0).
ModeCSharer = ModeCAdmin | ModeShare
- // Invalid mode to indicate an error
+ // Invalid mode to indicate an error.
ModeInvalid AccessMode = 0x100000
- // All possible valid bits (excluding ModeInvalid and ModeUnset) = 0xFF, 255
+ // All possible valid bits (excluding ModeInvalid and ModeUnset) = 0xFF, 255.
ModeBitmask AccessMode = ModeJoin | ModeRead | ModeWrite | ModePres | ModeApprove | ModeShare | ModeDelete | ModeOwner
)
@@ -1292,9 +1291,9 @@ const (
TopicCatMe TopicCat = iota
// TopicCatFnd is a value denoting 'fnd' topic.
TopicCatFnd
- // TopicCatP2P is a a value denoting 'p2p topic.
+ // TopicCatP2P is a value denoting 'p2p topic.
TopicCatP2P
- // TopicCatGrp is a a value denoting group topic.
+ // TopicCatGrp is a value denoting group topic.
TopicCatGrp
// TopicCatSys is a constant indicating a system topic.
TopicCatSys
diff --git a/server/templ/email-password-reset-ru.templ b/server/templ/email-password-reset-ru.templ
index 411037750..11073d762 100644
--- a/server/templ/email-password-reset-ru.templ
+++ b/server/templ/email-password-reset-ru.templ
@@ -1,6 +1,8 @@
{{/*
RUSSIAN
+ Password reset email.
+
See explanation in ./email-validation-en.templ
*/}}
diff --git a/server/templ/email-password-reset-vi.templ b/server/templ/email-password-reset-vi.templ
index 82b868066..6699ec137 100644
--- a/server/templ/email-password-reset-vi.templ
+++ b/server/templ/email-password-reset-vi.templ
@@ -1,5 +1,5 @@
{{/*
- ENGLISH
+ VIETNAMESE
This template defines contents of the password reset email.
diff --git a/server/templ/email-validation-vi.templ b/server/templ/email-validation-vi.templ
index c34a7a9fe..6532f96f7 100644
--- a/server/templ/email-validation-vi.templ
+++ b/server/templ/email-validation-vi.templ
@@ -1,16 +1,7 @@
{{/*
- ENGLISH
+ VIETNAMESE
- This template defines content of the email sent to users as a request to confirm registration email address.
- See https://golang.org/pkg/text/template/ for syntax.
-
- The template must contain the following parts parts:
- - 'subject': Subject line of an email message
- - One or both of the following:
- - 'body_html': HTML content of the message. A header "Content-type: text/html" will be added.
- - 'body_plain': plain text content of the message. A header "Content-type: text/plain" will be added.
-
- If both body_html and body_plain are included, both are sent as parts of 'multipart/alternative' message.
+ See explanation in ./email-validation-en.templ
*/}}
{{define "subject" -}}
diff --git a/server/templ/sms-universal-en.templ b/server/templ/sms-universal-en.templ
new file mode 100644
index 000000000..afa343d3a
--- /dev/null
+++ b/server/templ/sms-universal-en.templ
@@ -0,0 +1,8 @@
+{{/*
+ ENGLISH
+
+ Universal confirmation and password reset template for SMS.
+*/}}
+
+Tinode confirmation code: {{.Code}}
+{{.HostUrl}}
diff --git a/server/templ/sms-universal-es.templ b/server/templ/sms-universal-es.templ
new file mode 100644
index 000000000..e69a09645
--- /dev/null
+++ b/server/templ/sms-universal-es.templ
@@ -0,0 +1,8 @@
+{{/*
+ SPANISH
+
+ Universal confirmation and password reset template for SMS.
+*/}}
+
+Código de confirmación de Tinode: {{.Code}}
+{{.HostUrl}}
diff --git a/server/templ/sms-universal-fr.templ b/server/templ/sms-universal-fr.templ
new file mode 100644
index 000000000..0d80c1e86
--- /dev/null
+++ b/server/templ/sms-universal-fr.templ
@@ -0,0 +1,8 @@
+{{/*
+ FRENCH
+
+ Modèle universel de confirmation et de réinitialisation du mot de passe pour SMS.
+*/}}
+
+Code de confirmation Tinode : {{.Code}}
+{{.HostUrl}}
diff --git a/server/templ/sms-universal-pt.templ b/server/templ/sms-universal-pt.templ
new file mode 100644
index 000000000..1838842cb
--- /dev/null
+++ b/server/templ/sms-universal-pt.templ
@@ -0,0 +1,8 @@
+{{/*
+ PORTUGESE
+
+ Modelo universal de confirmação e redefinição de senha para SMS.
+*/}}
+
+Código de confirmação Tinode: {{.Code}}
+{{.HostUrl}}
diff --git a/server/templ/sms-universal-ru.templ b/server/templ/sms-universal-ru.templ
new file mode 100644
index 000000000..f360d448f
--- /dev/null
+++ b/server/templ/sms-universal-ru.templ
@@ -0,0 +1,8 @@
+{{/*
+ RUSSIAN
+
+ Универсальный шаблон подтверждения и сброса пароля для СМС.
+*/}}
+
+Код подтверждения Tinode: {{.Code}}
+{{.HostUrl}}
diff --git a/server/templ/sms-universal-vi.templ b/server/templ/sms-universal-vi.templ
new file mode 100644
index 000000000..fdf844a36
--- /dev/null
+++ b/server/templ/sms-universal-vi.templ
@@ -0,0 +1,8 @@
+{{/*
+ VIETNAMESE
+
+ Universal confirmation and password reset template for SMS.
+*/}}
+
+Mã xác thực từ Tinode: {{.Code}}
+{{.HostUrl}}
diff --git a/server/templ/sms-universal-zh.templ b/server/templ/sms-universal-zh.templ
new file mode 100644
index 000000000..0fac82d43
--- /dev/null
+++ b/server/templ/sms-universal-zh.templ
@@ -0,0 +1,8 @@
+{{/*
+ CHINESE
+
+ Universal confirmation and password reset template for SMS.
+*/}}
+
+【Tinode】验证码: {{.Code}}
+{{.HostUrl}}
diff --git a/server/templ/sms-validation-es.templ b/server/templ/sms-validation-es.templ
deleted file mode 100644
index 22fba38e2..000000000
--- a/server/templ/sms-validation-es.templ
+++ /dev/null
@@ -1 +0,0 @@
-Código de confirmación de Tinode: {{.Code}}
\ No newline at end of file
diff --git a/server/templ/sms-validation-vi.templ b/server/templ/sms-validation-vi.templ
deleted file mode 100644
index 4c39b832b..000000000
--- a/server/templ/sms-validation-vi.templ
+++ /dev/null
@@ -1 +0,0 @@
-Mã xác thực từ Tinode: {{.Code}}
\ No newline at end of file
diff --git a/server/templ/sms-validation-zh.templ b/server/templ/sms-validation-zh.templ
deleted file mode 100644
index fa6082887..000000000
--- a/server/templ/sms-validation-zh.templ
+++ /dev/null
@@ -1 +0,0 @@
-【Tinode】验证码: {{.Code}}
diff --git a/server/templ/sms-validation.templ b/server/templ/sms-validation.templ
deleted file mode 100644
index c71e75994..000000000
--- a/server/templ/sms-validation.templ
+++ /dev/null
@@ -1 +0,0 @@
-Tinode confirmation code: {{.Code}}
\ No newline at end of file
diff --git a/server/tinode.conf b/server/tinode.conf
index 7b141121a..e73e9b79f 100644
--- a/server/tinode.conf
+++ b/server/tinode.conf
@@ -15,6 +15,10 @@
// Cache-Control header for static content in seconds. 39600 is 11 hours.
"cache_control": 39600,
+ // If true, do not attempt to negotiate websocket per message compression (RFC 7692.4).
+ // It should be disabled (set to true) if you are using MSFT IIS as a reverse proxy.
+ "ws_compression_disabled": false,
+
// URL path for mounting the directory with static files.
"static_mount": "/",
@@ -44,6 +48,9 @@
// Maximum number of indexable tags per topic or user.
"max_tag_count": 16,
+ // If true, ordinary users cannot delete their accounts.
+ "permanent_accounts": false,
+
// URL path for exposing runtime stats. Disabled if the path is blank or "-".
// Could be overriden from the command line with --expvar.
"expvar": "/debug/vars",
@@ -185,6 +192,18 @@
// Windows:
// powershell -command "[Convert]::ToBase64String((1..32|%{[byte](Get-Random -Max 256)}))"
"key": "wfaY2RgF2S1OQI/ZlK+LSrp1KB2jwAdGAIHQ7JZn+Kc="
+ },
+
+ // Short code authenticator for resetting passwords.
+ "code": {
+ // Lifetime of a security code in seconds. 900 seconds = 15 minutes.
+ "expire_in": 900,
+
+ // Number of times a user can try to enter the code.
+ "max_retries": 3,
+
+ // Length of the secret code.
+ "code_length": 6
}
},
@@ -204,6 +223,28 @@
// Configurations of individual adapters.
"adapters": {
+ // PostgreSQL configuration. See https://godoc.org/github.com/jackc/pgx#Config
+ // for other possible options.
+ "postgres": {
+ // PostgreSQL connection settings.
+ "User": "postgres",
+ "Passwd": "postgres",
+ "Host": "localhost",
+ "Port": "5432",
+ "DBName": "tinode",
+
+ // PostgreSQL connection pool settings.
+ // Maximum number of open connections to the database. Zero means unlimited.
+ "max_open_conns": 64,
+ // Maximum number of connections in the idle connection pool. Zero means no idle connections are retained.
+ "max_idle_conns": 64,
+ // Maximum amount of time a connection may be reused. Zero means unlimited.
+ "conn_max_lifetime": 60,
+ // Maximum amount of time waiting for a connection from the pool. Zero means no timeout.
+ "sql_timeout": 10
+ },
+
+
// MySQL configuration. See https://godoc.org/github.com/go-sql-driver/mysql#Config
// for other possible options.
"mysql": {
@@ -373,14 +414,36 @@
}
},
- // Dummy placeholder validator for SMS and voice validation. Disabled by default.
- // Use something like twillio.com in production.
+ // Placeholder validator for SMS and voice validation. Disabled by default.
+ // Use something like twilio.com or sinch.com in production.
"tel": {
"add_to_tags": true,
"config": {
- "languages": ["en"],
- "template": "./templ/sms-validation.templ",
- "max_retries": 4,
+ // Address of the host where the Tinode server is running. This will be used
+ // in URLs in the SMS.
+ "host_url": "http://localhost:6060/",
+
+ // Optional list of locales to try to load templates for. If you don't care about i18n,
+ // leave it blank or remove. The first language in the list is the default language.
+ "languages": ["en", "es", "fr", "pt", "ru", "vi", "zh"],
+
+ // String to use in the From field of the SMS.
+ "sender": "Tinode",
+
+ // Message template for credential validation and password reset. The file path itself is
+ // treated as a template. It's resolved by using the "languages" field above. One template
+ // per language.
+ "universal_templ": "./templ/sms-universal-{{.Language}}.templ",
+
+ // Allow this many confirmation attempts before blocking the credential.
+ "max_retries": 3,
+
+ // Dummy response to accept.
+ //
+ // === IMPORTANT ===
+ //
+ // REMOVE IT IN PRODUCTION!!! Otherwise anyone will be able to register
+ // with fake phone numbers.
"debug_response": "123456"
}
}
diff --git a/server/topic.go b/server/topic.go
index f9c8b4b66..792e57325 100644
--- a/server/topic.go
+++ b/server/topic.go
@@ -60,9 +60,9 @@ type Topic struct {
tags []string
// Topic's public data
- public interface{}
+ public any
// Topic's trusted data
- trusted interface{}
+ trusted any
// Topic's per-subscriber data
perUser map[types.Uid]perUserData
@@ -137,14 +137,14 @@ type perUserData struct {
// ID of the latest Delete operation
delID int
- private interface{}
+ private any
modeWant types.AccessMode
modeGiven types.AccessMode
// P2P only:
- public interface{}
- trusted interface{}
+ public any
+ trusted any
lastSeen *time.Time
lastUA string
@@ -959,7 +959,7 @@ func (t *Topic) sendSubNotifications(asUid types.Uid, sid, userAgent string) {
// Saves a new message (defined by head, content and attachments) in the topic
// in response to a client request (msg, asUid) and broadcasts it to the attached sessions.
-func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid, noEcho bool, attachments []string, head map[string]interface{}, content interface{}) error {
+func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid, noEcho bool, attachments []string, head map[string]any, content any) error {
pud, userFound := t.perUser[asUid]
// Anyone is allowed to post to 'sys' topic.
if t.cat != types.TopicCatSys {
@@ -970,7 +970,19 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
}
}
- if err := store.Messages.Save(
+ if msg.sess != nil && msg.sess.uid != asUid {
+ // The "sender" header contains ID of the user who sent the message on behalf of asUid.
+ if head == nil {
+ head = map[string]any{}
+ }
+ head["sender"] = msg.sess.uid.UserId()
+ } else if head != nil {
+ // Make sure the received Head does not include a fake "sender" header.
+ delete(head, "sender")
+ }
+
+ markedReadBySender := false
+ if err, unreadUpdated := store.Messages.Save(
&types.Message{
ObjHeader: types.ObjHeader{CreatedAt: msg.Timestamp},
SeqId: t.lastID + 1,
@@ -983,6 +995,8 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
msg.sess.queueOut(ErrUnknown(msg.Id, t.original(asUid), msg.Timestamp))
return err
+ } else {
+ markedReadBySender = unreadUpdated
}
t.lastID++
@@ -995,7 +1009,7 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
if msg.Id != "" && msg.sess != nil {
reply := NoErrAccepted(msg.Id, t.original(asUid), msg.Timestamp)
- reply.Ctrl.Params = map[string]interface{}{"seq": t.lastID}
+ reply.Ctrl.Params = map[string]any{"seq": t.lastID}
msg.sess.queueOut(reply)
}
@@ -1032,7 +1046,7 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
logs.Warn.Printf("prep to pushForData[%s]: message: %v", asUid, msg.sess)
// sendPush will update unread message count and send push notification.
- if pushRcpt := t.pushForData(asUid, data.Data, msg.OrganizationId); pushRcpt != nil {
+ if pushRcpt := t.pushForData(asUid, data.Data, markedReadBySender, msg.OrganizationId); pushRcpt != nil {
sendPush(pushRcpt)
}
return nil
@@ -1171,7 +1185,7 @@ func (t *Topic) handleNoteBroadcast(msg *ClientComMessage) {
topicName = msg.Note.Topic
}
- upd := map[string]interface{}{}
+ upd := map[string]any{}
if recv > 0 {
upd["RecvSeqId"] = recv
}
@@ -1305,17 +1319,15 @@ func (t *Topic) broadcastToSessions(msg *ServerComMessage) {
continue
}
}
- } else {
+ } else if pssd.isChanSub && types.IsChannel(sess.sid) {
// If it's a chnX multiplexing session, check if there's a corresponding
// grpX multiplexing session as we don't want to send the message to both.
- if pssd.isChanSub && types.IsChannel(sess.sid) {
- grpSid := types.ChnToGrp(sess.sid)
- if grpSess := globals.sessionStore.Get(grpSid); grpSess != nil && grpSess.isMultiplex() {
- // If grpX multiplexing session's attached to topic, skip this chnX session
- // (message will be routed to the topic proxy via the grpX session).
- if _, attached := t.sessions[grpSess]; attached {
- continue
- }
+ grpSid := types.ChnToGrp(sess.sid)
+ if grpSess := globals.sessionStore.Get(grpSid); grpSess != nil && grpSess.isMultiplex() {
+ // If grpX multiplexing session's attached to topic, skip this chnX session
+ // (message will be routed to the topic proxy via the grpX session).
+ if _, attached := t.sessions[grpSess]; attached {
+ continue
}
}
}
@@ -1361,7 +1373,7 @@ func (t *Topic) subscriptionReply(asChan bool, msg *ClientComMessage) error {
msgsub.Newsub = !found || pud.deleted
}
- var private interface{}
+ var private any
var mode string
if msgsub.Set != nil {
if msgsub.Set.Sub != nil {
@@ -1409,7 +1421,7 @@ func (t *Topic) subscriptionReply(asChan bool, msg *ClientComMessage) error {
}
}
- params := map[string]interface{}{}
+ params := map[string]any{}
// Report back the assigned access mode.
if modeChanged != nil {
params["acs"] = modeChanged
@@ -1466,7 +1478,7 @@ func (t *Topic) subscriptionReply(asChan bool, msg *ClientComMessage) error {
// E. User is accepting ownership transfer (requesting ownership transfer is not permitted).
// In case of a group topic the user may be a reader or a full subscriber.
func (t *Topic) thisUserSub(sess *Session, pkt *ClientComMessage, asUid types.Uid, asChan bool, want string,
- private interface{}) (*MsgAccessMode, error) {
+ private any) (*MsgAccessMode, error) {
now := types.TimeNow()
asLvl := auth.Level(pkt.AuthLvl)
@@ -1623,7 +1635,7 @@ func (t *Topic) thisUserSub(sess *Session, pkt *ClientComMessage, asUid types.Ui
} else if asChan && userData.modeWant != oldWant {
// Channel reader changed access mode, save changed mode to db.
if err := store.Subs.Update(tname, asUid,
- map[string]interface{}{"ModeWant": userData.modeWant}); err != nil {
+ map[string]any{"ModeWant": userData.modeWant}); err != nil {
sess.queueOut(ErrUnknownReply(pkt, now))
return nil, err
}
@@ -1723,7 +1735,7 @@ func (t *Topic) thisUserSub(sess *Session, pkt *ClientComMessage, asUid types.Ui
}
// Save changes to DB
- update := map[string]interface{}{}
+ update := map[string]any{}
if isNullValue(private) {
update["Private"] = nil
userData.private = nil
@@ -1757,7 +1769,7 @@ func (t *Topic) thisUserSub(sess *Session, pkt *ClientComMessage, asUid types.Ui
oldOwnerData.modeGiven = (oldOwnerData.modeGiven & ^types.ModeOwner)
oldOwnerData.modeWant = (oldOwnerData.modeWant & ^types.ModeOwner)
if err := store.Subs.Update(t.name, t.owner,
- map[string]interface{}{
+ map[string]any{
"ModeWant": oldOwnerData.modeWant,
"ModeGiven": oldOwnerData.modeGiven,
}); err != nil {
@@ -2002,7 +2014,7 @@ func (t *Topic) anotherUserSub(sess *Session, asUid, target types.Uid, asChan bo
// Save changed value to database
if err := store.Subs.Update(t.name, target,
- map[string]interface{}{"ModeGiven": modeGiven}); err != nil {
+ map[string]any{"ModeGiven": modeGiven}); err != nil {
return nil, err
}
@@ -2165,7 +2177,7 @@ func (t *Topic) replySetDesc(sess *Session, asUid types.Uid, asChan bool,
authLevel auth.Level, msg *ClientComMessage) error {
now := types.TimeNow()
- assignAccess := func(upd map[string]interface{}, mode *MsgDefaultAcsMode) error {
+ assignAccess := func(upd map[string]any, mode *MsgDefaultAcsMode) error {
if mode == nil {
return nil
}
@@ -2202,7 +2214,7 @@ func (t *Topic) replySetDesc(sess *Session, asUid types.Uid, asChan bool,
return nil
}
- assignGenericValues := func(upd map[string]interface{}, what string, dst, src interface{}) (changed bool) {
+ assignGenericValues := func(upd map[string]any, what string, dst, src any) (changed bool) {
if dst, changed = mergeInterfaces(dst, src); changed {
upd[what] = dst
}
@@ -2216,9 +2228,9 @@ func (t *Topic) replySetDesc(sess *Session, asUid types.Uid, asChan bool,
var err error
// Change to the main object (user or topic).
- core := make(map[string]interface{})
+ core := make(map[string]any)
// Change to subscription.
- sub := make(map[string]interface{})
+ sub := make(map[string]any)
if set := msg.Set; set.Desc != nil {
if set.Desc.Trusted != nil && authLevel != auth.LevelRoot {
// Only ROOT can change Trusted.
@@ -2447,8 +2459,9 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level
return errors.New("attempt to search by restricted tags")
}
- // TODO: allow root to find suspended users and topics.
- subs, err = store.Users.FindSubs(asUid, req, opt)
+ // Ordinary users: find only active topics and accounts.
+ // Root users: find all topics and accounts, including suspended and soft-deleted.
+ subs, err = store.Users.FindSubs(asUid, req, opt, sess.authLvl != auth.LevelRoot)
if err != nil {
sess.queueOut(decodeStoreErrorExplicitTs(err, id, msg.Original, now, incomingReqTs, nil))
return err
@@ -2659,7 +2672,7 @@ func (t *Topic) replyGetSub(sess *Session, asUid types.Uid, authLevel auth.Level
sess.queueOut(&ServerComMessage{Meta: meta})
} else {
// Inform the client that there are no subscriptions.
- sess.queueOut(NoContentParamsReply(msg, now, map[string]interface{}{"what": "sub"}))
+ sess.queueOut(NoContentParamsReply(msg, now, map[string]any{"what": "sub"}))
}
return nil
@@ -2702,7 +2715,7 @@ func (t *Topic) replySetSub(sess *Session, pkt *ClientComMessage, asChan bool) e
var resp *ServerComMessage
if modeChanged != nil {
// Report resulting access mode.
- params := map[string]interface{}{"acs": modeChanged}
+ params := map[string]any{"acs": modeChanged}
if target != asUid {
params["user"] = target.UserId()
}
@@ -2767,10 +2780,10 @@ func (t *Topic) replyGetData(sess *Session, asUid types.Uid, asChan bool, req *M
// Inform the requester that all the data has been served.
if count == 0 {
- sess.queueOut(NoContentParamsReply(msg, now, map[string]interface{}{"what": "data"}))
+ sess.queueOut(NoContentParamsReply(msg, now, map[string]any{"what": "data"}))
} else {
sess.queueOut(NoErrDeliveredParams(msg.Id, msg.Original, now,
- map[string]interface{}{"what": "data", "count": count}))
+ map[string]any{"what": "data", "count": count}))
}
return nil
@@ -2829,7 +2842,7 @@ func (t *Topic) replySetTags(sess *Session, asUid types.Uid, msg *ClientComMessa
} else {
added, removed, _ := stringSliceDelta(t.tags, tags)
if len(added) > 0 || len(removed) > 0 {
- update := map[string]interface{}{"Tags": tags, "UpdatedAt": now}
+ update := map[string]any{"Tags": tags, "UpdatedAt": now}
if t.cat == types.TopicCatMe {
err = store.Users.Update(asUid, update)
} else if t.cat == types.TopicCatGrp {
@@ -2842,7 +2855,7 @@ func (t *Topic) replySetTags(sess *Session, asUid types.Uid, msg *ClientComMessa
t.tags = tags
t.presSubsOnline("tags", "", nilPresParams, &presFilters{singleUser: asUid.UserId()}, sess.sid)
- params := make(map[string]interface{})
+ params := make(map[string]any)
if len(added) > 0 {
params["added"] = len(added)
}
@@ -3347,7 +3360,7 @@ func (t *Topic) evictUser(uid types.Uid, unsub bool, skip string) {
// Detach all user's sessions
msg := NoErrEvicted("", t.original(uid), now)
- msg.Ctrl.Params = map[string]interface{}{"unsub": unsub}
+ msg.Ctrl.Params = map[string]any{"unsub": unsub}
msg.SkipSid = skip
msg.uid = uid
msg.AsUser = uid.UserId()
@@ -3595,12 +3608,12 @@ func (t *Topic) p2pOtherUser(uid types.Uid) types.Uid {
}
// Get per-session value of fnd.Public
-func (t *Topic) fndGetPublic(sess *Session) interface{} {
+func (t *Topic) fndGetPublic(sess *Session) any {
if t.cat == types.TopicCatFnd {
if t.public == nil {
return nil
}
- if pubmap, ok := t.public.(map[string]interface{}); ok {
+ if pubmap, ok := t.public.(map[string]any); ok {
return pubmap[sess.sid]
}
panic("Invalid Fnd.Public type")
@@ -3609,21 +3622,21 @@ func (t *Topic) fndGetPublic(sess *Session) interface{} {
}
// Assign per-session fnd.Public. Returns true if value has been changed.
-func (t *Topic) fndSetPublic(sess *Session, public interface{}) bool {
+func (t *Topic) fndSetPublic(sess *Session, public any) bool {
if t.cat != types.TopicCatFnd {
panic("Not Fnd topic")
}
- var pubmap map[string]interface{}
+ var pubmap map[string]any
var ok bool
if t.public != nil {
- if pubmap, ok = t.public.(map[string]interface{}); !ok {
+ if pubmap, ok = t.public.(map[string]any); !ok {
// This could only happen if fnd.public is assigned outside of this function.
panic("Invalid Fnd.Public type")
}
}
if pubmap == nil {
- pubmap = make(map[string]interface{})
+ pubmap = make(map[string]any)
}
if public != nil {
@@ -3646,7 +3659,7 @@ func (t *Topic) fndRemovePublic(sess *Session) {
}
// FIXME: case of a multiplexing session won't work correctly.
// Maybe handle it at the proxy topic.
- if pubmap, ok := t.public.(map[string]interface{}); ok {
+ if pubmap, ok := t.public.(map[string]any); ok {
delete(pubmap, sess.sid)
return
}
diff --git a/server/topic_test.go b/server/topic_test.go
index 06531f0d4..b733850b4 100644
--- a/server/topic_test.go
+++ b/server/topic_test.go
@@ -17,7 +17,7 @@ import (
)
type responses struct {
- messages []interface{}
+ messages []any
}
// Test fixture.
@@ -70,7 +70,7 @@ func (b *TopicTestHelper) newSession(sid string, uid types.Uid) (*Session, *resp
sid: sid,
uid: uid,
subs: make(map[string]*Subscription),
- send: make(chan interface{}, 10),
+ send: make(chan any, 10),
detach: make(chan string, 10),
}
r := &responses{}
@@ -190,7 +190,7 @@ func TestHandleBroadcastDataP2P(t *testing.T) {
helper := TopicTestHelper{}
helper.setUp(t, numUsers, types.TopicCatP2P, "p2p-test" /*attach=*/, true)
defer helper.tearDown()
- helper.mm.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
+ helper.mm.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, true)
from := helper.uids[0].UserId()
msg := &ClientComMessage{
@@ -270,7 +270,7 @@ func TestHandleBroadcastCall(t *testing.T) {
globals.iceServers = []iceServer{{Username: "dummy"}}
helper.topic.lastID = 5
defer helper.tearDown()
- helper.mm.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
+ helper.mm.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, true)
from := helper.uids[0].UserId()
msg := &ClientComMessage{
@@ -278,7 +278,7 @@ func TestHandleBroadcastCall(t *testing.T) {
Original: from,
Pub: &MsgClientPub{
Topic: "p2p",
- Head: map[string]interface{}{"webrtc": "started"},
+ Head: map[string]any{"webrtc": "started"},
Content: "test",
NoEcho: true,
},
@@ -376,7 +376,7 @@ func TestHandleBroadcastDataGroup(t *testing.T) {
store.Messages = nil
helper.tearDown()
}()
- helper.mm.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
+ helper.mm.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, true)
// User 3 isn't allowed to read.
pu3 := helper.topic.perUser[helper.uids[3]]
@@ -526,7 +526,7 @@ func TestHandleBroadcastDataDbError(t *testing.T) {
defer helper.tearDown()
// DB returns an error.
- helper.mm.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(types.ErrInternal)
+ helper.mm.EXPECT().Save(gomock.Any(), gomock.Any(), gomock.Any()).Return(types.ErrInternal, false)
// Make test message.
from := helper.uids[0].UserId()
@@ -626,7 +626,7 @@ func TestHandleBroadcastInfoP2P(t *testing.T) {
from := helper.uids[0]
to := helper.uids[1]
- helper.ss.EXPECT().Update(topicName, from, map[string]interface{}{"ReadSeqId": readId}).Return(nil)
+ helper.ss.EXPECT().Update(topicName, from, map[string]any{"ReadSeqId": readId}).Return(nil)
msg := &ClientComMessage{
AsUser: from.UserId(),
@@ -918,7 +918,7 @@ func TestHandleBroadcastInfoDbError(t *testing.T) {
from := helper.uids[0]
to := helper.uids[1]
- helper.ss.EXPECT().Update(topicName, from, map[string]interface{}{"ReadSeqId": readId}).Return(types.ErrInternal)
+ helper.ss.EXPECT().Update(topicName, from, map[string]any{"ReadSeqId": readId}).Return(types.ErrInternal)
msg := &ClientComMessage{
AsUser: from.UserId(),
@@ -1022,7 +1022,7 @@ func TestHandleBroadcastInfoChannelProcessing(t *testing.T) {
helper.topic.perUser[uid] = pud
}
- helper.ss.EXPECT().Update(chanName, from, map[string]interface{}{"ReadSeqId": readId}).Return(nil)
+ helper.ss.EXPECT().Update(chanName, from, map[string]any{"ReadSeqId": readId}).Return(nil)
msg := &ClientComMessage{
AsUser: from.UserId(),
@@ -2375,8 +2375,8 @@ func SupersetOf(subset map[string]string) gomock.Matcher {
return &supersetOf{subset}
}
-func (s *supersetOf) Matches(x interface{}) bool {
- super := x.(map[string]interface{})
+func (s *supersetOf) Matches(x any) bool {
+ super := x.(map[string]any)
if super == nil {
return false
}
@@ -2407,7 +2407,7 @@ func TestHandleMetaSetDescMePublicPrivate(t *testing.T) {
uid := helper.uids[0]
gomock.InOrder(
helper.uu.EXPECT().Update(uid, SupersetOf(map[string]string{"Public": "new public"})).Return(nil),
- helper.ss.EXPECT().Update(topicName, uid, map[string]interface{}{"Private": "new private"}).Return(nil),
+ helper.ss.EXPECT().Update(topicName, uid, map[string]any{"Private": "new private"}).Return(nil),
)
meta := &ClientComMessage{
diff --git a/server/user.go b/server/user.go
index d1d2c3be7..382ba673b 100644
--- a/server/user.go
+++ b/server/user.go
@@ -25,7 +25,7 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
// The session cannot authenticate with the new account because it's already authenticated.
if msg.Acc.Login && (!s.uid.IsZero() || rec != nil) {
s.queueOut(ErrAlreadyAuthenticated(msg.Id, "", msg.Timestamp))
- logs.Warn.Println("create user: login requested while authenticated", s.sid)
+ logs.Warn.Println("create user: login requested while authenticated, sid=", s.sid)
return
}
@@ -34,34 +34,34 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
if authhdl == nil {
// New accounts must have an authentication scheme
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
- logs.Warn.Println("create user: unknown auth handler", s.sid)
+ logs.Warn.Println("create user: unknown auth handler, sid=", s.sid)
return
}
// Check if login is unique.
if ok, err := authhdl.IsUnique(msg.Acc.Secret, s.remoteAddr); !ok {
- logs.Warn.Println("create user: auth secret is not unique", err, s.sid)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp,
- map[string]interface{}{"what": "auth"}))
+ logs.Warn.Println("create user: auth secret is not unique", err, "sid=", s.sid)
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp,
+ map[string]any{"what": "auth"}))
return
}
var user types.User
- var private interface{}
+ var private any
// If account state is being assigned, make sure the sender is a root user.
if msg.Acc.State != "" {
if auth.Level(msg.AuthLvl) != auth.LevelRoot {
- logs.Warn.Println("create user: attempt to set account state by non-root", s.sid)
+ logs.Warn.Println("create user: attempt to set account state by non-root, sid=", s.sid)
msg := ErrPermissionDenied(msg.Id, "", msg.Timestamp)
- msg.Ctrl.Params = map[string]interface{}{"what": "state"}
+ msg.Ctrl.Params = map[string]any{"what": "state"}
s.queueOut(msg)
return
}
state, err := types.NewObjState(msg.Acc.State)
if err != nil || state == types.StateUndefined || state == types.StateDeleted {
- logs.Warn.Println("create user: invalid account state", err, s.sid)
+ logs.Warn.Println("create user: invalid account state", err, "sid=", s.sid)
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
return
}
@@ -71,9 +71,9 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
// Ensure tags are unique and not restricted.
if tags := normalizeTags(msg.Acc.Tags); tags != nil {
if !restrictedTagsEqual(tags, nil, globals.immutableTagNS) {
- logs.Warn.Println("create user: attempt to directly assign restricted tags", s.sid)
+ logs.Warn.Println("create user: attempt to directly assign restricted tags, sid=", s.sid)
msg := ErrPermissionDenied(msg.Id, "", msg.Timestamp)
- msg.Ctrl.Params = map[string]interface{}{"what": "tags"}
+ msg.Ctrl.Params = map[string]any{"what": "tags"}
s.queueOut(msg)
return
}
@@ -87,9 +87,9 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
cr := &creds[i]
vld := store.Store.GetValidator(cr.Method)
if _, err := vld.PreCheck(cr.Value, cr.Params); err != nil {
- logs.Warn.Println("create user: failed credential pre-check", cr, err, s.sid)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp,
- map[string]interface{}{"what": cr.Method}))
+ logs.Warn.Println("create user: failed credential pre-check", cr, err, "sid=", s.sid)
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp,
+ map[string]any{"what": cr.Method}))
return
}
}
@@ -128,7 +128,7 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
// Create user record in the database.
if _, err := store.Users.Create(&user, private); err != nil {
- logs.Warn.Println("create user: failed to create user", err, s.sid)
+ logs.Warn.Println("create user: failed to create user", err, "sid=", s.sid)
s.queueOut(ErrUnknown(msg.Id, "", msg.Timestamp))
return
}
@@ -136,10 +136,12 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
// Add authentication record. The authhdl.AddRecord may change tags.
rec, err := authhdl.AddRecord(&auth.Rec{Uid: user.Uid(), Tags: user.Tags}, msg.Acc.Secret, s.remoteAddr)
if err != nil {
- logs.Warn.Println("create user: add auth record failed", err, s.sid)
+ logs.Warn.Println("create user: add auth record failed", err, "sid=", s.sid)
// Attempt to delete incomplete user record
- store.Users.Delete(user.Uid(), false)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp, nil))
+ if err = store.Users.Delete(user.Uid(), true); err != nil {
+ logs.Warn.Println("create user: failed to delete incomplete user record", err, "sid=", s.sid)
+ }
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
@@ -149,10 +151,12 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
logs.Warn.Println("create user: missing credentials; have:", creds, "want:",
globals.authValidators[rec.AuthLevel], s.sid)
// Attempt to delete incomplete user record
- store.Users.Delete(user.Uid(), false)
+ if err = store.Users.Delete(user.Uid(), true); err != nil {
+ logs.Warn.Println("create user: failed to delete incomplete user record", err, "sid=", s.sid)
+ }
_, missing, _ := stringSliceDelta(globals.authValidators[rec.AuthLevel], credentialMethods(creds))
- s.queueOut(decodeStoreError(types.ErrPolicy, msg.Id, "", msg.Timestamp,
- map[string]interface{}{"creds": missing}))
+ s.queueOut(decodeStoreError(types.ErrPolicy, msg.Id, msg.Timestamp,
+ map[string]any{"creds": missing}))
return
}
@@ -165,15 +169,17 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
validated, _, err := addCreds(user.Uid(), creds, rec.Tags, s.lang, tmpToken)
if err != nil {
// Delete incomplete user record.
- store.Users.Delete(user.Uid(), false)
- logs.Warn.Println("create user: failed to save or validate credential", err, s.sid)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp, nil))
+ logs.Warn.Println("create user: failed to save or validate credential", err, "sid=", s.sid)
+ if err = store.Users.Delete(user.Uid(), true); err != nil {
+ logs.Warn.Println("create user: failed to delete incomplete user record", err, "sid=", s.sid)
+ }
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
if msg.Extra != nil && len(msg.Extra.Attachments) > 0 {
if err := store.Files.LinkAttachments(user.Uid().UserId(), types.ZeroUid, msg.Extra.Attachments); err != nil {
- logs.Warn.Println("create user: failed to link avatar attachment", err, s.sid)
+ logs.Warn.Println("create user: failed to link avatar attachment", err, "sid=", s.sid)
// This is not a critical error, continue execution.
}
}
@@ -186,13 +192,13 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
} else {
// Not using the new account for logging in.
reply = NoErrCreated(msg.Id, "", msg.Timestamp)
- reply.Ctrl.Params = map[string]interface{}{
+ reply.Ctrl.Params = map[string]any{
"user": user.Uid().UserId(),
"authlvl": rec.AuthLevel.String(),
}
}
- params := reply.Ctrl.Params.(map[string]interface{})
+ params := reply.Ctrl.Params.(map[string]any)
params["desc"] = &MsgTopicDesc{
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
@@ -214,12 +220,12 @@ func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
// * Credentials update
func replyUpdateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
if s.uid.IsZero() && rec == nil {
- // Session is not authenticated and no token provided.
+ // Session is not authenticated and no temporary auth is provided.
logs.Warn.Println("replyUpdateUser: not a new account and not authenticated", s.sid)
s.queueOut(ErrPermissionDenied(msg.Id, "", msg.Timestamp))
return
} else if msg.AsUser != "" && rec != nil {
- // Two UIDs: one from msg.from, one from token. Ambigous, reject.
+ // Two UIDs: one from msg.from, one from temporary auth. Ambigous, reject.
logs.Warn.Println("replyUpdateUser: got both authenticated session and token", s.sid)
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
return
@@ -264,11 +270,11 @@ func replyUpdateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
}
if err != nil {
logs.Warn.Println("replyUpdateUser: failed to fetch user from DB", err, s.sid)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp, nil))
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
- var params map[string]interface{}
+ var params map[string]any
if msg.Acc.Scheme != "" {
err = updateUserAuth(msg, user, rec, s.remoteAddr)
} else if len(msg.Acc.Cred) > 0 {
@@ -294,7 +300,7 @@ func replyUpdateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
}
_, missing, _ := stringSliceDelta(globals.authValidators[authLvl], validated)
if len(missing) > 0 {
- params = map[string]interface{}{"cred": missing}
+ params = map[string]any{"cred": missing}
}
}
}
@@ -311,7 +317,7 @@ func replyUpdateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
if err != nil {
logs.Warn.Println("replyUpdateUser: failed to update user", err, s.sid)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp, nil))
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
@@ -356,8 +362,8 @@ func addCreds(uid types.Uid, creds []MsgCredClient, extraTags []string,
for i := range creds {
cr := &creds[i]
vld := store.Store.GetValidator(cr.Method)
- if vld == nil {
- // Ignore unknown validator.
+ if vld == nil || !vld.IsInitialized() {
+ // Ignore unknown or un-initialized validator.
continue
}
@@ -589,6 +595,13 @@ func replyDelUser(s *Session, msg *ClientComMessage) {
var uid types.Uid
if msg.Del.User == "" || msg.Del.User == s.uid.UserId() {
+ // Check if account deletion is disabled.
+ if globals.permanentAccounts && s.authLvl != auth.LevelRoot {
+ logs.Warn.Println("replyDelUser: account deletion disabled", s.sid)
+ s.queueOut(ErrPolicy(msg.Id, "", msg.Timestamp))
+ return
+ }
+
// Delete current user.
uid = s.uid
} else if s.authLvl == auth.LevelRoot {
@@ -658,7 +671,7 @@ func replyDelUser(s *Session, msg *ClientComMessage) {
// Delete user's records from the database.
if err := store.Users.Delete(uid, msg.Del.Hard); err != nil {
logs.Warn.Println("replyDelUser: failed to delete user", err, s.sid)
- s.queueOut(decodeStoreError(err, msg.Id, "", msg.Timestamp, nil))
+ s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
@@ -760,14 +773,14 @@ func (pq pendingReceiptsQueue) Swap(i, j int) {
pq[j].index = j
}
-func (pq *pendingReceiptsQueue) Push(x interface{}) {
+func (pq *pendingReceiptsQueue) Push(x any) {
n := len(*pq)
item := x.(*pendingReceipt)
item.index = n
*pq = append(*pq, item)
}
-func (pq *pendingReceiptsQueue) Pop() interface{} {
+func (pq *pendingReceiptsQueue) Pop() any {
old := *pq
n := len(old)
item := old[n-1]
@@ -956,10 +969,12 @@ func usersRequestFromCluster(req *UserCacheReq) {
}
}
+var usersCache map[types.Uid]userCacheEntry
+
// The go routine for processing updates to users cache.
func userUpdater() {
// Caches unread counters and numbers of topics the user's subscribed to.
- usersCache := make(map[types.Uid]userCacheEntry)
+ usersCache = make(map[types.Uid]userCacheEntry)
// Unread counter updates blocked by IO on per user basis. We flush them when the IO completes.
perUserBuffers := make(map[types.Uid][]bufferedUpdate)
@@ -974,10 +989,10 @@ func userUpdater() {
// IO callback queue.
ioDone := make(chan *ioResult, 1024)
- unreadUpdater := func(uids []types.Uid, val int, inc bool) map[types.Uid]int {
+ unreadUpdater := func(uids []types.Uid, vals []int, inc bool) map[types.Uid]int {
var dbPending []types.Uid
counts := make(map[types.Uid]int, len(uids))
- for _, uid := range uids {
+ for i, uid := range uids {
counts[uid] = 0
uce, ok := usersCache[uid]
if !ok {
@@ -986,6 +1001,7 @@ func userUpdater() {
continue
}
+ val := vals[i]
if uce.unread < 0 {
// Unread counter not initialized yet. Maybe start a DB read?
if updateBuf, ioInProgress := perUserBuffers[uid]; ioInProgress {
@@ -1104,19 +1120,19 @@ func userUpdater() {
if upd.PushRcpt != nil {
// List of uids for which the unread count is being read from the DB.
pendingUsers := []types.Uid{}
+
allUids := make([]types.Uid, 0, len(upd.PushRcpt.To))
- for uid := range upd.PushRcpt.To {
+ allDeltas := make([]int, 0, len(upd.PushRcpt.To))
+ for uid, r := range upd.PushRcpt.To {
allUids = append(allUids, uid)
+ delta := 0
+ if r.ShouldIncrementUnreadCountInCache {
+ delta = 1
+ }
+ allDeltas = append(allDeltas, delta)
}
- var delta int
- // Increment unread counter only on msg event.
- if upd.PushRcpt.Payload.What == "msg" {
- delta = 1
- } else {
- delta = 0
- }
- allUnread := unreadUpdater(allUids, delta, true)
+ allUnread := unreadUpdater(allUids, allDeltas, true)
for uid, unread := range allUnread {
rcptTo := upd.PushRcpt.To[uid]
// Handle update
@@ -1185,7 +1201,7 @@ func userUpdater() {
}
// Request to update unread count for one user.
- unreadUpdater([]types.Uid{upd.UserId}, upd.Unread, upd.Inc)
+ unreadUpdater([]types.Uid{upd.UserId}, []int{upd.Unread}, upd.Inc)
}
}
diff --git a/server/utils.go b/server/utils.go
index 896b6b910..c554a48ff 100644
--- a/server/utils.go
+++ b/server/utils.go
@@ -223,20 +223,19 @@ func msgOpts2storeOpts(req *MsgGetOpts) *types.QueryOpt {
}
// Check if the interface contains a string with a single Unicode Del control character.
-func isNullValue(i interface{}) bool {
+func isNullValue(i any) bool {
if str, ok := i.(string); ok {
return str == nullValue
}
return false
}
-func decodeStoreError(err error, id, topic string, ts time.Time,
- params map[string]interface{}) *ServerComMessage {
- return decodeStoreErrorExplicitTs(err, id, topic, ts, ts, params)
+func decodeStoreError(err error, id string, ts time.Time, params map[string]any) *ServerComMessage {
+ return decodeStoreErrorExplicitTs(err, id, "", ts, ts, params)
}
func decodeStoreErrorExplicitTs(err error, id, topic string, serverTs, incomingReqTs time.Time,
- params map[string]interface{}) *ServerComMessage {
+ params map[string]any) *ServerComMessage {
var errmsg *ServerComMessage
@@ -438,7 +437,7 @@ func rewriteTag(orig, countryCode string, withLogin bool) string {
}
// Check if token can be rewritten by any of the validators
- param := map[string]interface{}{"countryCode": countryCode}
+ param := map[string]any{"countryCode": countryCode}
for name, conf := range globals.validators {
if conf.addToTags {
val := store.Store.GetValidator(name)
@@ -761,7 +760,7 @@ func parseTLSConfig(tlsEnabled bool, jsconfig json.RawMessage) (*tls.Config, err
// Merge source interface{} into destination interface.
// If values are maps,deep-merge them. Otherwise shallow-copy.
// Returns dst, true if the dst value was changed.
-func mergeInterfaces(dst, src interface{}) (interface{}, bool) {
+func mergeInterfaces(dst, src any) (any, bool) {
var changed bool
if src == nil {
@@ -771,8 +770,8 @@ func mergeInterfaces(dst, src interface{}) (interface{}, bool) {
vsrc := reflect.ValueOf(src)
switch vsrc.Kind() {
case reflect.Map:
- if xsrc, ok := src.(map[string]interface{}); ok {
- xdst, _ := dst.(map[string]interface{})
+ if xsrc, ok := src.(map[string]any); ok {
+ xdst, _ := dst.(map[string]any)
dst, changed = mergeMaps(xdst, xsrc)
} else {
changed = true
@@ -794,7 +793,7 @@ func mergeInterfaces(dst, src interface{}) (interface{}, bool) {
}
// Deep copy maps.
-func mergeMaps(dst, src map[string]interface{}) (map[string]interface{}, bool) {
+func mergeMaps(dst, src map[string]any) (map[string]any, bool) {
var changed bool
if len(src) == 0 {
@@ -802,16 +801,16 @@ func mergeMaps(dst, src map[string]interface{}) (map[string]interface{}, bool) {
}
if dst == nil {
- dst = make(map[string]interface{})
+ dst = make(map[string]any)
}
for key, val := range src {
xval := reflect.ValueOf(val)
switch xval.Kind() {
case reflect.Map:
- if xsrc, _ := val.(map[string]interface{}); xsrc != nil {
+ if xsrc, _ := val.(map[string]any); xsrc != nil {
// Deep-copy map[string]interface{}
- xdst, _ := dst[key].(map[string]interface{})
+ xdst, _ := dst[key].(map[string]any)
var lchange bool
dst[key], lchange = mergeMaps(xdst, xsrc)
changed = changed || lchange
@@ -839,7 +838,7 @@ func mergeMaps(dst, src map[string]interface{}) (map[string]interface{}, bool) {
}
// netListener creates net.Listener for tcp and unix domains:
-// if addr is is in the form "unix:/run/tinode.sock" it's a unix socket, otherwise TCP host:port.
+// if addr is in the form "unix:/run/tinode.sock" it's a unix socket, otherwise TCP host:port.
func netListener(addr string) (net.Listener, error) {
addrParts := strings.SplitN(addr, ":", 2)
if len(addrParts) == 2 && addrParts[0] == "unix" {
diff --git a/server/utils_test.go b/server/utils_test.go
index 320221bce..345dbb9a4 100644
--- a/server/utils_test.go
+++ b/server/utils_test.go
@@ -27,24 +27,24 @@ func TestStringSliceDelta(t *testing.T) {
// - inputs: old, new
// - expected outputs: added, removed, intersection
cases := [][5][]string{
- [5][]string{
- []string{"abc", "def", "fff"}, []string{},
- []string{}, []string{"abc", "def", "fff"}, []string{},
+ {
+ {"abc", "def", "fff"}, {},
+ {}, {"abc", "def", "fff"}, {},
},
- [5][]string{
- []string{}, []string{}, []string{}, []string{}, []string{},
+ {
+ {}, {}, {}, {}, {},
},
- [5][]string{
- []string{"aa", "xx", "bb", "aa", "bb"}, []string{"yy", "aa"},
- []string{"yy"}, []string{"aa", "bb", "bb", "xx"}, []string{"aa"},
+ {
+ {"aa", "xx", "bb", "aa", "bb"}, {"yy", "aa"},
+ {"yy"}, {"aa", "bb", "bb", "xx"}, {"aa"},
},
- [5][]string{
- []string{"bb", "aa", "bb"}, []string{"yy", "aa", "bb", "zzz", "zzz", "cc"},
- []string{"cc", "yy", "zzz", "zzz"}, []string{"bb"}, []string{"aa", "bb"},
+ {
+ {"bb", "aa", "bb"}, {"yy", "aa", "bb", "zzz", "zzz", "cc"},
+ {"cc", "yy", "zzz", "zzz"}, {"bb"}, {"aa", "bb"},
},
- [5][]string{
- []string{"aa", "aa", "aa"}, []string{"aa", "aa", "aa"},
- []string{}, []string{}, []string{"aa", "aa", "aa"},
+ {
+ {"aa", "aa", "aa"}, {"aa", "aa", "aa"},
+ {}, {}, {"aa", "aa", "aa"},
},
}
diff --git a/server/validate/email/validate.go b/server/validate/email/validate.go
index 2141289e2..1bb8979cf 100644
--- a/server/validate/email/validate.go
+++ b/server/validate/email/validate.go
@@ -16,8 +16,6 @@ import (
"net/mail"
"net/smtp"
"net/url"
- "os"
- "path/filepath"
"strconv"
"strings"
textt "text/template"
@@ -25,6 +23,7 @@ import (
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store"
t "github.com/tinode/chat/server/store/types"
+ "github.com/tinode/chat/server/validate"
i18n "golang.org/x/text/language"
)
@@ -61,6 +60,8 @@ type validator struct {
TLSInsecureSkipVerify bool `json:"insecure_skip_verify"`
// Optional whitelist of email domains accepted for registration.
Domains []string `json:"domains"`
+ // Length of secret numeric code to sent for validation.
+ CodeLength int `json:"code_length"`
// Must use index into language array instead of language tags because language.Matcher is brain damaged:
// https://github.com/golang/go/issues/24211
@@ -69,121 +70,24 @@ type validator struct {
auth smtp.Auth
senderEmail string
langMatcher i18n.Matcher
+ maxCodeValue *big.Int
}
const (
validatorName = "email"
- maxRetries = 4
- defaultPort = "25"
+ defaultMaxRetries = 3
+ defaultPort = "25"
// Technically email could be up to 255 bytes long but practically 128 is enough.
maxEmailLength = 128
- // codeLength = log10(maxCodeValue)
- codeLength = 6
- maxCodeValue = 1000000
-
- // Email template parts
- emailSubject = "subject"
- emailBodyPlain = "body_plain"
- emailBodyHTML = "body_html"
+ // Default code length when one is not provided in the config
+ defaultCodeLength = 6
)
-func resolveTemplatePath(path string) (string, error) {
- if filepath.IsAbs(path) {
- return path, nil
- }
-
- curwd, err := os.Getwd()
- if err != nil {
- return "", err
- }
-
- return filepath.Clean(filepath.Join(curwd, path)), nil
-}
-
-func readTemplateFile(pathTempl *textt.Template, lang string) (*textt.Template, string, error) {
- buffer := bytes.Buffer{}
- err := pathTempl.Execute(&buffer, map[string]interface{}{"Language": lang})
- path := buffer.String()
- if err != nil {
- return nil, path, fmt.Errorf("reading %s: %w", path, err)
- }
-
- templ, err := textt.ParseFiles(path)
- return templ, path, err
-}
-
-// Check if the template contains all required parts.
-func isTemplateValid(templ *textt.Template) error {
- if templ.Lookup(emailSubject) == nil {
- return fmt.Errorf("template invalid: '%s' not found", emailSubject)
- }
- if templ.Lookup(emailBodyPlain) == nil && templ.Lookup(emailBodyHTML) == nil {
- return fmt.Errorf("template invalid: neither of '%s', '%s' is found", emailBodyPlain, emailBodyHTML)
- }
- return nil
-}
-
-type loginAuth struct {
- username, password []byte
-}
-
-// Start begins an authentication with a server. Exported only to satisfy the interface definition.
-func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
- return "LOGIN", []byte(a.username), nil
-}
-
-// Next continues the authentication. Exported only to satisfy the interface definition.
-func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
- if more {
- switch strings.ToLower(string(fromServer)) {
- case "username:":
- return a.username, nil
- case "password:":
- return a.password, nil
- default:
- return nil, fmt.Errorf("LOGIN AUTH unknown server response '%s'", string(fromServer))
- }
- }
- return nil, nil
-}
-
-type emailContent struct {
- subject string
- html string
- plain string
-}
-
-func executeTemplate(template *textt.Template, params map[string]interface{}) (*emailContent, error) {
- var content emailContent
- var err error
-
- buffer := new(bytes.Buffer)
-
- execComponent := func(name string) (string, error) {
- buffer.Reset()
- if templBody := template.Lookup(name); templBody != nil {
- if err := templBody.Execute(buffer, params); err != nil {
- return "", err
- }
- }
- return string(buffer.Bytes()), nil
- }
-
- if content.subject, err = execComponent(emailSubject); err != nil {
- return nil, err
- }
- if content.plain, err = execComponent(emailBodyPlain); err != nil {
- return nil, err
- }
- if content.html, err = execComponent(emailBodyHTML); err != nil {
- return nil, err
- }
-
- return &content, nil
-}
+// Email template parts
+var templateParts = []string{"subject", "body_plain", "body_html"}
// Init: initialize validator.
func (v *validator) Init(jsonconf string) error {
@@ -213,11 +117,11 @@ func (v *validator) Init(jsonconf string) error {
}
// Optionally resolve paths.
- v.ValidationTemplFile, err = resolveTemplatePath(v.ValidationTemplFile)
+ v.ValidationTemplFile, err = validate.ResolveTemplatePath(v.ValidationTemplFile)
if err != nil {
return err
}
- v.ResetTemplFile, err = resolveTemplatePath(v.ResetTemplFile)
+ v.ResetTemplFile, err = validate.ResolveTemplatePath(v.ResetTemplFile)
if err != nil {
return err
}
@@ -245,14 +149,14 @@ func (v *validator) Init(jsonconf string) error {
return err
}
langTags = append(langTags, tag)
- if v.validationTempl[idx], path, err = readTemplateFile(validationPathTempl, lang); err != nil {
+ if v.validationTempl[idx], path, err = validate.ReadTemplateFile(validationPathTempl, lang); err != nil {
return err
}
if err = isTemplateValid(v.validationTempl[idx]); err != nil {
return fmt.Errorf("parsing %s: %w", path, err)
}
- if v.resetTempl[idx], path, err = readTemplateFile(resetPathTempl, lang); err != nil {
+ if v.resetTempl[idx], path, err = validate.ReadTemplateFile(resetPathTempl, lang); err != nil {
return err
}
if err = isTemplateValid(v.resetTempl[idx]); err != nil {
@@ -264,7 +168,7 @@ func (v *validator) Init(jsonconf string) error {
v.validationTempl = make([]*textt.Template, 1)
v.resetTempl = make([]*textt.Template, 1)
// No i18n support. Use defaults.
- v.validationTempl[0], path, err = readTemplateFile(validationPathTempl, "")
+ v.validationTempl[0], path, err = validate.ReadTemplateFile(validationPathTempl, "")
if err != nil {
return err
}
@@ -272,7 +176,7 @@ func (v *validator) Init(jsonconf string) error {
return fmt.Errorf("parsing %s: %w", path, err)
}
- v.resetTempl[0], path, err = readTemplateFile(resetPathTempl, "")
+ v.resetTempl[0], path, err = validate.ReadTemplateFile(resetPathTempl, "")
if err != nil {
return err
}
@@ -281,29 +185,26 @@ func (v *validator) Init(jsonconf string) error {
}
}
- hostUrl, err := url.Parse(v.HostUrl)
- if err != nil {
+ if v.HostUrl, err = validate.ValidateHostURL(v.HostUrl); err != nil {
return err
}
- if !hostUrl.IsAbs() {
- return errors.New("host_url must be absolute")
- }
- if hostUrl.Hostname() == "" {
- return errors.New("invalid host_url")
- }
- if hostUrl.Fragment != "" {
- return errors.New("fragment is not allowed in host_url")
- }
- if hostUrl.Path == "" {
- hostUrl.Path = "/"
- }
- v.HostUrl = hostUrl.String()
+
if v.SMTPHeloHost == "" {
+ hostUrl, _ := url.Parse(v.HostUrl)
v.SMTPHeloHost = hostUrl.Hostname()
}
+ if v.SMTPHeloHost == "" {
+ return errors.New("missing SMTP host")
+ }
+
if v.MaxRetries == 0 {
- v.MaxRetries = maxRetries
+ v.MaxRetries = defaultMaxRetries
+ }
+ if v.CodeLength == 0 {
+ v.CodeLength = defaultCodeLength
}
+ v.maxCodeValue = big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(v.CodeLength)), nil)
+
if v.SMTPPort == "" {
v.SMTPPort = defaultPort
}
@@ -311,6 +212,11 @@ func (v *validator) Init(jsonconf string) error {
return nil
}
+// IsInitialized returns true if the validator is initialized.
+func (v *validator) IsInitialized() bool {
+ return v.SMTPHeloHost != ""
+}
+
// PreCheck validates the credential and parameters without sending an email.
// If the credential is valid, it's returned with an appropriate prefix.
func (v *validator) PreCheck(cred string, _ map[string]interface{}) (string, error) {
@@ -365,12 +271,12 @@ func (v *validator) Request(user t.Uid, email, lang, resp string, tmpToken []byt
base64.StdEncoding.Encode(token, tmpToken)
// Generate expected response as a random numeric string between 0 and 999999.
- code, err := crand.Int(crand.Reader, big.NewInt(maxCodeValue))
+ code, err := crand.Int(crand.Reader, v.maxCodeValue)
if err != nil {
return false, err
}
resp = strconv.FormatInt(code.Int64(), 10)
- resp = strings.Repeat("0", codeLength-len(resp)) + resp
+ resp = strings.Repeat("0", v.CodeLength-len(resp)) + resp
var template *textt.Template
if v.langMatcher != nil {
@@ -380,7 +286,7 @@ func (v *validator) Request(user t.Uid, email, lang, resp string, tmpToken []byt
template = v.validationTempl[0]
}
- content, err := executeTemplate(template, map[string]interface{}{
+ content, err := validate.ExecuteTemplate(template, templateParts, map[string]interface{}{
"Token": url.QueryEscape(string(token)),
"Code": resp,
"HostUrl": v.HostUrl})
@@ -426,7 +332,7 @@ func (v *validator) ResetSecret(email, scheme, lang string, tmpToken []byte, par
login = params["login"].(string)
}
- content, err := executeTemplate(template, map[string]interface{}{
+ content, err := validate.ExecuteTemplate(template, templateParts, map[string]interface{}{
"Login": login,
"Token": url.QueryEscape(string(token)),
"Scheme": scheme,
@@ -539,7 +445,7 @@ func (v *validator) sendMail(rcpt []string, msg []byte) error {
// https://docs.aws.amazon.com/sdk-for-go/api/service/ses/#example_SES_SendEmail_shared00
// -
// Mailjet and SendGrid have some free email limits.
-func (v *validator) send(to string, content *emailContent) error {
+func (v *validator) send(to string, content map[string]string) error {
message := &bytes.Buffer{}
// Common headers.
@@ -548,23 +454,23 @@ func (v *validator) send(to string, content *emailContent) error {
message.WriteString("Subject: ")
// Old email clients may barf on UTF-8 strings.
// Encode as quoted printable with 75-char strings separated by spaces, split by spaces, reassemble.
- message.WriteString(strings.Join(strings.Split(mime.QEncoding.Encode("utf-8", content.subject), " "), "\r\n "))
+ message.WriteString(strings.Join(strings.Split(mime.QEncoding.Encode("utf-8", content["subject"]), " "), "\r\n "))
message.WriteString("\r\n")
message.WriteString("MIME-version: 1.0;\r\n")
- if content.html == "" {
+ if content["body_html"] == "" {
// Plain text message
message.WriteString("Content-Type: text/plain; charset=\"UTF-8\"; format=flowed; delsp=yes\r\n")
message.WriteString("Content-Transfer-Encoding: base64\r\n\r\n")
b64w := base64.NewEncoder(base64.StdEncoding, message)
- b64w.Write([]byte(content.plain))
+ b64w.Write([]byte(content["body_plain"]))
b64w.Close()
- } else if content.plain == "" {
+ } else if content["body_plain"] == "" {
// HTML-formatted message
message.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n")
message.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
qpw := qp.NewWriter(message)
- qpw.Write([]byte(content.html))
+ qpw.Write([]byte(content["body_html"]))
qpw.Close()
} else {
// Multipart-alternative message includes both HTML and plain text components.
@@ -575,7 +481,7 @@ func (v *validator) send(to string, content *emailContent) error {
message.WriteString("Content-Type: text/plain; charset=\"UTF-8\"; format=flowed; delsp=yes\r\n")
message.WriteString("Content-Transfer-Encoding: base64\r\n\r\n")
b64w := base64.NewEncoder(base64.StdEncoding, message)
- b64w.Write([]byte(content.plain))
+ b64w.Write([]byte(content["body_plain"]))
b64w.Close()
message.WriteString("\r\n")
@@ -584,7 +490,7 @@ func (v *validator) send(to string, content *emailContent) error {
message.WriteString("Content-Type: text/html; charset=\"UTF-8\"\r\n")
message.WriteString("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
qpw := qp.NewWriter(message)
- qpw.Write([]byte(content.html))
+ qpw.Write([]byte(content["body_html"]))
qpw.Close()
message.WriteString("\r\n--" + boundary + "--")
@@ -600,6 +506,41 @@ func (v *validator) send(to string, content *emailContent) error {
return err
}
+// Check if the template contains all required parts.
+func isTemplateValid(templ *textt.Template) error {
+ if templ.Lookup("subject") == nil {
+ return fmt.Errorf("template invalid: '%s' not found", "subject")
+ }
+ if templ.Lookup("body_plain") == nil && templ.Lookup("body_html") == nil {
+ return fmt.Errorf("template invalid: neither of '%s', '%s' is found", "body_plain", "body_html")
+ }
+ return nil
+}
+
+type loginAuth struct {
+ username, password []byte
+}
+
+// Start begins an authentication with a server. Exported only to satisfy the interface definition.
+func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+ return "LOGIN", []byte(a.username), nil
+}
+
+// Next continues the authentication. Exported only to satisfy the interface definition.
+func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+ if more {
+ switch strings.ToLower(string(fromServer)) {
+ case "username:":
+ return a.username, nil
+ case "password:":
+ return a.password, nil
+ default:
+ return nil, fmt.Errorf("LOGIN AUTH unknown server response '%s'", string(fromServer))
+ }
+ }
+ return nil, nil
+}
+
func randomBoundary() string {
var buf [24]byte
rand.Read(buf[:])
diff --git a/server/validate/tel/validate.go b/server/validate/tel/validate.go
index 1902cbdfc..78bc34c06 100644
--- a/server/validate/tel/validate.go
+++ b/server/validate/tel/validate.go
@@ -2,58 +2,207 @@
package tel
import (
+ "crypto/rand"
+ "encoding/json"
+ "math/big"
+ "strconv"
+ "strings"
+ textt "text/template"
+
"github.com/nyaruka/phonenumbers"
+ "github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store"
t "github.com/tinode/chat/server/store/types"
+ "github.com/tinode/chat/server/validate"
+ i18n "golang.org/x/text/language"
)
-const validatorName = "tel"
-
// Empty placeholder struct.
type validator struct {
+ // Base URL of the web client to tell clients.
+ HostUrl string `json:"host_url"`
+ // List of languages supported by templates.
+ Languages []string `json:"languages"`
+ // Path to email validation and password reset templates, either a template itself or a literal string.
+ UniversalTemplFile string `json:"universal_templ"`
+ // Sender address.
+ Sender string `json:"sender"`
+ // Debug response to accept during testing.
DebugResponse string `json:"debug_response"`
- MaxRetries int `json:"max_retries"`
+ // Maximum number of validation retires.
+ MaxRetries int `json:"max_retries"`
+ // Length of secret numeric code to sent for validation.
+ CodeLength int `json:"code_length"`
+
+ // Must use index into language array instead of language tags because language.Matcher is brain damaged:
+ // https://github.com/golang/go/issues/24211
+ universalTempl []*textt.Template
+ langMatcher i18n.Matcher
+ maxCodeValue *big.Int
}
-// Init is a noop.
+const (
+ validatorName = "tel"
+
+ defaultMaxRetries = 3
+
+ // Default code length when one is not provided in the config
+ defaultCodeLength = 6
+
+ defaultSender = "Tinode"
+)
+
func (v *validator) Init(jsonconf string) error {
- // Implement: Parse config and initialize SMS service.
+ var err error
+
+ if err = json.Unmarshal([]byte(jsonconf), v); err != nil {
+ return err
+ }
- v.MaxRetries = 1000
- v.DebugResponse = "123456"
+ if v.HostUrl, err = validate.ValidateHostURL(v.HostUrl); err != nil {
+ return err
+ }
+
+ var universalPathTempl *textt.Template
+ universalPathTempl, err = textt.New("universal").Parse(v.UniversalTemplFile)
+ if err != nil {
+ return err
+ }
+
+ if len(v.Languages) > 0 {
+ v.universalTempl = make([]*textt.Template, len(v.Languages))
+ var langTags []i18n.Tag
+ // Find actual content templates for each defined language.
+ for idx, lang := range v.Languages {
+ tag, err := i18n.Parse(lang)
+ if err != nil {
+ return err
+ }
+ langTags = append(langTags, tag)
+ if v.universalTempl[idx], _, err = validate.ReadTemplateFile(universalPathTempl, lang); err != nil {
+ return err
+ }
+ }
+ v.langMatcher = i18n.NewMatcher(langTags)
+ } else {
+ v.universalTempl = make([]*textt.Template, 1)
+ // No i18n support. Use defaults.
+ v.universalTempl[0], _, err = validate.ReadTemplateFile(universalPathTempl, "")
+ if err != nil {
+ return err
+ }
+ }
+
+ if v.Sender == "" {
+ v.Sender = defaultSender
+ }
+ if v.MaxRetries == 0 {
+ v.MaxRetries = defaultMaxRetries
+ }
+ if v.CodeLength == 0 {
+ v.CodeLength = defaultCodeLength
+ }
+ v.maxCodeValue = big.NewInt(0).Exp(big.NewInt(10), big.NewInt(int64(v.CodeLength)), nil)
return nil
}
+// IsInitialized returns true if the validator is initialized.
+func (v *validator) IsInitialized() bool {
+ return v.CodeLength > 0
+}
+
// PreCheck validates the credential and parameters without sending an SMS or making the call.
-// If credential is valid it's formatted and prefixed with a tag namespace.
+// If credential is valid, it's formatted and prefixed with a tag namespace.
func (*validator) PreCheck(cred string, params map[string]interface{}) (string, error) {
+ // Parse will try to extract the number from any text, make sure it's just the number.
+ if !phonenumbers.VALID_PHONE_NUMBER_PATTERN.MatchString(cred) {
+ return "", t.ErrMalformed
+ }
countryCode, ok := params["countryCode"].(string)
if !ok {
countryCode = "US"
}
-
- // Libphonenumber is broken by design: Parse will try to extract the number from any text.
- if phonenumbers.VALID_PHONE_NUMBER_PATTERN.MatchString(cred) {
- if num, err := phonenumbers.Parse(cred, countryCode); err == nil {
- // It's a phone number. Not checking the length because phone numbers cannot be that long.
- if phonenumbers.IsValidNumber(num) {
- return validatorName + ":" + phonenumbers.Format(num, phonenumbers.E164), nil
- }
- }
+ number, err := phonenumbers.Parse(cred, countryCode)
+ if err != nil {
+ return "", t.ErrMalformed
}
- return "", t.ErrMalformed
+ if !phonenumbers.IsValidNumber(number) {
+ return "", t.ErrMalformed
+ }
+ if numType := phonenumbers.GetNumberType(number); numType != phonenumbers.FIXED_LINE_OR_MOBILE &&
+ numType != phonenumbers.MOBILE {
+ return "", t.ErrMalformed
+ }
+ return validatorName + ":" + phonenumbers.Format(number, phonenumbers.E164), nil
}
-// Request sends a request for confirmation to the user: makes a record in DB and nothing else.
-func (*validator) Request(user t.Uid, cred, lang, resp string, tmpToken []byte) (bool, error) {
- // TODO: actually send a validation SMS or make a call to the provided `cred` here.
- return true, nil
+// Request sends a request for confirmation to the user: makes a record in DB and nothing else.
+func (v *validator) Request(user t.Uid, phone, lang, resp string, tmpToken []byte) (bool, error) {
+ // Phone validator cannot accept an immediate response.
+ if resp != "" {
+ return false, t.ErrFailed
+ }
+
+ // Generate expected response as a random numeric string between 0 and 999999.
+ code, err := rand.Int(rand.Reader, v.maxCodeValue)
+ if err != nil {
+ return false, err
+ }
+ resp = strconv.FormatInt(code.Int64(), 10)
+ resp = strings.Repeat("0", v.CodeLength-len(resp)) + resp
+
+ var template *textt.Template
+ if v.langMatcher != nil {
+ _, idx := i18n.MatchStrings(v.langMatcher, lang)
+ template = v.universalTempl[idx]
+ } else {
+ template = v.universalTempl[0]
+ }
+
+ content, err := validate.ExecuteTemplate(template, nil, map[string]interface{}{
+ "Code": resp,
+ "HostUrl": v.HostUrl})
+ if err != nil {
+ return false, err
+ }
+
+ // Create or update validation record in DB.
+ isNew, err := store.Users.UpsertCred(&t.Credential{
+ User: user.String(),
+ Method: validatorName,
+ Value: phone,
+ Resp: resp})
+ if err != nil {
+ return false, err
+ }
+
+ // Send SMS without blocking. It sending may take long time.
+ go v.send(phone, content[""])
+
+ return isNew, nil
}
// ResetSecret sends a message with instructions for resetting an authentication secret.
-func (*validator) ResetSecret(cred, scheme, lang string, tmpToken []byte, params map[string]interface{}) error {
- // TODO: send SMS with rest instructions.
+func (v *validator) ResetSecret(phone, scheme, lang string, code []byte, params map[string]interface{}) error {
+ var template *textt.Template
+ if v.langMatcher != nil {
+ _, idx := i18n.MatchStrings(v.langMatcher, lang)
+ template = v.universalTempl[idx]
+ } else {
+ template = v.universalTempl[0]
+ }
+
+ content, err := validate.ExecuteTemplate(template, nil, map[string]interface{}{
+ "Code": string(code),
+ "HostUrl": v.HostUrl})
+ if err != nil {
+ return err
+ }
+
+ // Send SMS without blocking. It sending may take long time.
+ go v.send(phone, content[""])
+
return nil
}
@@ -65,7 +214,7 @@ func (v *validator) Check(user t.Uid, resp string) (string, error) {
}
if cred == nil {
- // Request to validate non-existent credential.
+ // Blank credential.
return "", t.ErrNotFound
}
@@ -94,13 +243,14 @@ func (*validator) Delete(user t.Uid) error {
return store.Users.DelCred(user, validatorName, "")
}
-// Remove or disable the given record
+// Remove or disable the given record.
func (*validator) Remove(user t.Uid, value string) error {
return store.Users.DelCred(user, validatorName, value)
}
-// Implement sending a text message
+// Implement sending the SMS.
func (*validator) send(to, body string) error {
+ logs.Info.Println("Send SMS, To:", to, "; Text:", body)
return nil
}
diff --git a/server/validate/validator.go b/server/validate/validator.go
index 58485bb8c..e2c7c89f3 100644
--- a/server/validate/validator.go
+++ b/server/validate/validator.go
@@ -2,6 +2,14 @@
package validate
import (
+ "bytes"
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "text/template"
+
t "github.com/tinode/chat/server/store/types"
)
@@ -10,6 +18,9 @@ type Validator interface {
// Init initializes the validator.
Init(jsonconf string) error
+ // IsInitialized returns true if the validator is initialized.
+ IsInitialized() bool
+
// PreCheck pre-validates the credential without sending an actual request for validation:
// check uniqueness (if appropriate), format, etc
// Returns normalized credential prefixed with an appropriate namespace prefix.
@@ -42,3 +53,72 @@ type Validator interface {
// Delete deletes user's record.
Delete(user t.Uid) error
}
+
+func ValidateHostURL(origUrl string) (string, error) {
+ hostUrl, err := url.Parse(origUrl)
+ if err != nil {
+ return "", err
+ }
+ if !hostUrl.IsAbs() {
+ return "", errors.New("host_url must be absolute")
+ }
+ if hostUrl.Hostname() == "" {
+ return "", errors.New("invalid host_url")
+ }
+ if hostUrl.Fragment != "" {
+ return "", errors.New("fragment is not allowed in host_url")
+ }
+ if hostUrl.Path == "" {
+ hostUrl.Path = "/"
+ }
+ return hostUrl.String(), nil
+}
+
+func ExecuteTemplate(template *template.Template, parts []string, params map[string]interface{}) (map[string]string, error) {
+ content := map[string]string{}
+ buffer := new(bytes.Buffer)
+
+ if parts == nil {
+ if err := template.Execute(buffer, params); err != nil {
+ return nil, err
+ }
+ content[""] = buffer.String()
+ } else {
+ for _, part := range parts {
+ buffer.Reset()
+ if templBody := template.Lookup(part); templBody != nil {
+ if err := templBody.Execute(buffer, params); err != nil {
+ return nil, err
+ }
+ }
+ content[part] = buffer.String()
+ }
+ }
+
+ return content, nil
+}
+
+func ResolveTemplatePath(path string) (string, error) {
+ if filepath.IsAbs(path) {
+ return path, nil
+ }
+
+ curwd, err := os.Getwd()
+ if err != nil {
+ return "", err
+ }
+
+ return filepath.Clean(filepath.Join(curwd, path)), nil
+}
+
+func ReadTemplateFile(pathTempl *template.Template, lang string) (*template.Template, string, error) {
+ buffer := bytes.Buffer{}
+ err := pathTempl.Execute(&buffer, map[string]interface{}{"Language": lang})
+ path := buffer.String()
+ if err != nil {
+ return nil, path, fmt.Errorf("reading %s: %w", path, err)
+ }
+
+ templ, err := template.ParseFiles(path)
+ return templ, path, err
+}
diff --git a/tinode-db/README.md b/tinode-db/README.md
index 6eda9fb73..b0cdb1461 100644
--- a/tinode-db/README.md
+++ b/tinode-db/README.md
@@ -13,6 +13,9 @@ This utility initializes the `tinode` database (or upgrades an existing DB from
- **MongoDB**
`go build -tags mongodb` or `go build -i -tags mongodb` to automatically install missing dependencies.
+ - **PostgreSQL**
+ `go build -tags postgres` or `go build -i -tags postgres` to automatically install missing dependencies.
+
## Run
@@ -48,3 +51,4 @@ Avatar photos curtesy of https://www.pexels.com/ under [CC0 license](https://www
* [RethinkDB schema](https://github.com/tinode/chat/tree/master/server/db/rethinkdb/schema.md)
* [MySQL schema](https://github.com/tinode/chat/tree/master/server/db/mysql/schema.sql)
* [MongoDB schema](https://github.com/tinode/chat/tree/master/server/db/mongodb/schema.md)
+* [PostgreSQL schema](https://github.com/tinode/chat/tree/master/server/db/postgres/schema.sql)
diff --git a/tinode-db/gendb.go b/tinode-db/gendb.go
index 1e1e10f94..0f0fb045a 100644
--- a/tinode-db/gendb.go
+++ b/tinode-db/gendb.go
@@ -327,7 +327,7 @@ func genDb(data *Data) {
if timestamp.After(now) {
now = timestamp
}
- if err = store.Messages.Save(&types.Message{
+ if err, _ = store.Messages.Save(&types.Message{
ObjHeader: types.ObjHeader{CreatedAt: timestamp},
SeqId: seqId,
Topic: topic,
@@ -349,7 +349,7 @@ func genDb(data *Data) {
for _, gt := range data.Grouptopics {
seqIds[nameIndex[gt.Name]] = 1
- if err = store.Messages.Save(&types.Message{
+ if err, _ = store.Messages.Save(&types.Message{
ObjHeader: types.ObjHeader{CreatedAt: now},
SeqId: 1,
Topic: nameIndex[gt.Name],
@@ -368,7 +368,7 @@ func genDb(data *Data) {
}
usedp2p[nameIndex[sub.pair]] = true
seqIds[nameIndex[sub.pair]] = 1
- if err = store.Messages.Save(&types.Message{
+ if err, _ = store.Messages.Save(&types.Message{
ObjHeader: types.ObjHeader{CreatedAt: now},
SeqId: 1,
Topic: nameIndex[sub.pair],
@@ -391,7 +391,7 @@ func genDb(data *Data) {
seqIds[nameIndex[sub.pair]]++
seqId := seqIds[nameIndex[sub.pair]]
if sub.Users[0].Name == botAccount || sub.Users[1].Name == botAccount {
- if err = store.Messages.Save(&types.Message{
+ if err, _ = store.Messages.Save(&types.Message{
ObjHeader: types.ObjHeader{CreatedAt: ts},
SeqId: seqId,
Topic: nameIndex[sub.pair],
@@ -406,7 +406,7 @@ func genDb(data *Data) {
}
}
- log.Println("All done.")
+ log.Println("Sample data processing completed.")
}
// Go json cannot unmarshal Duration from a string, thus this hack.
diff --git a/tinode-db/main.go b/tinode-db/main.go
index 6be833f64..1e1664c4b 100644
--- a/tinode-db/main.go
+++ b/tinode-db/main.go
@@ -16,6 +16,7 @@ import (
"github.com/tinode/chat/server/auth"
_ "github.com/tinode/chat/server/db/mongodb"
_ "github.com/tinode/chat/server/db/mysql"
+ _ "github.com/tinode/chat/server/db/postgres"
_ "github.com/tinode/chat/server/db/rethinkdb"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
@@ -241,25 +242,43 @@ func main() {
defer store.Store.Close()
adapterVersion := store.Store.GetAdapterVersion()
- databaseVersion := store.Store.GetDbVersion()
+ databaseVersion := 0
+ if store.Store.IsOpen() {
+ databaseVersion = store.Store.GetDbVersion()
+ }
log.Printf("Database adapter: '%s'; version: %d", store.Store.GetAdapterName(), adapterVersion)
+ var created bool
+
if err != nil {
if strings.Contains(err.Error(), "Database not initialized") {
if *noInit {
log.Fatalln("Database not found.")
}
log.Println("Database not found. Creating.")
+ err = store.Store.InitDb(config.StoreConfig, false)
+ if err == nil {
+ log.Println("Database successfully created.")
+ created = true
+ }
} else if strings.Contains(err.Error(), "Invalid database version") {
msg := "Wrong DB version: expected " + strconv.Itoa(adapterVersion) + ", got " +
strconv.Itoa(databaseVersion) + "."
if *reset {
- log.Println(msg, "Dropping and recreating the database.")
+ log.Println(msg, "Reset Requested. Dropping and recreating the database.")
+ err = store.Store.InitDb(config.StoreConfig, true)
+ if err == nil {
+ log.Println("Database successfully reset.")
+ }
} else if *upgrade {
if databaseVersion > adapterVersion {
log.Fatalln(msg, "Unable to upgrade: database has greater version than the adapter.")
}
log.Println(msg, "Upgrading the database.")
+ err = store.Store.UpgradeDb(config.StoreConfig)
+ if err == nil {
+ log.Println("Database successfully upgraded.")
+ }
} else {
log.Fatalln(msg, "Use --reset to reset, --upgrade to upgrade.")
}
@@ -267,37 +286,20 @@ func main() {
log.Fatalln("Failed to init DB adapter:", err)
}
} else if *reset {
- log.Println("Database reset requested")
- } else {
- log.Println("Database exists, DB version is correct. All done.")
- os.Exit(0)
- }
-
- if *upgrade {
- // Upgrade DB from one version to another.
- err = store.Store.UpgradeDb(config.StoreConfig)
- if err == nil {
- log.Println("Database successfully upgraded.")
- }
- } else {
- // Reset or create DB
+ log.Println("Reset requested. Dropping and recreating the database.")
err = store.Store.InitDb(config.StoreConfig, true)
if err == nil {
- var action string
- if *reset {
- action = "reset"
- } else {
- action = "initialized"
- }
- log.Println("Database", action)
+ log.Println("Database successfully reset.")
}
+ } else {
+ log.Println("Database exists, version is correct.")
}
if err != nil {
- log.Fatalln("Failed to init DB:", err)
+ log.Fatalln("Failure:", err)
}
- if !*upgrade {
+ if *reset || created {
genDb(&data)
} else if len(data.Users) > 0 {
log.Println("Sample data ignored.")
diff --git a/tinode-db/tinode.conf b/tinode-db/tinode.conf
index 80de0ae8a..7394cd205 100644
--- a/tinode-db/tinode.conf
+++ b/tinode-db/tinode.conf
@@ -3,6 +3,10 @@
"uid_key": "la6YsO+bNX/+XIkOqc5Svw==",
"use_adapter": "",
"adapters": {
+ "postgres": {
+ "database": "tinode",
+ "dsn": "postgresql://postgres:postgres@localhost:5432/tinode?sslmode=disable"
+ },
"mysql": {
"database": "tinode",
"dsn": "root@tcp(localhost)/tinode?parseTime=true&collation=utf8mb4_unicode_ci"
@@ -14,7 +18,7 @@
"mongodb": {
"database": "tinode",
"addresses": "localhost:27017",
- "replica_set": "rs0",
+ //"replica_set": "rs0",
//"auth_source": "admin",
//"username": "tinode",
//"password": "tinode",