From ad8cf73373068d704dfeee643da44eede63660f9 Mon Sep 17 00:00:00 2001 From: AVVS Date: Wed, 3 May 2023 22:00:45 -0700 Subject: [PATCH] feat: upgrade to main-0.22 --- INSTALL.md | 22 +- README.md | 17 +- build-all.sh | 4 +- docker-build.sh | 4 +- docker-release.sh | 2 +- docker/README.md | 39 +- docker/docker-compose/README.md | 16 +- docker/docker-compose/cluster.mongodb.yml | 2 +- docker/docker-compose/cluster.postgres.yml | 24 + docker/docker-compose/cluster.rethinkdb.yml | 4 +- docker/docker-compose/cluster.yml | 2 +- .../single-instance.mongodb.yml | 2 +- .../single-instance.postgres.yml | 16 + .../single-instance.rethinkdb.yml | 2 +- docker/docker-compose/single-instance.yml | 2 +- docker/tinode/Dockerfile | 14 +- docker/tinode/config.template | 4 + docs/API.md | 12 +- docs/drafty.md | 3 +- docs/faq.md | 6 +- go.mod | 85 +- go.sum | 854 ++-- monitoring/exporter/main.go | 11 +- pbx/model.pb.go | 1025 ++--- pbx/model.proto | 9 +- pbx/model_grpc.pb.go | 43 +- server/auth/auth.go | 2 + server/auth/code/auth_code.go | 209 + server/auth/token/auth_token.go | 2 +- server/calls.go | 74 +- server/cluster.go | 12 +- server/datamodel.go | 69 +- server/db/adapter.go | 19 +- server/db/mongodb/adapter.go | 95 +- server/db/mongodb/tests/mongo_test.go | 4 +- server/db/mysql/adapter.go | 134 +- server/db/mysql/schema.sql | 35 +- server/db/postgres/adapter.go | 3541 +++++++++++++++++ server/db/postgres/blank.go | 8 + server/db/postgres/schema.sql | 2 + server/db/rethinkdb/adapter.go | 98 +- server/drafty/drafty.go | 2 +- server/hdl_files.go | 93 +- server/hdl_grpc.go | 13 +- server/hdl_longpoll.go | 8 +- server/hdl_websock.go | 9 +- server/http.go | 34 +- server/hub.go | 21 +- server/init_topic.go | 2 - server/main.go | 48 +- server/media/media.go | 39 +- server/media/s3/s3.go | 12 +- server/pbconverter.go | 71 +- server/plugins.go | 9 +- server/pres.go | 2 +- server/push.go | 5 +- server/push/push.go | 2 + server/push/tnpg/README.md | 2 +- server/session.go | 83 +- server/session_test.go | 64 +- server/sessionstore.go | 8 +- server/stats.go | 6 +- server/store/mock_store/mock_store.go | 93 +- server/store/store.go | 67 +- server/store/types/types.go | 31 +- server/templ/email-password-reset-ru.templ | 2 + server/templ/email-password-reset-vi.templ | 2 +- server/templ/email-validation-vi.templ | 13 +- server/templ/sms-universal-en.templ | 8 + server/templ/sms-universal-es.templ | 8 + server/templ/sms-universal-fr.templ | 8 + server/templ/sms-universal-pt.templ | 8 + server/templ/sms-universal-ru.templ | 8 + server/templ/sms-universal-vi.templ | 8 + server/templ/sms-universal-zh.templ | 8 + server/templ/sms-validation-es.templ | 1 - server/templ/sms-validation-vi.templ | 1 - server/templ/sms-validation-zh.templ | 1 - server/templ/sms-validation.templ | 1 - server/tinode.conf | 73 +- server/topic.go | 105 +- server/topic_test.go | 26 +- server/user.go | 118 +- server/utils.go | 27 +- server/utils_test.go | 28 +- server/validate/email/validate.go | 221 +- server/validate/tel/validate.go | 204 +- server/validate/validator.go | 80 + tinode-db/README.md | 4 + tinode-db/gendb.go | 10 +- tinode-db/main.go | 52 +- tinode-db/tinode.conf | 6 +- 92 files changed, 6372 insertions(+), 1911 deletions(-) create mode 100644 docker/docker-compose/cluster.postgres.yml create mode 100644 docker/docker-compose/single-instance.postgres.yml create mode 100644 server/auth/code/auth_code.go create mode 100644 server/db/postgres/adapter.go create mode 100644 server/db/postgres/blank.go create mode 100644 server/db/postgres/schema.sql create mode 100644 server/templ/sms-universal-en.templ create mode 100644 server/templ/sms-universal-es.templ create mode 100644 server/templ/sms-universal-fr.templ create mode 100644 server/templ/sms-universal-pt.templ create mode 100644 server/templ/sms-universal-ru.templ create mode 100644 server/templ/sms-universal-vi.templ create mode 100644 server/templ/sms-universal-zh.templ delete mode 100644 server/templ/sms-validation-es.templ delete mode 100644 server/templ/sms-validation-vi.templ delete mode 100644 server/templ/sms-validation-zh.templ delete mode 100644 server/templ/sms-validation.templ 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 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 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",