From dfdfaa5afd7651c4113f4c38825ff61786e52f0b Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Tue, 20 Dec 2022 11:35:59 -0500 Subject: [PATCH 01/10] copy in comms code --- comms/.dockerignore | 5 + comms/.editorconfig | 15 + comms/.gitignore | 6 + comms/.vscode/settings.json | 4 + comms/Dockerfile | 25 + comms/Makefile | 76 + comms/README.md | 57 + comms/config/config.go | 150 + comms/config/constants.go | 7 + comms/config/env_staging.go | 5 + comms/config/env_standalone.go | 6 + comms/config/get_ip.go | 28 + comms/db/conn.go | 50 + comms/db/db.go | 31 + comms/db/docker-initdb/00_schema.sql | 4055 +++++++++++++++++ comms/db/docker-initdb/01_changes.sql | 7 + comms/db/docker-initdb/02_dbs.sql | 4 + .../20221128164712_create_rpc_log_table.sql | 12 + .../20221130042018_create_pubkey_table.sql | 8 + .../20221130171438_drop_pubkey_table.sql | 5 + .../migrations/20221202144236_create_chat.sql | 46 + ...20221210021301_create_chat_permissions.sql | 9 + ...221212074228_create_chat_blocked_users.sql | 11 + ...12081249_create_chat_message_reactions.sql | 12 + comms/db/models.go | 979 ++++ comms/db/queries/get_chat_blocked_users.go | 37 + comms/db/queries/get_chat_members.go | 46 + comms/db/queries/get_chat_messages.go | 76 + comms/db/queries/get_chats.go | 61 + comms/db/queries/get_summaries.go | 66 + comms/db/queries/get_users.go | 17 + comms/db/query.sql | 7 + comms/db/query.sql.go | 65 + comms/db/sqlx_db.go | 34 + comms/docker-compose.yml | 85 + comms/em/abi.go | 193 + comms/em/chain_test.go | 114 + comms/em/cid.go | 47 + comms/em/em.go | 77 + comms/em/metadata.go | 88 + comms/em/nats_test.go | 90 + comms/em/processor.go | 31 + comms/example/deno_client.ts | 364 ++ comms/go.mod | 66 + comms/go.sum | 313 ++ comms/internal/pubkeystore/abi.go | 88 + comms/internal/pubkeystore/recover.go | 178 + .../internal/pubkeystore/recover_add_user.go | 114 + .../pubkeystore/recover_entity_manager.go | 131 + comms/internal/pubkeystore/recover_test.go | 53 + comms/internal/rpcz/apply.go | 165 + comms/internal/rpcz/chat.go | 97 + comms/internal/rpcz/chat_block_test.go | 102 + comms/internal/rpcz/chat_delete_test.go | 65 + comms/internal/rpcz/chat_permissions_test.go | 54 + comms/internal/rpcz/chat_test.go | 133 + comms/internal/rpcz/test_utils.go | 22 + comms/internal/rpcz/validator.go | 258 ++ comms/main.go | 52 + comms/misc/hashids_util.go | 25 + comms/misc/pretty_print.go | 15 + comms/misc/recover_wallet.go | 28 + comms/peering/info.go | 30 + comms/peering/info_test.go | 21 + comms/peering/nats.go | 239 + comms/peering/solicit.go | 131 + comms/peering/sps.go | 130 + comms/peering/sps_test.go | 39 + comms/schema/raw_rpc.go | 11 + comms/schema/schema.go | 237 + comms/schema/schema.ts | 157 + comms/server/response_mapper.go | 75 + comms/server/server.go | 363 ++ comms/server/server_test.go | 56 + comms/sqlc.yaml | 25 + 75 files changed, 10584 insertions(+) create mode 100644 comms/.dockerignore create mode 100644 comms/.editorconfig create mode 100644 comms/.gitignore create mode 100644 comms/.vscode/settings.json create mode 100644 comms/Dockerfile create mode 100644 comms/Makefile create mode 100644 comms/README.md create mode 100644 comms/config/config.go create mode 100644 comms/config/constants.go create mode 100644 comms/config/env_staging.go create mode 100644 comms/config/env_standalone.go create mode 100644 comms/config/get_ip.go create mode 100644 comms/db/conn.go create mode 100644 comms/db/db.go create mode 100644 comms/db/docker-initdb/00_schema.sql create mode 100644 comms/db/docker-initdb/01_changes.sql create mode 100644 comms/db/docker-initdb/02_dbs.sql create mode 100644 comms/db/migrations/20221128164712_create_rpc_log_table.sql create mode 100644 comms/db/migrations/20221130042018_create_pubkey_table.sql create mode 100644 comms/db/migrations/20221130171438_drop_pubkey_table.sql create mode 100644 comms/db/migrations/20221202144236_create_chat.sql create mode 100644 comms/db/migrations/20221210021301_create_chat_permissions.sql create mode 100644 comms/db/migrations/20221212074228_create_chat_blocked_users.sql create mode 100644 comms/db/migrations/20221212081249_create_chat_message_reactions.sql create mode 100644 comms/db/models.go create mode 100644 comms/db/queries/get_chat_blocked_users.go create mode 100644 comms/db/queries/get_chat_members.go create mode 100644 comms/db/queries/get_chat_messages.go create mode 100644 comms/db/queries/get_chats.go create mode 100644 comms/db/queries/get_summaries.go create mode 100644 comms/db/queries/get_users.go create mode 100644 comms/db/query.sql create mode 100644 comms/db/query.sql.go create mode 100644 comms/db/sqlx_db.go create mode 100644 comms/docker-compose.yml create mode 100644 comms/em/abi.go create mode 100644 comms/em/chain_test.go create mode 100644 comms/em/cid.go create mode 100644 comms/em/em.go create mode 100644 comms/em/metadata.go create mode 100644 comms/em/nats_test.go create mode 100644 comms/em/processor.go create mode 100644 comms/example/deno_client.ts create mode 100644 comms/go.mod create mode 100644 comms/go.sum create mode 100644 comms/internal/pubkeystore/abi.go create mode 100644 comms/internal/pubkeystore/recover.go create mode 100644 comms/internal/pubkeystore/recover_add_user.go create mode 100644 comms/internal/pubkeystore/recover_entity_manager.go create mode 100644 comms/internal/pubkeystore/recover_test.go create mode 100644 comms/internal/rpcz/apply.go create mode 100644 comms/internal/rpcz/chat.go create mode 100644 comms/internal/rpcz/chat_block_test.go create mode 100644 comms/internal/rpcz/chat_delete_test.go create mode 100644 comms/internal/rpcz/chat_permissions_test.go create mode 100644 comms/internal/rpcz/chat_test.go create mode 100644 comms/internal/rpcz/test_utils.go create mode 100644 comms/internal/rpcz/validator.go create mode 100644 comms/main.go create mode 100644 comms/misc/hashids_util.go create mode 100644 comms/misc/pretty_print.go create mode 100644 comms/misc/recover_wallet.go create mode 100644 comms/peering/info.go create mode 100644 comms/peering/info_test.go create mode 100644 comms/peering/nats.go create mode 100644 comms/peering/solicit.go create mode 100644 comms/peering/sps.go create mode 100644 comms/peering/sps_test.go create mode 100644 comms/schema/raw_rpc.go create mode 100644 comms/schema/schema.go create mode 100644 comms/schema/schema.ts create mode 100644 comms/server/response_mapper.go create mode 100644 comms/server/server.go create mode 100644 comms/server/server_test.go create mode 100644 comms/sqlc.yaml diff --git a/comms/.dockerignore b/comms/.dockerignore new file mode 100644 index 00000000000..bd92e369f63 --- /dev/null +++ b/comms/.dockerignore @@ -0,0 +1,5 @@ +Dockerfile +docker-compose.yml +comms-linux +wip +.vscode diff --git a/comms/.editorconfig b/comms/.editorconfig new file mode 100644 index 00000000000..ead9eacbd87 --- /dev/null +++ b/comms/.editorconfig @@ -0,0 +1,15 @@ +[*] +end_of_line = lf +insert_final_newline = true + +[*.go] +indent_style = tab +indent_size = 8 + +[*.sql] +indent_style = tab +indent_size = 8 + +[Makefile] +indent_style = tab +indent_size = 8 diff --git a/comms/.gitignore b/comms/.gitignore new file mode 100644 index 00000000000..10e7a2e71fb --- /dev/null +++ b/comms/.gitignore @@ -0,0 +1,6 @@ +wip +comms.audius.co +comms-linux +build + +db/schema.sql diff --git a/comms/.vscode/settings.json b/comms/.vscode/settings.json new file mode 100644 index 00000000000..aa1c94e25c9 --- /dev/null +++ b/comms/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "deno.enable": true, + "deno.unstable": true +} diff --git a/comms/Dockerfile b/comms/Dockerfile new file mode 100644 index 00000000000..8d2d308a7e3 --- /dev/null +++ b/comms/Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1 + +FROM golang:latest + +# install in-container tools: dbmate, nats cli +RUN curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 && \ + chmod +x /usr/local/bin/dbmate && \ + CGO_ENABLED=0 go install github.com/nats-io/natscli/nats@latest + +WORKDIR /app + +# COPY go.mod ./ +# COPY go.sum ./ +# RUN go mod download +# COPY . ./ +# RUN CGO_ENABLED=0 GOOS=linux go build -o /comms-linux + +COPY build/comms-linux-amd64 /comms-linux +COPY db/migrations ./db/migrations + +EXPOSE 8925 + +VOLUME ["/tmp/nats"] + +CMD [ "/comms-linux" ] diff --git a/comms/Makefile b/comms/Makefile new file mode 100644 index 00000000000..b8089dd3439 --- /dev/null +++ b/comms/Makefile @@ -0,0 +1,76 @@ + +server:: + @ audius_discprov_env='standalone' \ + audius_delegate_private_key='293589cdf207ed2f2253bb72b17bb7f2cfe399cdc34712b1d32908d969682238' \ + audius_db_url='postgresql://postgres:postgres@localhost:5454/audius_discovery?sslmode=disable' \ + go run main.go + + +reset:: + docker compose down --volumes + docker compose up comdb -d + DATABASE_URL="postgresql://postgres:postgres@localhost:5454/audius_discovery?sslmode=disable" \ + dbmate --wait up + docker exec -it comdb psql -U postgres -c "create database comtest WITH TEMPLATE audius_discovery" + +psql:: + docker exec -it comdb psql -U postgres audius_discovery + +db.schema:: + pg_dump --schema-only --create --no-owner --no-privileges postgres://postgres:postgres@10.128.0.23:5454/audius_discovery > database/docker-initdb/00_schema.sql + + + + + +cluster.up:: + GOOS=linux go build -o comms-linux + docker compose up + +cluster.stagger:: + GOOS=linux go build -o comms-linux + docker compose up -d com1 + sleep 10 + docker compose up -d com2 + sleep 40 + docker compose up -d com3 + sleep 80 + docker compose up -d com4 + + +fmt:: + go fmt ./... + +test:: + docker compose up -d comdb + # go test ./database -v -count=1 + go test ./... + + +# this is a "fast build and push" +# we build target specific binary on our host machine (osx) +# and copy into container (see Dockerfile with build instructions commented out) +# this is nice because go is good at cross compiling and cross compiling inside a docker container running on QEMU is 10x slower +# also we get build cache... end result is a 10 second build and push, which is nice. +# also... this is pretty much identical to doing a docker "multi-stage" build, but way simpler. +build.fast:: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/comms-linux-amd64 + DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build . -t audius/wip-comms:latest + docker push audius/wip-comms:latest + +# build:: +# DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build . -t audius/wip-comms:latest +# docker push audius/wip-comms:latest + + +tools:: + go install github.com/kyleconroy/sqlc/cmd/sqlc@main + CGO_ENABLED=0 go install github.com/nats-io/natscli/nats@latest + CGO_ENABLED=0 go install github.com/amacneil/dbmate@latest + +sqlc:: reset + sqlc generate + +quicktype:: + cp ../audius-protocol/libs/src/sdk/api/chats/serverTypes.ts schema/schema.ts + npx quicktype --package schema --out schema/schema.go --just-types-and-package schema/*.ts diff --git a/comms/README.md b/comms/README.md new file mode 100644 index 00000000000..c33dd53dc56 --- /dev/null +++ b/comms/README.md @@ -0,0 +1,57 @@ +# comms + +You need: + +* docker +* go 1.19 +* make + +First time: + +* Ensure you have `~/go/bin` in your path +* Run `make tools` +* verify `dbmate -h` works + +### running single instance + +``` +make reset +make +``` + +* if you edit go code, restart `make` +* `make psql` to psql + + +### Migrations + +Use [dbmate](https://github.com/amacneil/dbmate): + +* `dbmate new create_cool_table` +* `make db.jet` + + +### Typings + +* Update `schema/schema.ts` to add or modify type definitions +* Update `example/deno_client.ts` to use types +* Run `make quicktype` +* Update go code to use types + +### running cluster + +``` +make cluster.up +``` + +start example client + +``` +go run example/go_client.go +``` + +or + +``` +deno run -A example/deno_client.ts +``` diff --git a/comms/config/config.go b/comms/config/config.go new file mode 100644 index 00000000000..a6d4179d4ad --- /dev/null +++ b/comms/config/config.go @@ -0,0 +1,150 @@ +package config + +import ( + "crypto/ecdsa" + "encoding/hex" + "log" + "os" + "os/exec" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/inconshreveable/log15" + "github.com/nats-io/nkeys" +) + +var ( + Logger = log15.New() + + Env = os.Getenv("audius_discprov_env") + + PrivateKey *ecdsa.PrivateKey + WalletAddress string + + NkeyPair nkeys.KeyPair + NkeyPublic string + + IP string + + NatsClusterUsername = "" + NatsClusterPassword = "" + NatsUseNkeys = true + NatsReplicaCount = 3 + + IsStaging = Env == "stage" +) + +func init() { + Logger.SetHandler(log15.StreamHandler(os.Stdout, log15.TerminalFormat())) +} + +func Init() { + var err error + + switch Env { + case "standalone": + envStandalone() + default: + Logger.Info("no env defaults for: " + Env) + } + + privateKeyHex := os.Getenv("audius_delegate_private_key") + if privateKeyHex == "" { + privateKeyHex = generatePrivateKeyHex() + Logger.Warn("audius_delegate_private_key not provided. Using randomly generated private key.") + } + + // wallet address + privateBytes, err := hex.DecodeString(privateKeyHex) + if err != nil { + log.Fatal("audius_delegate_private_key: invalid hex", err) + } + + PrivateKey, err = crypto.ToECDSA(privateBytes) + if err != nil { + log.Fatal("audius_delegate_private_key: invalid key", err) + } + + WalletAddress = crypto.PubkeyToAddress(PrivateKey.PublicKey).Hex() + + // nkey + NkeyPair, err = nkeys.FromRawSeed(nkeys.PrefixByteUser, privateBytes) + if err != nil { + log.Fatal("audius_delegate_private_key: invalid nkey", err) + } + + if NatsUseNkeys { + if err := configureNatsCliNkey(); err != nil { + Logger.Warn("failed to write cli nkey config: " + err.Error()) + } + } + + NkeyPublic, err = NkeyPair.PublicKey() + if err != nil { + log.Fatal("audius_delegate_private_key: invalid nkey", err) + } + + // ip addr + for i := 0; i < 5; i++ { + IP, err = getIp() + if err != nil { + Logger.Warn("getIp failed", "attempt", i, "err", err) + } else { + break + } + } + + // use our private key to sign our wallet address + // to generate a consistent username + password for this node's NATS cluster route + // that is unguessable + // we return this in the "exchange" endpoint to valid peer nodes + // so that NATS clients can only cluster with us after doing the "exchange" + signed, err := NkeyPair.Sign([]byte(WalletAddress)) + dieOnErr(err) + signedHex := hex.EncodeToString(signed) + NatsClusterUsername = signedHex[0:10] + NatsClusterPassword = signedHex[10:20] + + Logger.Info("config", "wallet", WalletAddress, "nkey", NkeyPublic, "ip", IP, "nu", NatsClusterUsername, "np", NatsClusterPassword) +} + +func dieOnErr(err error) { + if err != nil { + log.Fatal(err) + } +} + +func generatePrivateKeyHex() string { + privateKey, err := crypto.GenerateKey() + if err != nil { + log.Fatal(err) + } + privateKeyBytes := crypto.FromECDSA(privateKey) + return hex.EncodeToString(privateKeyBytes) +} + +func configureNatsCliNkey() error { + var err error + + bytes, err := NkeyPair.Seed() + if err != nil { + return err + } + + tmpFile := "/tmp/nkey_seed.txt" + err = os.WriteFile(tmpFile, bytes, 0644) + if err != nil { + return err + } + + _, err = exec.Command("nats", "context", "save", "--nkey", tmpFile, "default").Output() + if err != nil { + return err + } + + _, err = exec.Command("nats", "context", "select", "default").Output() + if err != nil { + return err + } + + return nil +} diff --git a/comms/config/constants.go b/comms/config/constants.go new file mode 100644 index 00000000000..a8ce13158aa --- /dev/null +++ b/comms/config/constants.go @@ -0,0 +1,7 @@ +package config + +var ( + SigHeader = "x-sig" + + PubkeystoreBucketName = "pubkeystore" +) diff --git a/comms/config/env_staging.go b/comms/config/env_staging.go new file mode 100644 index 00000000000..63faebaf61c --- /dev/null +++ b/comms/config/env_staging.go @@ -0,0 +1,5 @@ +package config + +func stagingDefaults() { + +} diff --git a/comms/config/env_standalone.go b/comms/config/env_standalone.go new file mode 100644 index 00000000000..e0b90f2412e --- /dev/null +++ b/comms/config/env_standalone.go @@ -0,0 +1,6 @@ +package config + +func envStandalone() { + NatsReplicaCount = 1 + NatsUseNkeys = false +} diff --git a/comms/config/get_ip.go b/comms/config/get_ip.go new file mode 100644 index 00000000000..c2b63b74af8 --- /dev/null +++ b/comms/config/get_ip.go @@ -0,0 +1,28 @@ +package config + +import ( + "io" + "net/http" + "os" + "strings" +) + +func getIp() (string, error) { + if testHost := os.Getenv("test_host"); testHost != "" { + return testHost, nil + } + + resp, err := http.Get("https://icanhazip.com/") + if err != nil { + return "", err + } + + rawIp, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + ip := strings.TrimSpace(string(rawIp)) + + return ip, nil +} diff --git a/comms/db/conn.go b/comms/db/conn.go new file mode 100644 index 00000000000..c300c73957f --- /dev/null +++ b/comms/db/conn.go @@ -0,0 +1,50 @@ +package db + +import ( + "log" + "net/url" + "os" + "strings" + + "comms.audius.co/config" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" +) + +var ( + Conn *sqlx.DB +) + +func Dial() error { + var err error + + if Conn != nil { + return nil + } + + dbUrlString := os.Getenv("audius_db_url") + if dbUrlString == "" { + log.Fatal("audius_db_url is required") + } + + dbUrl, err := url.Parse(dbUrlString) + if err != nil { + log.Fatal("invalid db string: "+dbUrlString, "err", err) + } + + logger := config.Logger.New("host", dbUrl.Host, "db", dbUrl.Path) + + // todo: should this be dev only? + if !strings.HasSuffix(dbUrlString, "?sslmode=disable") { + dbUrlString += "?sslmode=disable" + } + + Conn, err = sqlx.Open("postgres", dbUrlString) + if err != nil { + logger.Crit("database.Dial failed " + err.Error()) + return err + } + logger.Info("database dialed") + + return nil +} diff --git a/comms/db/db.go b/comms/db/db.go new file mode 100644 index 00000000000..79b63a944e6 --- /dev/null +++ b/comms/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/comms/db/docker-initdb/00_schema.sql b/comms/db/docker-initdb/00_schema.sql new file mode 100644 index 00000000000..3cc94b5146a --- /dev/null +++ b/comms/db/docker-initdb/00_schema.sql @@ -0,0 +1,4055 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 11.4 (Debian 11.4-1.pgdg90+1) +-- Dumped by pg_dump version 12.12 (Ubuntu 12.12-0ubuntu0.20.04.1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: audius_discovery; Type: DATABASE; Schema: -; Owner: - +-- + +CREATE DATABASE audius_discovery WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.utf8' LC_CTYPE = 'en_US.utf8'; + + +\connect audius_discovery + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: pg_stat_statements; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public; + + +-- +-- Name: EXTENSION pg_stat_statements; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION pg_stat_statements IS 'track execution statistics of all SQL statements executed'; + + +-- +-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public; + + +-- +-- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams'; + + +-- +-- Name: tsm_system_rows; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS tsm_system_rows WITH SCHEMA public; + + +-- +-- Name: EXTENSION tsm_system_rows; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION tsm_system_rows IS 'TABLESAMPLE method which accepts number of rows as a limit'; + + +-- +-- Name: challengetype; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.challengetype AS ENUM ( + 'boolean', + 'numeric', + 'aggregate', + 'trending' +); + + +-- +-- Name: reposttype; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.reposttype AS ENUM ( + 'track', + 'playlist', + 'album' +); + + +-- +-- Name: savetype; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.savetype AS ENUM ( + 'track', + 'playlist', + 'album' +); + + +-- +-- Name: skippedtransactionlevel; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.skippedtransactionlevel AS ENUM ( + 'node', + 'network' +); + + +-- +-- Name: wallet_chain; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.wallet_chain AS ENUM ( + 'eth', + 'sol' +); + + +-- +-- Name: handle_challenge_disbursement(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_challenge_disbursement() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + reward_manager_tx reward_manager_txs%ROWTYPE; +begin + + select * into reward_manager_tx from reward_manager_txs where reward_manager_txs.signature = new.signature limit 1; + + if reward_manager_tx is not null then + -- create a notification for the challenge disbursement + insert into notification + (slot, user_ids, timestamp, type, group_id, specifier, data) + values + ( + new.slot, + ARRAY [new.user_id], + reward_manager_tx.created_at, + 'challenge_reward', + 'challenge_reward:' || new.user_id || ':challenge:' || new.challenge_id || ':specifier:' || new.specifier, + new.user_id, + json_build_object('specifier', new.specifier, 'challenge_id', new.challenge_id, 'amount', new.amount) + ) + on conflict do nothing; + end if; + return null; + +exception + when others then return null; +end; +$$; + + +-- +-- Name: handle_follow(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_follow() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + new_follower_count int; + milestone integer; +begin + insert into aggregate_user (user_id) values (new.followee_user_id) on conflict do nothing; + insert into aggregate_user (user_id) values (new.follower_user_id) on conflict do nothing; + + update aggregate_user + set following_count = ( + select count(*) + from follows + where follower_user_id = new.follower_user_id + and is_current = true + and is_delete = false + ) + where user_id = new.follower_user_id; + + update aggregate_user + set follower_count = ( + select count(*) + from follows + where followee_user_id = new.followee_user_id + and is_current = true + and is_delete = false + ) + where user_id = new.followee_user_id + returning follower_count into new_follower_count; + + -- create a milestone if applicable + select new_follower_count into milestone where new_follower_count in (10, 25, 50, 100, 250, 500, 1000, 5000, 10000, 20000, 50000, 100000, 1000000); + if milestone is not null and new.is_delete is false then + insert into milestones + (id, name, threshold, blocknumber, slot, timestamp) + values + (new.followee_user_id, 'FOLLOWER_COUNT', milestone, new.blocknumber, new.slot, new.created_at) + on conflict do nothing; + insert into notification + (user_ids, type, group_id, specifier, blocknumber, timestamp, data) + values + ( + ARRAY [new.followee_user_id], + 'milestone_follower_count', + 'milestone:FOLLOWER_COUNT:id:' || new.followee_user_id || ':threshold:' || milestone, + new.followee_user_id, + new.blocknumber, + new.created_at, + json_build_object('type', 'FOLLOWER_COUNT', 'user_id', new.followee_user_id, 'threshold', milestone) + ) + on conflict do nothing; + end if; + + begin + -- create a notification for the followee + if new.is_delete is false then + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY [new.followee_user_id], + new.created_at, + 'follow', + new.follower_user_id, + 'follow:' || new.followee_user_id, + json_build_object('followee_user_id', new.followee_user_id, 'follower_user_id', new.follower_user_id) + ) + on conflict do nothing; + end if; + exception + when others then null; + end; + + return null; +end; +$$; + + +-- +-- Name: handle_play(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_play() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + new_listen_count int; + milestone int; + owner_user_id int; +begin + + insert into aggregate_plays (play_item_id, count) values (new.play_item_id, 0) on conflict do nothing; + + update aggregate_plays + set count = count + 1 + where play_item_id = new.play_item_id + returning count into new_listen_count; + + select new_listen_count + into milestone + where new_listen_count in (10,25,50,100,250,500,1000,5000,10000,20000,50000,100000,1000000); + + if milestone is not null then + insert into milestones + (id, name, threshold, slot, timestamp) + values + (new.play_item_id, 'LISTEN_COUNT', milestone, new.slot, new.created_at) + on conflict do nothing; + select tracks.owner_id into owner_user_id from tracks where is_current and track_id = new.play_item_id; + if owner_user_id is not null then + insert into notification + (user_ids, specifier, group_id, type, slot, timestamp, data) + values + ( + array[owner_user_id], + owner_user_id, + 'milestone:LISTEN_COUNT:id:' || new.play_item_id || ':threshold:' || milestone, + 'milestone', + new.slot, + new.created_at, + json_build_object('type', 'LISTEN_COUNT', 'track_id', new.play_item_id, 'threshold', milestone) + ) + on conflict do nothing; + end if; + end if; + return null; +end; +$$; + + +-- +-- Name: handle_playlist(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_playlist() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + track_owner_id int := 0; + track_item json; +begin + + insert into aggregate_user (user_id) values (new.playlist_owner_id) on conflict do nothing; + insert into aggregate_playlist (playlist_id, is_album) values (new.playlist_id, new.is_album) on conflict do nothing; + + if new.is_album then + update aggregate_user + set album_count = ( + select count(*) + from playlists p + where p.is_album IS TRUE + AND p.is_current IS TRUE + AND p.is_delete IS FALSE + AND p.is_private IS FALSE + AND p.playlist_owner_id = new.playlist_owner_id + ) + where user_id = new.playlist_owner_id; + else + update aggregate_user + set playlist_count = ( + select count(*) + from playlists p + where p.is_album IS FALSE + AND p.is_current IS TRUE + AND p.is_delete IS FALSE + AND p.is_private IS FALSE + AND p.playlist_owner_id = new.playlist_owner_id + ) + where user_id = new.playlist_owner_id; + end if; + + begin + if new.is_delete IS FALSE and new.is_private IS FALSE then + for track_item IN select jsonb_array_elements from jsonb_array_elements(new.playlist_contents -> 'track_ids') + loop + if (track_item->>'time')::double precision::int >= extract(epoch from new.updated_at)::int then + select owner_id into track_owner_id from tracks where is_current and track_id=(track_item->>'track')::int; + if track_owner_id != new.playlist_owner_id then + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY [track_owner_id], + new.updated_at, + 'track_added_to_playlist', + track_owner_id, + 'track_added_to_playlist:playlist_id:' || new.playlist_id || ':track_id:' || (track_item->>'track')::int || ':blocknumber:' || new.blocknumber, + json_build_object('track_id', (track_item->>'track')::int, 'playlist_id', new.playlist_id) + ) + on conflict do nothing; + end if; + end if; + end loop; + end if; + exception + when others then null; + end; + + return null; +end; +$$; + + +-- +-- Name: handle_reaction(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_reaction() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + sender_user_id int; +begin + + select user_id into sender_user_id from users where users.wallet=new.sender_wallet and is_current limit 1; + + if sender_user_id is not null then + insert into notification + (slot, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.slot, + ARRAY [sender_user_id], + new.timestamp, + 'reaction', + sender_user_id, + 'reaction:' || 'reaction_to:' || new.reacted_to || ':reaction_type:' || new.reaction_type || ':reaction_value:' || new.reaction_value || ':timestamp:' || new.timestamp, + json_build_object('sender_wallet', new.sender_wallet, 'reaction_type', new.reaction_type, 'reacted_to', new.reacted_to, 'reaction_value', new.reaction_value) + ) + on conflict do nothing; + end if; + return null; + +exception + when others then return null; +end; +$$; + + +-- +-- Name: handle_repost(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_repost() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + new_val int; + milestone_name text; + milestone integer; + owner_user_id int; + track_remix_of json; + is_remix_cosign boolean; +begin + + insert into aggregate_user (user_id) values (new.user_id) on conflict do nothing; + if new.repost_type = 'track' then + insert into aggregate_track (track_id) values (new.repost_item_id) on conflict do nothing; + else + insert into aggregate_playlist (playlist_id, is_album) values (new.repost_item_id, new.repost_type = 'album') on conflict do nothing; + end if; + + -- update agg user + update aggregate_user + set repost_count = ( + select count(*) + from reposts r + where r.is_current IS TRUE + AND r.is_delete IS FALSE + AND r.user_id = new.user_id + ) + where user_id = new.user_id; + + -- update agg track or playlist + if new.repost_type = 'track' then + milestone_name := 'TRACK_REPOST_COUNT'; + update aggregate_track + set repost_count = ( + SELECT count(*) + FROM reposts r + WHERE + r.is_current IS TRUE + AND r.is_delete IS FALSE + AND r.repost_type = new.repost_type + AND r.repost_item_id = new.repost_item_id + ) + where track_id = new.repost_item_id + returning repost_count into new_val; + if new.is_delete IS FALSE then + select tracks.owner_id, tracks.remix_of into owner_user_id, track_remix_of from tracks where is_current and track_id = new.repost_item_id; + end if; + else + milestone_name := 'PLAYLIST_REPOST_COUNT'; + update aggregate_playlist + set repost_count = ( + SELECT count(*) + FROM reposts r + WHERE + r.is_current IS TRUE + AND r.is_delete IS FALSE + AND r.repost_type = new.repost_type + AND r.repost_item_id = new.repost_item_id + ) + where playlist_id = new.repost_item_id + returning repost_count into new_val; + if new.is_delete IS FALSE then + select playlist_owner_id into owner_user_id from playlists where is_current and playlist_id = new.repost_item_id; + end if; + end if; + + -- create a milestone if applicable + select new_val into milestone where new_val in (10, 25, 50, 100, 250, 500, 1000, 5000, 10000, 20000, 50000, 100000, 1000000); + if new.is_delete = false and milestone is not null then + insert into milestones + (id, name, threshold, blocknumber, slot, timestamp) + values + (new.repost_item_id, milestone_name, milestone, new.blocknumber, new.slot, new.created_at) + on conflict do nothing; + insert into notification + (user_ids, type, specifier, group_id, blocknumber, timestamp, data) + values + ( + ARRAY [owner_user_id], + 'milestone', + owner_user_id, + 'milestone:' || milestone_name || ':id:' || new.repost_item_id || ':threshold:' || milestone, + new.blocknumber, + new.created_at, + json_build_object('type', milestone_name, 'threshold', milestone) + ) + on conflict do nothing; + end if; + + begin + -- create a notification for the reposted content's owner + if new.is_delete is false then + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY [owner_user_id], + new.created_at, + 'repost', + new.user_id, + 'repost:' || new.repost_item_id || ':type:'|| new.repost_type, + json_build_object('repost_item_id', new.repost_item_id, 'user_id', new.user_id, 'type', new.repost_type) + ) + on conflict do nothing; + end if; + + -- create a notification for remix cosign + if new.is_delete is false and new.repost_type = 'track' and track_remix_of is not null then + select + case when tracks.owner_id = new.user_id then TRUE else FALSE end as boolean into is_remix_cosign + from tracks + where is_current and track_id = (track_remix_of->'tracks'->0->>'parent_track_id')::int; + if is_remix_cosign then + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY [owner_user_id], + new.created_at, + 'cosign', + new.user_id, + 'cosign:parent_track' || (track_remix_of->'tracks'->0->>'parent_track_id')::int || ':original_track:'|| new.repost_item_id, + json_build_object('parent_track_id', (track_remix_of->'tracks'->0->>'parent_track_id')::int, 'track_id', new.repost_item_id, 'track_owner_id', owner_user_id) + ) + on conflict do nothing; + end if; + end if; + + exception + when others then null; + end; + + return null; +end; +$$; + + +-- +-- Name: handle_save(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_save() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + new_val int; + milestone_name text; + milestone integer; + owner_user_id int; + track_remix_of json; + is_remix_cosign boolean; +begin + + insert into aggregate_user (user_id) values (new.user_id) on conflict do nothing; + if new.save_type = 'track' then + insert into aggregate_track (track_id) values (new.save_item_id) on conflict do nothing; + else + insert into aggregate_playlist (playlist_id, is_album) values (new.save_item_id, new.save_type = 'album') on conflict do nothing; + end if; + + -- update agg track or playlist + if new.save_type = 'track' then + milestone_name := 'TRACK_SAVE_COUNT'; + + update aggregate_track + set save_count = ( + SELECT count(*) + FROM saves r + WHERE + r.is_current IS TRUE + AND r.is_delete IS FALSE + AND r.save_type = new.save_type + AND r.save_item_id = new.save_item_id + ) + where track_id = new.save_item_id + returning save_count into new_val; + + -- update agg user + update aggregate_user + set track_save_count = ( + select count(*) + from saves r + where r.is_current IS TRUE + AND r.is_delete IS FALSE + AND r.user_id = new.user_id + AND r.save_type = new.save_type + ) + where user_id = new.user_id; + if new.is_delete IS FALSE then + select tracks.owner_id, tracks.remix_of into owner_user_id, track_remix_of from tracks where is_current and track_id = new.save_item_id; + end if; + else + milestone_name := 'PLAYLIST_SAVE_COUNT'; + + update aggregate_playlist + set save_count = ( + SELECT count(*) + FROM saves r + WHERE + r.is_current IS TRUE + AND r.is_delete IS FALSE + AND r.save_type = new.save_type + AND r.save_item_id = new.save_item_id + ) + where playlist_id = new.save_item_id + returning save_count into new_val; + if new.is_delete IS FALSE then + select playlists.playlist_owner_id into owner_user_id from playlists where is_current and playlist_id = new.save_item_id; + end if; + + end if; + + -- create a milestone if applicable + select new_val into milestone where new_val in (10, 25, 50, 100, 250, 500, 1000, 5000, 10000, 20000, 50000, 100000, 1000000); + if new.is_delete = false and milestone is not null then + insert into milestones + (id, name, threshold, blocknumber, slot, timestamp) + values + (new.save_item_id, milestone_name, milestone, new.blocknumber, new.slot, new.created_at) + on conflict do nothing; + insert into notification + (user_ids, type, specifier, group_id, blocknumber, timestamp, data) + values + ( + ARRAY [owner_user_id], + 'milestone', + owner_user_id, + 'milestone:' || milestone_name || ':id:' || new.save_item_id || ':threshold:' || milestone, + new.blocknumber, + new.created_at, + json_build_object('type', milestone_name, 'threshold', milestone) + ) + on conflict do nothing; + end if; + + begin + -- create a notification for the saved content's owner + if new.is_delete is false then + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY [owner_user_id], + new.created_at, + 'save', + new.user_id, + 'save:' || new.save_item_id || ':type:'|| new.save_type, + json_build_object('save_item_id', new.save_item_id, 'user_id', new.user_id, 'type', new.save_type) + ) + on conflict do nothing; + end if; + + -- create a notification for remix cosign + if new.is_delete is false and new.save_type = 'track' and track_remix_of is not null then + select + case when tracks.owner_id = new.user_id then TRUE else FALSE end as boolean into is_remix_cosign + from tracks + where is_current and track_id = (track_remix_of->'tracks'->0->>'parent_track_id')::int; + if is_remix_cosign then + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY [owner_user_id], + new.created_at, + 'cosign', + new.user_id, + 'cosign:parent_track' || (track_remix_of->'tracks'->0->>'parent_track_id')::int || ':original_track:'|| new.save_item_id, + json_build_object('parent_track_id', (track_remix_of->'tracks'->0->>'parent_track_id')::int, 'track_id', new.save_item_id, 'track_owner_id', owner_user_id) + ) + on conflict do nothing; + end if; + end if; + exception + when others then return null; + end; + + return null; +end; +$$; + + +-- +-- Name: handle_supporter_rank_up(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_supporter_rank_up() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + user_bank_tx user_bank_txs%ROWTYPE; +begin + select * into user_bank_tx from user_bank_txs where user_bank_txs.slot = new.slot limit 1; + + if user_bank_tx is not null then + -- create a notification for the sender and receiver + insert into notification + (slot, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.slot, + ARRAY [new.sender_user_id], + user_bank_tx.created_at, + 'supporter_rank_up', + new.sender_user_id, + 'supporter_rank_up:' || new.rank || ':slot:' || new.slot, + json_build_object('sender_user_id', new.sender_user_id, 'receiver_user_id', new.receiver_user_id, 'rank', new.rank) + ), + ( + new.slot, + ARRAY [new.receiver_user_id], + user_bank_tx.created_at, + 'supporting_rank_up', + new.receiver_user_id, + 'supporting_rank_up:' || new.rank || ':slot:' || new.slot, + json_build_object('sender_user_id', new.sender_user_id, 'receiver_user_id', new.receiver_user_id, 'rank', new.rank) + ) + on conflict do nothing; + end if; + return null; + +exception + when others then return null; +end; +$$; + + +-- +-- Name: handle_track(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_track() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare + old_row tracks%ROWTYPE; + new_val int; + delta int := 0; + parent_track_owner_id int; +begin + insert into aggregate_track (track_id) values (new.track_id) on conflict do nothing; + insert into aggregate_user (user_id) values (new.owner_id) on conflict do nothing; + + update aggregate_user + set track_count = ( + select count(*) + from tracks t + where t.is_current is true + and t.is_delete is false + and t.is_unlisted is false + and t.is_available is true + and t.stem_of is null + and t.owner_id = new.owner_id + ) + where user_id = new.owner_id + ; + + -- If remix, create notification + begin + if new.remix_of is not null AND new.is_unlisted = FALSE and new.is_available = true AND new.is_delete = FALSE AND new.stem_of IS NULL then + select owner_id into parent_track_owner_id from tracks where is_current and track_id = (new.remix_of->'tracks'->0->>'parent_track_id')::int limit 1; + if parent_track_owner_id is not null then + insert into notification + (blocknumber, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.blocknumber, + ARRAY [parent_track_owner_id], + new.updated_at, + 'remix', + new.owner_id, + 'remix:track:' || new.track_id || ':parent_track:' || (new.remix_of->'tracks'->0->>'parent_track_id')::int || ':blocknumber:' || new.blocknumber, + json_build_object('track_id', new.track_id, 'parent_track_id', (new.remix_of->'tracks'->0->>'parent_track_id')::int) + ) + on conflict do nothing; + end if; + end if; + exception + when others then null; + end; + + return null; +end; +$$; + + +-- +-- Name: handle_user(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_user() RETURNS trigger + LANGUAGE plpgsql + AS $$ +declare +begin + insert into aggregate_user (user_id) values (new.user_id) on conflict do nothing; + return null; +end; +$$; + + +-- +-- Name: handle_user_tip(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.handle_user_tip() RETURNS trigger + LANGUAGE plpgsql + AS $$ +begin + + -- create a notification for the sender and receiver + insert into notification + (slot, user_ids, timestamp, type, specifier, group_id, data) + values + ( + new.slot, + ARRAY [new.receiver_user_id], + new.created_at, + 'tip_receive', + new.receiver_user_id, + 'tip_receive:user_id:' || new.receiver_user_id || ':slot:' || new.slot, + json_build_object('sender_user_id', new.sender_user_id, 'receiver_user_id', new.receiver_user_id, 'amount', new.amount) + ), + ( + new.slot, + ARRAY [new.sender_user_id], + new.created_at, + 'tip_send', + new.sender_user_id, + 'tip_send:user_id:' || new.sender_user_id || ':slot:' || new.slot, + json_build_object('sender_user_id', new.sender_user_id, 'receiver_user_id', new.receiver_user_id, 'amount', new.amount) + ) + on conflict do nothing; + return null; +exception + when others then return null; +end; +$$; + + +-- +-- Name: on_new_row(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.on_new_row() RETURNS trigger + LANGUAGE plpgsql + AS $$ +begin + case TG_TABLE_NAME + when 'tracks' then + PERFORM pg_notify(TG_TABLE_NAME, json_build_object('track_id', new.track_id)::text); + when 'users' then + PERFORM pg_notify(TG_TABLE_NAME, json_build_object('user_id', new.user_id)::text); + when 'playlists' then + PERFORM pg_notify(TG_TABLE_NAME, json_build_object('playlist_id', new.playlist_id)::text); + else + PERFORM pg_notify(TG_TABLE_NAME, to_json(new)::text); + end case; + return null; +end; +$$; + + +-- +-- Name: to_date_safe(character varying, character varying); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.to_date_safe(p_date character varying, p_format character varying) RETURNS date + LANGUAGE plpgsql + AS $$ + DECLARE + ret_date DATE; + BEGIN + IF p_date = '' THEN + RETURN NULL; + END IF; + RETURN to_date( p_date, p_format ); + EXCEPTION + WHEN others THEN + RETURN null; + END; + $$; + + +-- +-- Name: audius_ts_dict; Type: TEXT SEARCH DICTIONARY; Schema: public; Owner: - +-- + +CREATE TEXT SEARCH DICTIONARY public.audius_ts_dict ( + TEMPLATE = pg_catalog.simple ); + + +-- +-- Name: audius_ts_config; Type: TEXT SEARCH CONFIGURATION; Schema: public; Owner: - +-- + +CREATE TEXT SEARCH CONFIGURATION public.audius_ts_config ( + PARSER = pg_catalog."default" ); + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR asciiword WITH public.audius_ts_dict; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR word WITH public.audius_ts_dict; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR numword WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR email WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR url WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR host WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR sfloat WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR version WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR hword_numpart WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR hword_part WITH public.audius_ts_dict; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR hword_asciipart WITH public.audius_ts_dict; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR numhword WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR asciihword WITH public.audius_ts_dict; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR hword WITH public.audius_ts_dict; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR url_path WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR file WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR "float" WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR "int" WITH simple; + +ALTER TEXT SEARCH CONFIGURATION public.audius_ts_config + ADD MAPPING FOR uint WITH simple; + + +SET default_tablespace = ''; + +-- +-- Name: aggregate_daily_app_name_metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_daily_app_name_metrics ( + id integer NOT NULL, + application_name character varying NOT NULL, + count integer NOT NULL, + "timestamp" date NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: aggregate_daily_app_name_metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.aggregate_daily_app_name_metrics_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: aggregate_daily_app_name_metrics_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.aggregate_daily_app_name_metrics_id_seq OWNED BY public.aggregate_daily_app_name_metrics.id; + + +-- +-- Name: aggregate_daily_total_users_metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_daily_total_users_metrics ( + id integer NOT NULL, + count integer NOT NULL, + "timestamp" date NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: aggregate_daily_total_users_metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.aggregate_daily_total_users_metrics_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: aggregate_daily_total_users_metrics_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.aggregate_daily_total_users_metrics_id_seq OWNED BY public.aggregate_daily_total_users_metrics.id; + + +-- +-- Name: aggregate_daily_unique_users_metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_daily_unique_users_metrics ( + id integer NOT NULL, + count integer NOT NULL, + "timestamp" date NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + summed_count integer +); + + +-- +-- Name: aggregate_daily_unique_users_metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.aggregate_daily_unique_users_metrics_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: aggregate_daily_unique_users_metrics_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.aggregate_daily_unique_users_metrics_id_seq OWNED BY public.aggregate_daily_unique_users_metrics.id; + + +-- +-- Name: plays; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.plays ( + id integer NOT NULL, + user_id integer, + source character varying, + play_item_id integer NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + slot integer, + signature character varying, + city character varying, + region character varying, + country character varying +); + + +-- +-- Name: tracks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.tracks ( + blockhash character varying, + track_id integer NOT NULL, + is_current boolean NOT NULL, + is_delete boolean NOT NULL, + owner_id integer NOT NULL, + title text, + length integer, + cover_art character varying, + tags character varying, + genre character varying, + mood character varying, + credits_splits character varying, + create_date character varying, + release_date character varying, + file_type character varying, + metadata_multihash character varying, + blocknumber integer, + track_segments jsonb NOT NULL, + created_at timestamp without time zone NOT NULL, + description character varying, + isrc character varying, + iswc character varying, + license character varying, + updated_at timestamp without time zone NOT NULL, + cover_art_sizes character varying, + download jsonb, + is_unlisted boolean DEFAULT false NOT NULL, + field_visibility jsonb, + route_id character varying, + stem_of jsonb, + remix_of jsonb, + txhash character varying DEFAULT ''::character varying NOT NULL, + slot integer, + is_available boolean DEFAULT true NOT NULL, + is_premium boolean DEFAULT false NOT NULL, + premium_conditions jsonb +); + + +-- +-- Name: aggregate_interval_plays; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.aggregate_interval_plays AS + SELECT tracks.track_id, + tracks.genre, + tracks.created_at, + COALESCE(week_listen_counts.count, (0)::bigint) AS week_listen_counts, + COALESCE(month_listen_counts.count, (0)::bigint) AS month_listen_counts + FROM ((public.tracks + LEFT JOIN ( SELECT plays.play_item_id, + count(plays.id) AS count + FROM public.plays + WHERE (plays.created_at > (now() - '7 days'::interval)) + GROUP BY plays.play_item_id) week_listen_counts ON ((week_listen_counts.play_item_id = tracks.track_id))) + LEFT JOIN ( SELECT plays.play_item_id, + count(plays.id) AS count + FROM public.plays + WHERE (plays.created_at > (now() - '1 mon'::interval)) + GROUP BY plays.play_item_id) month_listen_counts ON ((month_listen_counts.play_item_id = tracks.track_id))) + WHERE ((tracks.is_current IS TRUE) AND (tracks.is_delete IS FALSE) AND (tracks.is_unlisted IS FALSE) AND (tracks.stem_of IS NULL)) + WITH NO DATA; + + +-- +-- Name: aggregate_monthly_app_name_metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_monthly_app_name_metrics ( + id integer NOT NULL, + application_name character varying NOT NULL, + count integer NOT NULL, + "timestamp" date NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: aggregate_monthly_app_name_metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.aggregate_monthly_app_name_metrics_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: aggregate_monthly_app_name_metrics_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.aggregate_monthly_app_name_metrics_id_seq OWNED BY public.aggregate_monthly_app_name_metrics.id; + + +-- +-- Name: aggregate_monthly_plays; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_monthly_plays ( + play_item_id integer NOT NULL, + "timestamp" date DEFAULT CURRENT_TIMESTAMP NOT NULL, + count integer NOT NULL +); + + +-- +-- Name: aggregate_monthly_total_users_metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_monthly_total_users_metrics ( + id integer NOT NULL, + count integer NOT NULL, + "timestamp" date NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: aggregate_monthly_total_users_metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.aggregate_monthly_total_users_metrics_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: aggregate_monthly_total_users_metrics_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.aggregate_monthly_total_users_metrics_id_seq OWNED BY public.aggregate_monthly_total_users_metrics.id; + + +-- +-- Name: aggregate_monthly_unique_users_metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_monthly_unique_users_metrics ( + id integer NOT NULL, + count integer NOT NULL, + "timestamp" date NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + summed_count integer +); + + +-- +-- Name: aggregate_monthly_unique_users_metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.aggregate_monthly_unique_users_metrics_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: aggregate_monthly_unique_users_metrics_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.aggregate_monthly_unique_users_metrics_id_seq OWNED BY public.aggregate_monthly_unique_users_metrics.id; + + +-- +-- Name: aggregate_playlist; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_playlist ( + playlist_id integer NOT NULL, + is_album boolean, + repost_count integer DEFAULT 0, + save_count integer DEFAULT 0 +); + + +-- +-- Name: aggregate_plays; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_plays ( + play_item_id integer NOT NULL, + count bigint +); + + +-- +-- Name: aggregate_track; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_track ( + track_id integer NOT NULL, + repost_count integer DEFAULT 0 NOT NULL, + save_count integer DEFAULT 0 NOT NULL +); + + +-- +-- Name: aggregate_user; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_user ( + user_id integer NOT NULL, + track_count bigint DEFAULT 0, + playlist_count bigint DEFAULT 0, + album_count bigint DEFAULT 0, + follower_count bigint DEFAULT 0, + following_count bigint DEFAULT 0, + repost_count bigint DEFAULT 0, + track_save_count bigint DEFAULT 0, + supporter_count integer DEFAULT 0 NOT NULL, + supporting_count integer DEFAULT 0 NOT NULL +); + + +-- +-- Name: aggregate_user_tips; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.aggregate_user_tips ( + sender_user_id integer NOT NULL, + receiver_user_id integer NOT NULL, + amount bigint NOT NULL +); + + +-- +-- Name: alembic_version; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.alembic_version ( + version_num character varying(32) NOT NULL +); + + +-- +-- Name: app_name_metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.app_name_metrics ( + application_name character varying NOT NULL, + count integer NOT NULL, + "timestamp" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + id bigint NOT NULL, + ip character varying +); + + +-- +-- Name: app_name_metrics_all_time; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.app_name_metrics_all_time AS + SELECT app_name_metrics.application_name AS name, + sum(app_name_metrics.count) AS count + FROM public.app_name_metrics + GROUP BY app_name_metrics.application_name + WITH NO DATA; + + +-- +-- Name: app_name_metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +ALTER TABLE public.app_name_metrics ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME public.app_name_metrics_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: app_name_metrics_trailing_month; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.app_name_metrics_trailing_month AS + SELECT app_name_metrics.application_name AS name, + sum(app_name_metrics.count) AS count + FROM public.app_name_metrics + WHERE (app_name_metrics."timestamp" > (now() - '1 mon'::interval)) + GROUP BY app_name_metrics.application_name + WITH NO DATA; + + +-- +-- Name: app_name_metrics_trailing_week; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.app_name_metrics_trailing_week AS + SELECT app_name_metrics.application_name AS name, + sum(app_name_metrics.count) AS count + FROM public.app_name_metrics + WHERE (app_name_metrics."timestamp" > (now() - '7 days'::interval)) + GROUP BY app_name_metrics.application_name + WITH NO DATA; + + +-- +-- Name: associated_wallets; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.associated_wallets ( + id integer NOT NULL, + user_id integer NOT NULL, + wallet character varying NOT NULL, + blockhash character varying NOT NULL, + blocknumber integer NOT NULL, + is_current boolean NOT NULL, + is_delete boolean NOT NULL, + chain public.wallet_chain NOT NULL +); + + +-- +-- Name: associated_wallets_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.associated_wallets_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: associated_wallets_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.associated_wallets_id_seq OWNED BY public.associated_wallets.id; + + +-- +-- Name: audio_transactions_history; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.audio_transactions_history ( + user_bank character varying NOT NULL, + slot integer NOT NULL, + signature character varying NOT NULL, + transaction_type character varying NOT NULL, + method character varying NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + transaction_created_at timestamp without time zone NOT NULL, + change numeric NOT NULL, + balance numeric NOT NULL, + tx_metadata character varying +); + + +-- +-- Name: audius_data_txs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.audius_data_txs ( + signature character varying NOT NULL, + slot integer NOT NULL +); + + +-- +-- Name: blocks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.blocks ( + blockhash character varying NOT NULL, + parenthash character varying, + is_current boolean, + number integer +); + + +-- +-- Name: blocks_copy; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.blocks_copy ( + blockhash character varying NOT NULL, + parenthash character varying, + is_current boolean, + number integer +); + + +-- +-- Name: challenge_disbursements; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.challenge_disbursements ( + challenge_id character varying NOT NULL, + user_id integer NOT NULL, + specifier character varying NOT NULL, + signature character varying NOT NULL, + slot integer NOT NULL, + amount character varying NOT NULL +); + + +-- +-- Name: challenge_listen_streak; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.challenge_listen_streak ( + user_id integer NOT NULL, + last_listen_date timestamp without time zone, + listen_streak integer NOT NULL +); + + +-- +-- Name: challenge_listen_streak_user_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.challenge_listen_streak_user_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: challenge_listen_streak_user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.challenge_listen_streak_user_id_seq OWNED BY public.challenge_listen_streak.user_id; + + +-- +-- Name: challenge_profile_completion; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.challenge_profile_completion ( + user_id integer NOT NULL, + profile_description boolean NOT NULL, + profile_name boolean NOT NULL, + profile_picture boolean NOT NULL, + profile_cover_photo boolean NOT NULL, + follows boolean NOT NULL, + favorites boolean NOT NULL, + reposts boolean NOT NULL +); + + +-- +-- Name: challenge_profile_completion_user_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.challenge_profile_completion_user_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: challenge_profile_completion_user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.challenge_profile_completion_user_id_seq OWNED BY public.challenge_profile_completion.user_id; + + +-- +-- Name: challenges; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.challenges ( + id character varying NOT NULL, + type public.challengetype NOT NULL, + amount character varying NOT NULL, + active boolean NOT NULL, + step_count integer, + starting_block integer +); + + +-- +-- Name: eth_blocks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.eth_blocks ( + last_scanned_block integer NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: eth_blocks_last_scanned_block_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.eth_blocks_last_scanned_block_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: eth_blocks_last_scanned_block_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.eth_blocks_last_scanned_block_seq OWNED BY public.eth_blocks.last_scanned_block; + + +-- +-- Name: follows; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.follows ( + blockhash character varying, + blocknumber integer, + follower_user_id integer NOT NULL, + followee_user_id integer NOT NULL, + is_current boolean NOT NULL, + is_delete boolean NOT NULL, + created_at timestamp without time zone NOT NULL, + txhash character varying DEFAULT ''::character varying NOT NULL, + slot integer +); + + +-- +-- Name: hourly_play_counts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.hourly_play_counts ( + hourly_timestamp timestamp without time zone NOT NULL, + play_count integer NOT NULL +); + + +-- +-- Name: indexing_checkpoints; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.indexing_checkpoints ( + tablename character varying NOT NULL, + last_checkpoint integer NOT NULL, + signature character varying +); + + +-- +-- Name: milestones; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.milestones ( + id integer NOT NULL, + name character varying NOT NULL, + threshold integer NOT NULL, + blocknumber integer, + slot integer, + "timestamp" timestamp without time zone NOT NULL +); + + +-- +-- Name: notification; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.notification ( + id integer NOT NULL, + specifier character varying NOT NULL, + group_id character varying NOT NULL, + notification_group_id integer, + type character varying NOT NULL, + slot integer, + blocknumber integer, + "timestamp" timestamp without time zone NOT NULL, + data jsonb, + user_ids integer[] +); + + +-- +-- Name: notification_group; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.notification_group ( + id integer NOT NULL, + notification_id integer, + slot integer, + blocknumber integer, + user_id integer NOT NULL, + "timestamp" timestamp without time zone NOT NULL +); + + +-- +-- Name: notification_group_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.notification_group_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: notification_group_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.notification_group_id_seq OWNED BY public.notification_group.id; + + +-- +-- Name: notification_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.notification_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: notification_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.notification_id_seq OWNED BY public.notification.id; + + +-- +-- Name: playlist_routes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playlist_routes ( + slug character varying NOT NULL, + title_slug character varying NOT NULL, + collision_id integer NOT NULL, + owner_id integer NOT NULL, + playlist_id integer NOT NULL, + is_current boolean NOT NULL, + blockhash character varying NOT NULL, + blocknumber integer NOT NULL, + txhash character varying NOT NULL +); + + +-- +-- Name: playlists; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.playlists ( + blockhash character varying, + blocknumber integer, + playlist_id integer NOT NULL, + playlist_owner_id integer NOT NULL, + is_album boolean NOT NULL, + is_private boolean NOT NULL, + playlist_name character varying, + playlist_contents jsonb NOT NULL, + playlist_image_multihash character varying, + is_current boolean NOT NULL, + is_delete boolean NOT NULL, + description character varying, + created_at timestamp without time zone NOT NULL, + upc character varying, + updated_at timestamp without time zone NOT NULL, + playlist_image_sizes_multihash character varying, + txhash character varying DEFAULT ''::character varying NOT NULL, + last_added_to timestamp without time zone, + slot integer, + metadata_multihash character varying +); + + +-- +-- Name: plays_archive; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.plays_archive ( + id integer NOT NULL, + user_id integer, + source character varying, + play_item_id integer NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + slot integer, + signature character varying, + archived_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: plays_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.plays_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: plays_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.plays_id_seq OWNED BY public.plays.id; + + +-- +-- Name: reactions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.reactions ( + id integer NOT NULL, + slot integer NOT NULL, + reaction_value integer NOT NULL, + sender_wallet character varying NOT NULL, + reaction_type character varying NOT NULL, + reacted_to character varying NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + tx_signature character varying +); + + +-- +-- Name: reactions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.reactions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: reactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.reactions_id_seq OWNED BY public.reactions.id; + + +-- +-- Name: related_artists; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.related_artists ( + user_id integer NOT NULL, + related_artist_user_id integer NOT NULL, + score double precision NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: remixes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.remixes ( + parent_track_id integer NOT NULL, + child_track_id integer NOT NULL +); + + +-- +-- Name: reposts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.reposts ( + blockhash character varying, + blocknumber integer, + user_id integer NOT NULL, + repost_item_id integer NOT NULL, + repost_type public.reposttype NOT NULL, + is_current boolean NOT NULL, + is_delete boolean NOT NULL, + created_at timestamp without time zone NOT NULL, + txhash character varying DEFAULT ''::character varying NOT NULL, + slot integer +); + + +-- +-- Name: reward_manager_txs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.reward_manager_txs ( + signature character varying NOT NULL, + slot integer NOT NULL, + created_at timestamp without time zone NOT NULL +); + + +-- +-- Name: rewards_manager_backfill_txs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.rewards_manager_backfill_txs ( + signature character varying NOT NULL, + slot integer NOT NULL, + created_at timestamp without time zone NOT NULL +); + + +-- +-- Name: route_metrics; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.route_metrics ( + route_path character varying NOT NULL, + version character varying NOT NULL, + query_string character varying DEFAULT ''::character varying NOT NULL, + count integer NOT NULL, + "timestamp" timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + id bigint NOT NULL, + ip character varying +); + + +-- +-- Name: route_metrics_all_time; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.route_metrics_all_time AS + SELECT count(DISTINCT route_metrics.ip) AS unique_count, + sum(route_metrics.count) AS count + FROM public.route_metrics + WITH NO DATA; + + +-- +-- Name: route_metrics_day_bucket; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.route_metrics_day_bucket AS + SELECT count(DISTINCT route_metrics.ip) AS unique_count, + sum(route_metrics.count) AS count, + date_trunc('day'::text, route_metrics."timestamp") AS "time" + FROM public.route_metrics + GROUP BY (date_trunc('day'::text, route_metrics."timestamp")) + WITH NO DATA; + + +-- +-- Name: route_metrics_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +ALTER TABLE public.route_metrics ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME public.route_metrics_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + +-- +-- Name: route_metrics_month_bucket; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.route_metrics_month_bucket AS + SELECT count(DISTINCT route_metrics.ip) AS unique_count, + sum(route_metrics.count) AS count, + date_trunc('month'::text, route_metrics."timestamp") AS "time" + FROM public.route_metrics + GROUP BY (date_trunc('month'::text, route_metrics."timestamp")) + WITH NO DATA; + + +-- +-- Name: route_metrics_trailing_month; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.route_metrics_trailing_month AS + SELECT count(DISTINCT route_metrics.ip) AS unique_count, + sum(route_metrics.count) AS count + FROM public.route_metrics + WHERE (route_metrics."timestamp" > (now() - '1 mon'::interval)) + WITH NO DATA; + + +-- +-- Name: route_metrics_trailing_week; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.route_metrics_trailing_week AS + SELECT count(DISTINCT route_metrics.ip) AS unique_count, + sum(route_metrics.count) AS count + FROM public.route_metrics + WHERE (route_metrics."timestamp" > (now() - '7 days'::interval)) + WITH NO DATA; + + +-- +-- Name: saves; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.saves ( + blockhash character varying, + blocknumber integer, + user_id integer NOT NULL, + save_item_id integer NOT NULL, + save_type public.savetype NOT NULL, + is_current boolean NOT NULL, + is_delete boolean NOT NULL, + created_at timestamp without time zone NOT NULL, + txhash character varying DEFAULT ''::character varying NOT NULL, + slot integer +); + + +-- +-- Name: skipped_transactions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.skipped_transactions ( + id integer NOT NULL, + blocknumber integer NOT NULL, + blockhash character varying NOT NULL, + txhash character varying NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + level public.skippedtransactionlevel DEFAULT 'node'::public.skippedtransactionlevel NOT NULL +); + + +-- +-- Name: skipped_transactions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.skipped_transactions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: skipped_transactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.skipped_transactions_id_seq OWNED BY public.skipped_transactions.id; + + +-- +-- Name: spl_token_backfill_txs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.spl_token_backfill_txs ( + last_scanned_slot integer NOT NULL, + signature character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: spl_token_backfill_txs_last_scanned_slot_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.spl_token_backfill_txs_last_scanned_slot_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: spl_token_backfill_txs_last_scanned_slot_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.spl_token_backfill_txs_last_scanned_slot_seq OWNED BY public.spl_token_backfill_txs.last_scanned_slot; + + +-- +-- Name: spl_token_tx; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.spl_token_tx ( + last_scanned_slot integer NOT NULL, + signature character varying NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: stems; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.stems ( + parent_track_id integer NOT NULL, + child_track_id integer NOT NULL +); + + +-- +-- Name: subscriptions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.subscriptions ( + blockhash character varying, + blocknumber integer, + subscriber_id integer NOT NULL, + user_id integer NOT NULL, + is_current boolean NOT NULL, + is_delete boolean NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + txhash character varying DEFAULT ''::character varying NOT NULL +); + + +-- +-- Name: supporter_rank_ups; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.supporter_rank_ups ( + slot integer NOT NULL, + sender_user_id integer NOT NULL, + receiver_user_id integer NOT NULL, + rank integer NOT NULL +); + + +-- +-- Name: tag_track_user; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.tag_track_user AS + SELECT unnest(t.tags) AS tag, + t.track_id, + t.owner_id + FROM ( SELECT string_to_array(lower((tracks.tags)::text), ','::text) AS tags, + tracks.track_id, + tracks.owner_id + FROM public.tracks + WHERE (((tracks.tags)::text <> ''::text) AND (tracks.tags IS NOT NULL) AND (tracks.is_current IS TRUE) AND (tracks.is_unlisted IS FALSE) AND (tracks.stem_of IS NULL)) + ORDER BY tracks.updated_at DESC) t + GROUP BY (unnest(t.tags)), t.track_id, t.owner_id + WITH NO DATA; + + +-- +-- Name: track_routes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.track_routes ( + slug character varying NOT NULL, + title_slug character varying NOT NULL, + collision_id integer NOT NULL, + owner_id integer NOT NULL, + track_id integer NOT NULL, + is_current boolean NOT NULL, + blockhash character varying NOT NULL, + blocknumber integer NOT NULL, + txhash character varying NOT NULL +); + + +-- +-- Name: track_trending_scores; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.track_trending_scores ( + track_id integer NOT NULL, + type character varying NOT NULL, + genre character varying, + version character varying NOT NULL, + time_range character varying NOT NULL, + score double precision NOT NULL, + created_at timestamp without time zone NOT NULL +); + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users ( + blockhash character varying, + user_id integer NOT NULL, + is_current boolean NOT NULL, + handle character varying, + wallet character varying, + name text, + profile_picture character varying, + cover_photo character varying, + bio character varying, + location character varying, + metadata_multihash character varying, + creator_node_endpoint character varying, + blocknumber integer, + is_verified boolean DEFAULT false NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + handle_lc character varying, + cover_photo_sizes character varying, + profile_picture_sizes character varying, + primary_id integer, + secondary_ids integer[], + replica_set_update_signer character varying, + has_collectibles boolean DEFAULT false NOT NULL, + txhash character varying DEFAULT ''::character varying NOT NULL, + playlist_library jsonb, + is_deactivated boolean DEFAULT false NOT NULL, + slot integer, + user_storage_account character varying, + user_authority_account character varying, + artist_pick_track_id integer +); + + +-- +-- Name: trending_params; Type: MATERIALIZED VIEW; Schema: public; Owner: - +-- + +CREATE MATERIALIZED VIEW public.trending_params AS + SELECT t.track_id, + t.genre, + t.owner_id, + ap.play_count, + au.follower_count AS owner_follower_count, + COALESCE(aggregate_track.repost_count, 0) AS repost_count, + COALESCE(aggregate_track.save_count, 0) AS save_count, + COALESCE(repost_week.repost_count, (0)::bigint) AS repost_week_count, + COALESCE(repost_month.repost_count, (0)::bigint) AS repost_month_count, + COALESCE(repost_year.repost_count, (0)::bigint) AS repost_year_count, + COALESCE(save_week.repost_count, (0)::bigint) AS save_week_count, + COALESCE(save_month.repost_count, (0)::bigint) AS save_month_count, + COALESCE(save_year.repost_count, (0)::bigint) AS save_year_count, + COALESCE(karma.karma, (0)::numeric) AS karma + FROM ((((((((((public.tracks t + LEFT JOIN ( SELECT ap_1.count AS play_count, + ap_1.play_item_id + FROM public.aggregate_plays ap_1) ap ON ((ap.play_item_id = t.track_id))) + LEFT JOIN ( SELECT au_1.user_id, + au_1.follower_count + FROM public.aggregate_user au_1) au ON ((au.user_id = t.owner_id))) + LEFT JOIN ( SELECT aggregate_track_1.track_id, + aggregate_track_1.repost_count, + aggregate_track_1.save_count + FROM public.aggregate_track aggregate_track_1) aggregate_track ON ((aggregate_track.track_id = t.track_id))) + LEFT JOIN ( SELECT r.repost_item_id AS track_id, + count(r.repost_item_id) AS repost_count + FROM public.reposts r + WHERE ((r.is_current IS TRUE) AND (r.repost_type = 'track'::public.reposttype) AND (r.is_delete IS FALSE) AND (r.created_at > (now() - '1 year'::interval))) + GROUP BY r.repost_item_id) repost_year ON ((repost_year.track_id = t.track_id))) + LEFT JOIN ( SELECT r.repost_item_id AS track_id, + count(r.repost_item_id) AS repost_count + FROM public.reposts r + WHERE ((r.is_current IS TRUE) AND (r.repost_type = 'track'::public.reposttype) AND (r.is_delete IS FALSE) AND (r.created_at > (now() - '1 mon'::interval))) + GROUP BY r.repost_item_id) repost_month ON ((repost_month.track_id = t.track_id))) + LEFT JOIN ( SELECT r.repost_item_id AS track_id, + count(r.repost_item_id) AS repost_count + FROM public.reposts r + WHERE ((r.is_current IS TRUE) AND (r.repost_type = 'track'::public.reposttype) AND (r.is_delete IS FALSE) AND (r.created_at > (now() - '7 days'::interval))) + GROUP BY r.repost_item_id) repost_week ON ((repost_week.track_id = t.track_id))) + LEFT JOIN ( SELECT r.save_item_id AS track_id, + count(r.save_item_id) AS repost_count + FROM public.saves r + WHERE ((r.is_current IS TRUE) AND (r.save_type = 'track'::public.savetype) AND (r.is_delete IS FALSE) AND (r.created_at > (now() - '1 year'::interval))) + GROUP BY r.save_item_id) save_year ON ((save_year.track_id = t.track_id))) + LEFT JOIN ( SELECT r.save_item_id AS track_id, + count(r.save_item_id) AS repost_count + FROM public.saves r + WHERE ((r.is_current IS TRUE) AND (r.save_type = 'track'::public.savetype) AND (r.is_delete IS FALSE) AND (r.created_at > (now() - '1 mon'::interval))) + GROUP BY r.save_item_id) save_month ON ((save_month.track_id = t.track_id))) + LEFT JOIN ( SELECT r.save_item_id AS track_id, + count(r.save_item_id) AS repost_count + FROM public.saves r + WHERE ((r.is_current IS TRUE) AND (r.save_type = 'track'::public.savetype) AND (r.is_delete IS FALSE) AND (r.created_at > (now() - '7 days'::interval))) + GROUP BY r.save_item_id) save_week ON ((save_week.track_id = t.track_id))) + LEFT JOIN ( SELECT save_and_reposts.item_id AS track_id, + sum(au_1.follower_count) AS karma + FROM (( SELECT r_and_s.user_id, + r_and_s.item_id + FROM (( SELECT reposts.user_id, + reposts.repost_item_id AS item_id + FROM public.reposts + WHERE ((reposts.is_delete IS FALSE) AND (reposts.is_current IS TRUE) AND (reposts.repost_type = 'track'::public.reposttype)) + UNION ALL + SELECT saves.user_id, + saves.save_item_id AS item_id + FROM public.saves + WHERE ((saves.is_delete IS FALSE) AND (saves.is_current IS TRUE) AND (saves.save_type = 'track'::public.savetype))) r_and_s + JOIN public.users ON ((r_and_s.user_id = users.user_id))) + WHERE (((users.cover_photo IS NOT NULL) OR (users.cover_photo_sizes IS NOT NULL)) AND ((users.profile_picture IS NOT NULL) OR (users.profile_picture_sizes IS NOT NULL)) AND (users.bio IS NOT NULL))) save_and_reposts + JOIN public.aggregate_user au_1 ON ((save_and_reposts.user_id = au_1.user_id))) + GROUP BY save_and_reposts.item_id) karma ON ((karma.track_id = t.track_id))) + WHERE ((t.is_current IS TRUE) AND (t.is_delete IS FALSE) AND (t.is_unlisted IS FALSE) AND (t.stem_of IS NULL)) + WITH NO DATA; + + +-- +-- Name: trending_results; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.trending_results ( + user_id integer NOT NULL, + id character varying, + rank integer NOT NULL, + type character varying NOT NULL, + version character varying NOT NULL, + week date NOT NULL +); + + +-- +-- Name: ursm_content_nodes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.ursm_content_nodes ( + blockhash character varying, + blocknumber integer, + created_at timestamp without time zone NOT NULL, + is_current boolean NOT NULL, + cnode_sp_id integer NOT NULL, + delegate_owner_wallet character varying NOT NULL, + owner_wallet character varying NOT NULL, + proposer_sp_ids integer[] NOT NULL, + proposer_1_delegate_owner_wallet character varying NOT NULL, + proposer_2_delegate_owner_wallet character varying NOT NULL, + proposer_3_delegate_owner_wallet character varying NOT NULL, + endpoint character varying, + txhash character varying DEFAULT ''::character varying NOT NULL, + slot integer +); + + +-- +-- Name: user_balance_changes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_balance_changes ( + user_id integer NOT NULL, + blocknumber integer NOT NULL, + current_balance character varying NOT NULL, + previous_balance character varying NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: user_balance_changes_user_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_balance_changes_user_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_balance_changes_user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_balance_changes_user_id_seq OWNED BY public.user_balance_changes.user_id; + + +-- +-- Name: user_balances; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_balances ( + user_id integer NOT NULL, + balance character varying NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + associated_wallets_balance character varying DEFAULT '0'::character varying NOT NULL, + waudio character varying DEFAULT '0'::character varying, + associated_sol_wallets_balance character varying DEFAULT '0'::character varying NOT NULL +); + + +-- +-- Name: user_balances_user_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_balances_user_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_balances_user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_balances_user_id_seq OWNED BY public.user_balances.user_id; + + +-- +-- Name: user_bank_accounts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_bank_accounts ( + signature character varying NOT NULL, + ethereum_address character varying NOT NULL, + created_at timestamp without time zone NOT NULL, + bank_account character varying NOT NULL +); + + +-- +-- Name: user_bank_backfill_txs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_bank_backfill_txs ( + signature character varying NOT NULL, + slot integer NOT NULL, + created_at timestamp without time zone NOT NULL +); + + +-- +-- Name: user_bank_txs; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_bank_txs ( + signature character varying NOT NULL, + slot integer NOT NULL, + created_at timestamp without time zone NOT NULL +); + + +-- +-- Name: user_challenges; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_challenges ( + challenge_id character varying NOT NULL, + user_id integer NOT NULL, + specifier character varying NOT NULL, + is_complete boolean NOT NULL, + current_step_count integer, + completed_blocknumber integer +); + + +-- +-- Name: user_events; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_events ( + id integer NOT NULL, + blockhash character varying, + blocknumber integer, + is_current boolean NOT NULL, + user_id integer NOT NULL, + referrer integer, + is_mobile_user boolean DEFAULT false NOT NULL, + slot integer +); + + +-- +-- Name: user_events_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_events_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_events_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_events_id_seq OWNED BY public.user_events.id; + + +-- +-- Name: user_listening_history; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_listening_history ( + user_id integer NOT NULL, + listening_history jsonb NOT NULL +); + + +-- +-- Name: user_listening_history_user_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_listening_history_user_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_listening_history_user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_listening_history_user_id_seq OWNED BY public.user_listening_history.user_id; + + +-- +-- Name: user_tips; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_tips ( + slot integer NOT NULL, + signature character varying NOT NULL, + sender_user_id integer NOT NULL, + receiver_user_id integer NOT NULL, + amount bigint NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: aggregate_daily_app_name_metrics id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_daily_app_name_metrics ALTER COLUMN id SET DEFAULT nextval('public.aggregate_daily_app_name_metrics_id_seq'::regclass); + + +-- +-- Name: aggregate_daily_total_users_metrics id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_daily_total_users_metrics ALTER COLUMN id SET DEFAULT nextval('public.aggregate_daily_total_users_metrics_id_seq'::regclass); + + +-- +-- Name: aggregate_daily_unique_users_metrics id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_daily_unique_users_metrics ALTER COLUMN id SET DEFAULT nextval('public.aggregate_daily_unique_users_metrics_id_seq'::regclass); + + +-- +-- Name: aggregate_monthly_app_name_metrics id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_monthly_app_name_metrics ALTER COLUMN id SET DEFAULT nextval('public.aggregate_monthly_app_name_metrics_id_seq'::regclass); + + +-- +-- Name: aggregate_monthly_total_users_metrics id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_monthly_total_users_metrics ALTER COLUMN id SET DEFAULT nextval('public.aggregate_monthly_total_users_metrics_id_seq'::regclass); + + +-- +-- Name: aggregate_monthly_unique_users_metrics id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_monthly_unique_users_metrics ALTER COLUMN id SET DEFAULT nextval('public.aggregate_monthly_unique_users_metrics_id_seq'::regclass); + + +-- +-- Name: associated_wallets id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.associated_wallets ALTER COLUMN id SET DEFAULT nextval('public.associated_wallets_id_seq'::regclass); + + +-- +-- Name: challenge_listen_streak user_id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_listen_streak ALTER COLUMN user_id SET DEFAULT nextval('public.challenge_listen_streak_user_id_seq'::regclass); + + +-- +-- Name: challenge_profile_completion user_id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_profile_completion ALTER COLUMN user_id SET DEFAULT nextval('public.challenge_profile_completion_user_id_seq'::regclass); + + +-- +-- Name: eth_blocks last_scanned_block; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.eth_blocks ALTER COLUMN last_scanned_block SET DEFAULT nextval('public.eth_blocks_last_scanned_block_seq'::regclass); + + +-- +-- Name: notification id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notification ALTER COLUMN id SET DEFAULT nextval('public.notification_id_seq'::regclass); + + +-- +-- Name: notification_group id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notification_group ALTER COLUMN id SET DEFAULT nextval('public.notification_group_id_seq'::regclass); + + +-- +-- Name: plays id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.plays ALTER COLUMN id SET DEFAULT nextval('public.plays_id_seq'::regclass); + + +-- +-- Name: reactions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reactions ALTER COLUMN id SET DEFAULT nextval('public.reactions_id_seq'::regclass); + + +-- +-- Name: skipped_transactions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.skipped_transactions ALTER COLUMN id SET DEFAULT nextval('public.skipped_transactions_id_seq'::regclass); + + +-- +-- Name: spl_token_backfill_txs last_scanned_slot; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.spl_token_backfill_txs ALTER COLUMN last_scanned_slot SET DEFAULT nextval('public.spl_token_backfill_txs_last_scanned_slot_seq'::regclass); + + +-- +-- Name: user_balance_changes user_id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_balance_changes ALTER COLUMN user_id SET DEFAULT nextval('public.user_balance_changes_user_id_seq'::regclass); + + +-- +-- Name: user_balances user_id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_balances ALTER COLUMN user_id SET DEFAULT nextval('public.user_balances_user_id_seq'::regclass); + + +-- +-- Name: user_events id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_events ALTER COLUMN id SET DEFAULT nextval('public.user_events_id_seq'::regclass); + + +-- +-- Name: user_listening_history user_id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_listening_history ALTER COLUMN user_id SET DEFAULT nextval('public.user_listening_history_user_id_seq'::regclass); + + +-- +-- Name: aggregate_daily_app_name_metrics aggregate_daily_app_name_metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_daily_app_name_metrics + ADD CONSTRAINT aggregate_daily_app_name_metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: aggregate_daily_total_users_metrics aggregate_daily_total_users_metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_daily_total_users_metrics + ADD CONSTRAINT aggregate_daily_total_users_metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: aggregate_daily_unique_users_metrics aggregate_daily_unique_users_metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_daily_unique_users_metrics + ADD CONSTRAINT aggregate_daily_unique_users_metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: aggregate_monthly_app_name_metrics aggregate_monthly_app_name_metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_monthly_app_name_metrics + ADD CONSTRAINT aggregate_monthly_app_name_metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: aggregate_monthly_plays aggregate_monthly_plays_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_monthly_plays + ADD CONSTRAINT aggregate_monthly_plays_pkey PRIMARY KEY (play_item_id, "timestamp"); + + +-- +-- Name: aggregate_monthly_total_users_metrics aggregate_monthly_total_users_metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_monthly_total_users_metrics + ADD CONSTRAINT aggregate_monthly_total_users_metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: aggregate_monthly_unique_users_metrics aggregate_monthly_unique_users_metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_monthly_unique_users_metrics + ADD CONSTRAINT aggregate_monthly_unique_users_metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: aggregate_playlist aggregate_playlist_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_playlist + ADD CONSTRAINT aggregate_playlist_pkey PRIMARY KEY (playlist_id); + + +-- +-- Name: aggregate_track aggregate_track_table_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_track + ADD CONSTRAINT aggregate_track_table_pkey PRIMARY KEY (track_id); + + +-- +-- Name: aggregate_user aggregate_user_table_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_user + ADD CONSTRAINT aggregate_user_table_pkey PRIMARY KEY (user_id); + + +-- +-- Name: aggregate_user_tips aggregate_user_tips_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_user_tips + ADD CONSTRAINT aggregate_user_tips_pkey PRIMARY KEY (sender_user_id, receiver_user_id); + + +-- +-- Name: alembic_version alembic_version_pkc; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.alembic_version + ADD CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num); + + +-- +-- Name: app_name_metrics app_name_metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.app_name_metrics + ADD CONSTRAINT app_name_metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: associated_wallets associated_wallets_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.associated_wallets + ADD CONSTRAINT associated_wallets_pkey PRIMARY KEY (id); + + +-- +-- Name: audio_transactions_history audio_transactions_history_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audio_transactions_history + ADD CONSTRAINT audio_transactions_history_pkey PRIMARY KEY (user_bank, signature); + + +-- +-- Name: audius_data_txs audius_data_txs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.audius_data_txs + ADD CONSTRAINT audius_data_txs_pkey PRIMARY KEY (signature); + + +-- +-- Name: blocks_copy blocks_copy1_number_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blocks_copy + ADD CONSTRAINT blocks_copy1_number_key UNIQUE (number); + + +-- +-- Name: blocks_copy blocks_copy1_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blocks_copy + ADD CONSTRAINT blocks_copy1_pkey PRIMARY KEY (blockhash); + + +-- +-- Name: blocks blocks_number_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blocks + ADD CONSTRAINT blocks_number_key UNIQUE (number); + + +-- +-- Name: blocks blocks_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.blocks + ADD CONSTRAINT blocks_pkey PRIMARY KEY (blockhash); + + +-- +-- Name: challenge_disbursements challenge_disbursements_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_disbursements + ADD CONSTRAINT challenge_disbursements_pkey PRIMARY KEY (challenge_id, specifier); + + +-- +-- Name: challenge_listen_streak challenge_listen_streak_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_listen_streak + ADD CONSTRAINT challenge_listen_streak_pkey PRIMARY KEY (user_id); + + +-- +-- Name: challenge_profile_completion challenge_profile_completion_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenge_profile_completion + ADD CONSTRAINT challenge_profile_completion_pkey PRIMARY KEY (user_id); + + +-- +-- Name: challenges challenges_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.challenges + ADD CONSTRAINT challenges_pkey PRIMARY KEY (id); + + +-- +-- Name: eth_blocks eth_blocks_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.eth_blocks + ADD CONSTRAINT eth_blocks_pkey PRIMARY KEY (last_scanned_block); + + +-- +-- Name: follows follows_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.follows + ADD CONSTRAINT follows_pkey PRIMARY KEY (is_current, follower_user_id, followee_user_id, txhash); + + +-- +-- Name: hourly_play_counts hourly_play_counts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.hourly_play_counts + ADD CONSTRAINT hourly_play_counts_pkey PRIMARY KEY (hourly_timestamp); + + +-- +-- Name: indexing_checkpoints indexing_checkpoints_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.indexing_checkpoints + ADD CONSTRAINT indexing_checkpoints_pkey PRIMARY KEY (tablename); + + +-- +-- Name: milestones milestones_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.milestones + ADD CONSTRAINT milestones_pkey PRIMARY KEY (id, name, threshold); + + +-- +-- Name: notification_group notification_group_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notification_group + ADD CONSTRAINT notification_group_pkey PRIMARY KEY (id); + + +-- +-- Name: notification notification_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notification + ADD CONSTRAINT notification_pkey PRIMARY KEY (id); + + +-- +-- Name: aggregate_plays play_item_id_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.aggregate_plays + ADD CONSTRAINT play_item_id_pkey PRIMARY KEY (play_item_id); + + +-- +-- Name: playlist_routes playlist_routes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playlist_routes + ADD CONSTRAINT playlist_routes_pkey PRIMARY KEY (owner_id, slug); + + +-- +-- Name: playlists playlists_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playlists + ADD CONSTRAINT playlists_pkey PRIMARY KEY (is_current, playlist_id, txhash); + + +-- +-- Name: plays_archive plays_archive_id_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.plays_archive + ADD CONSTRAINT plays_archive_id_pkey PRIMARY KEY (id); + + +-- +-- Name: plays plays_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.plays + ADD CONSTRAINT plays_pkey PRIMARY KEY (id); + + +-- +-- Name: reactions reactions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reactions + ADD CONSTRAINT reactions_pkey PRIMARY KEY (id); + + +-- +-- Name: related_artists related_artists_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.related_artists + ADD CONSTRAINT related_artists_pkey PRIMARY KEY (user_id, related_artist_user_id); + + +-- +-- Name: remixes remixes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.remixes + ADD CONSTRAINT remixes_pkey PRIMARY KEY (parent_track_id, child_track_id); + + +-- +-- Name: reposts reposts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reposts + ADD CONSTRAINT reposts_pkey PRIMARY KEY (is_current, user_id, repost_item_id, repost_type, txhash); + + +-- +-- Name: reward_manager_txs reward_manager_txs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reward_manager_txs + ADD CONSTRAINT reward_manager_txs_pkey PRIMARY KEY (signature); + + +-- +-- Name: rewards_manager_backfill_txs rewards_manager_backfill_txs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.rewards_manager_backfill_txs + ADD CONSTRAINT rewards_manager_backfill_txs_pkey PRIMARY KEY (signature); + + +-- +-- Name: route_metrics route_metrics_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.route_metrics + ADD CONSTRAINT route_metrics_pkey PRIMARY KEY (id); + + +-- +-- Name: saves saves_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.saves + ADD CONSTRAINT saves_pkey PRIMARY KEY (is_current, user_id, save_item_id, save_type, txhash); + + +-- +-- Name: skipped_transactions skipped_transactions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.skipped_transactions + ADD CONSTRAINT skipped_transactions_pkey PRIMARY KEY (id); + + +-- +-- Name: spl_token_backfill_txs spl_token_backfill_txs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.spl_token_backfill_txs + ADD CONSTRAINT spl_token_backfill_txs_pkey PRIMARY KEY (last_scanned_slot); + + +-- +-- Name: spl_token_tx spl_token_tx_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.spl_token_tx + ADD CONSTRAINT spl_token_tx_pkey PRIMARY KEY (last_scanned_slot); + + +-- +-- Name: stems stems_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.stems + ADD CONSTRAINT stems_pkey PRIMARY KEY (parent_track_id, child_track_id); + + +-- +-- Name: subscriptions subscriptions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.subscriptions + ADD CONSTRAINT subscriptions_pkey PRIMARY KEY (subscriber_id, user_id, is_current, txhash); + + +-- +-- Name: supporter_rank_ups supporter_rank_ups_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.supporter_rank_ups + ADD CONSTRAINT supporter_rank_ups_pkey PRIMARY KEY (slot, sender_user_id, receiver_user_id); + + +-- +-- Name: track_routes track_routes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.track_routes + ADD CONSTRAINT track_routes_pkey PRIMARY KEY (owner_id, slug); + + +-- +-- Name: track_trending_scores track_trending_scores_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.track_trending_scores + ADD CONSTRAINT track_trending_scores_pkey PRIMARY KEY (track_id, type, version, time_range); + + +-- +-- Name: tracks tracks_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tracks + ADD CONSTRAINT tracks_pkey PRIMARY KEY (is_current, track_id, txhash); + + +-- +-- Name: trending_results trending_results_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.trending_results + ADD CONSTRAINT trending_results_pkey PRIMARY KEY (rank, type, version, week); + + +-- +-- Name: notification uq_notification; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notification + ADD CONSTRAINT uq_notification UNIQUE (group_id, specifier); + + +-- +-- Name: ursm_content_nodes ursm_content_nodes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ursm_content_nodes + ADD CONSTRAINT ursm_content_nodes_pkey PRIMARY KEY (is_current, cnode_sp_id, txhash); + + +-- +-- Name: user_balance_changes user_balance_changes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_balance_changes + ADD CONSTRAINT user_balance_changes_pkey PRIMARY KEY (user_id); + + +-- +-- Name: user_balances user_balances_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_balances + ADD CONSTRAINT user_balances_pkey PRIMARY KEY (user_id); + + +-- +-- Name: user_bank_accounts user_bank_accounts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_bank_accounts + ADD CONSTRAINT user_bank_accounts_pkey PRIMARY KEY (signature); + + +-- +-- Name: user_bank_backfill_txs user_bank_backfill_txs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_bank_backfill_txs + ADD CONSTRAINT user_bank_backfill_txs_pkey PRIMARY KEY (signature); + + +-- +-- Name: user_bank_txs user_bank_txs_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_bank_txs + ADD CONSTRAINT user_bank_txs_pkey PRIMARY KEY (signature); + + +-- +-- Name: user_challenges user_challenges_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_challenges + ADD CONSTRAINT user_challenges_pkey PRIMARY KEY (challenge_id, specifier); + + +-- +-- Name: user_events user_events_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_events + ADD CONSTRAINT user_events_pkey PRIMARY KEY (id); + + +-- +-- Name: user_listening_history user_listening_history_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_listening_history + ADD CONSTRAINT user_listening_history_pkey PRIMARY KEY (user_id); + + +-- +-- Name: user_tips user_tips_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_tips + ADD CONSTRAINT user_tips_pkey PRIMARY KEY (slot, signature); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (is_current, user_id, txhash); + + +-- +-- Name: blocks_is_current_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX blocks_is_current_idx ON public.blocks USING btree (is_current) WHERE (is_current IS TRUE); + + +-- +-- Name: blocks_is_current_idx_copy1; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX blocks_is_current_idx_copy1 ON public.blocks_copy USING btree (is_current) WHERE (is_current IS TRUE); + + +-- +-- Name: blocks_number_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX blocks_number_idx ON public.blocks USING btree (number); + + +-- +-- Name: blocks_number_idx_copy1; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX blocks_number_idx_copy1 ON public.blocks_copy USING btree (number); + + +-- +-- Name: follows_blocknumber_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX follows_blocknumber_idx ON public.follows USING btree (blocknumber); + + +-- +-- Name: follows_inbound_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX follows_inbound_idx ON public.follows USING btree (followee_user_id, follower_user_id, is_current, is_delete); + + +-- +-- Name: idx_challenge_disbursements_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_challenge_disbursements_slot ON public.challenge_disbursements USING btree (slot); + + +-- +-- Name: idx_reward_manager_txs_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_reward_manager_txs_slot ON public.reward_manager_txs USING btree (slot); + + +-- +-- Name: idx_rewards_manager_backfill_txs_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_rewards_manager_backfill_txs_slot ON public.rewards_manager_backfill_txs USING btree (slot); + + +-- +-- Name: idx_user_bank_backfill_txs_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_bank_backfill_txs_slot ON public.user_bank_backfill_txs USING btree (slot); + + +-- +-- Name: idx_user_bank_eth_address; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_bank_eth_address ON public.user_bank_accounts USING btree (ethereum_address); + + +-- +-- Name: idx_user_bank_txs_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_user_bank_txs_slot ON public.user_bank_txs USING btree (slot); + + +-- +-- Name: interval_play_month_count_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX interval_play_month_count_idx ON public.aggregate_interval_plays USING btree (month_listen_counts); + + +-- +-- Name: interval_play_track_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX interval_play_track_id_idx ON public.aggregate_interval_plays USING btree (track_id); + + +-- +-- Name: interval_play_week_count_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX interval_play_week_count_idx ON public.aggregate_interval_plays USING btree (week_listen_counts); + + +-- +-- Name: is_current_blocks_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX is_current_blocks_idx ON public.blocks USING btree (is_current); + + +-- +-- Name: is_current_blocks_idx_copy1; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX is_current_blocks_idx_copy1 ON public.blocks_copy USING btree (is_current); + + +-- +-- Name: ix_aggregate_user_tips_receiver_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_aggregate_user_tips_receiver_user_id ON public.aggregate_user_tips USING btree (receiver_user_id); + + +-- +-- Name: ix_associated_wallets_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_associated_wallets_user_id ON public.associated_wallets USING btree (user_id, is_current); + + +-- +-- Name: ix_associated_wallets_wallet; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_associated_wallets_wallet ON public.associated_wallets USING btree (wallet, is_current); + + +-- +-- Name: ix_audio_transactions_history_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_audio_transactions_history_slot ON public.audio_transactions_history USING btree (slot); + + +-- +-- Name: ix_audio_transactions_history_transaction_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_audio_transactions_history_transaction_type ON public.audio_transactions_history USING btree (transaction_type); + + +-- +-- Name: ix_follows_followee_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_follows_followee_user_id ON public.follows USING btree (followee_user_id); + + +-- +-- Name: ix_follows_follower_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_follows_follower_user_id ON public.follows USING btree (follower_user_id); + + +-- +-- Name: ix_notification; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_notification ON public.notification USING gin (user_ids); + + +-- +-- Name: ix_notification_group; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_notification_group ON public.notification_group USING btree (user_id, "timestamp"); + + +-- +-- Name: ix_plays_created_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_plays_created_at ON public.plays USING btree (created_at); + + +-- +-- Name: ix_plays_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_plays_slot ON public.plays USING btree (slot); + + +-- +-- Name: ix_plays_sol_signature; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_plays_sol_signature ON public.plays USING btree (signature); + + +-- +-- Name: ix_plays_user_play_item; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_plays_user_play_item ON public.plays USING btree (play_item_id, user_id); + + +-- +-- Name: ix_plays_user_play_item_date; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_plays_user_play_item_date ON public.plays USING btree (play_item_id, user_id, created_at); + + +-- +-- Name: ix_reactions_reacted_to_reaction_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_reactions_reacted_to_reaction_type ON public.reactions USING btree (reacted_to, reaction_type); + + +-- +-- Name: ix_reactions_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_reactions_slot ON public.reactions USING btree (slot); + + +-- +-- Name: ix_subscriptions_blocknumber; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_subscriptions_blocknumber ON public.subscriptions USING btree (blocknumber); + + +-- +-- Name: ix_subscriptions_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_subscriptions_user_id ON public.subscriptions USING btree (user_id); + + +-- +-- Name: ix_supporter_rank_ups_receiver_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_supporter_rank_ups_receiver_user_id ON public.supporter_rank_ups USING btree (receiver_user_id); + + +-- +-- Name: ix_supporter_rank_ups_sender_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_supporter_rank_ups_sender_user_id ON public.supporter_rank_ups USING btree (sender_user_id); + + +-- +-- Name: ix_supporter_rank_ups_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_supporter_rank_ups_slot ON public.supporter_rank_ups USING btree (slot); + + +-- +-- Name: ix_track_trending_scores_genre; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_track_trending_scores_genre ON public.track_trending_scores USING btree (genre); + + +-- +-- Name: ix_track_trending_scores_score; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_track_trending_scores_score ON public.track_trending_scores USING btree (score); + + +-- +-- Name: ix_track_trending_scores_track_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_track_trending_scores_track_id ON public.track_trending_scores USING btree (track_id); + + +-- +-- Name: ix_track_trending_scores_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_track_trending_scores_type ON public.track_trending_scores USING btree (type); + + +-- +-- Name: ix_updated_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_updated_at ON public.plays USING btree (updated_at, id); + + +-- +-- Name: ix_user_tips_receiver_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_user_tips_receiver_user_id ON public.user_tips USING btree (receiver_user_id); + + +-- +-- Name: ix_user_tips_sender_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_user_tips_sender_user_id ON public.user_tips USING btree (sender_user_id); + + +-- +-- Name: ix_user_tips_slot; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_user_tips_slot ON public.user_tips USING btree (slot); + + +-- +-- Name: ix_users_handle_lc; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_users_handle_lc ON public.users USING btree (handle_lc); + + +-- +-- Name: ix_users_is_deactivated; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_users_is_deactivated ON public.users USING btree (is_deactivated); + + +-- +-- Name: ix_users_wallet; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX ix_users_wallet ON public.users USING btree (wallet); + + +-- +-- Name: milestones_name_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX milestones_name_idx ON public.milestones USING btree (name, id); + + +-- +-- Name: play_item_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX play_item_idx ON public.plays USING btree (play_item_id); + + +-- +-- Name: play_updated_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX play_updated_at_idx ON public.plays USING btree (updated_at); + + +-- +-- Name: playlist_created_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX playlist_created_at_idx ON public.playlists USING btree (created_at); + + +-- +-- Name: playlist_owner_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX playlist_owner_id_idx ON public.playlists USING btree (playlist_owner_id); + + +-- +-- Name: playlist_routes_playlist_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX playlist_routes_playlist_id_idx ON public.playlist_routes USING btree (playlist_id, is_current); + + +-- +-- Name: playlists_blocknumber_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX playlists_blocknumber_idx ON public.playlists USING btree (blocknumber); + + +-- +-- Name: related_artists_related_artist_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX related_artists_related_artist_id_idx ON public.related_artists USING btree (related_artist_user_id, user_id); + + +-- +-- Name: repost_created_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX repost_created_at_idx ON public.reposts USING btree (created_at); + + +-- +-- Name: repost_item_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX repost_item_id_idx ON public.reposts USING btree (repost_item_id, repost_type); + + +-- +-- Name: repost_user_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX repost_user_id_idx ON public.reposts USING btree (user_id, repost_type); + + +-- +-- Name: reposts_blocknumber_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX reposts_blocknumber_idx ON public.reposts USING btree (blocknumber); + + +-- +-- Name: save_item_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX save_item_id_idx ON public.saves USING btree (save_item_id, save_type); + + +-- +-- Name: save_user_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX save_user_id_idx ON public.saves USING btree (user_id, save_type); + + +-- +-- Name: saves_blocknumber_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX saves_blocknumber_idx ON public.saves USING btree (blocknumber); + + +-- +-- Name: tag_track_user_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX tag_track_user_idx ON public.tag_track_user USING btree (tag, track_id, owner_id); + + +-- +-- Name: tag_track_user_tag_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX tag_track_user_tag_idx ON public.tag_track_user USING btree (tag); + + +-- +-- Name: track_created_at_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX track_created_at_idx ON public.tracks USING btree (created_at); + + +-- +-- Name: track_is_premium_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX track_is_premium_idx ON public.tracks USING btree (is_premium, is_current, is_delete); + + +-- +-- Name: track_owner_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX track_owner_id_idx ON public.tracks USING btree (owner_id); + + +-- +-- Name: track_routes_track_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX track_routes_track_id_idx ON public.track_routes USING btree (track_id, is_current); + + +-- +-- Name: tracks_blocknumber_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX tracks_blocknumber_idx ON public.tracks USING btree (blocknumber); + + +-- +-- Name: trending_params_track_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX trending_params_track_id_idx ON public.trending_params USING btree (track_id); + + +-- +-- Name: user_challenges_challenge_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX user_challenges_challenge_idx ON public.user_challenges USING btree (challenge_id); + + +-- +-- Name: user_events_user_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX user_events_user_id_idx ON public.user_events USING btree (user_id, is_current); + + +-- +-- Name: users_blocknumber_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX users_blocknumber_idx ON public.users USING btree (blocknumber); + + +-- +-- Name: challenge_disbursements on_challenge_disbursement; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_challenge_disbursement AFTER INSERT ON public.challenge_disbursements FOR EACH ROW EXECUTE PROCEDURE public.handle_challenge_disbursement(); + + +-- +-- Name: follows on_follow; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_follow AFTER INSERT ON public.follows FOR EACH ROW EXECUTE PROCEDURE public.handle_follow(); + + +-- +-- Name: plays on_play; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_play AFTER INSERT ON public.plays FOR EACH ROW EXECUTE PROCEDURE public.handle_play(); + + +-- +-- Name: playlists on_playlist; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_playlist AFTER INSERT ON public.playlists FOR EACH ROW EXECUTE PROCEDURE public.handle_playlist(); + + +-- +-- Name: reactions on_reaction; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_reaction AFTER INSERT ON public.reactions FOR EACH ROW EXECUTE PROCEDURE public.handle_reaction(); + + +-- +-- Name: reposts on_repost; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_repost AFTER INSERT ON public.reposts FOR EACH ROW EXECUTE PROCEDURE public.handle_repost(); + + +-- +-- Name: saves on_save; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_save AFTER INSERT ON public.saves FOR EACH ROW EXECUTE PROCEDURE public.handle_save(); + + +-- +-- Name: supporter_rank_ups on_supporter_rank_up; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_supporter_rank_up AFTER INSERT ON public.supporter_rank_ups FOR EACH ROW EXECUTE PROCEDURE public.handle_supporter_rank_up(); + + +-- +-- Name: tracks on_track; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_track AFTER INSERT OR UPDATE ON public.tracks FOR EACH ROW EXECUTE PROCEDURE public.handle_track(); + + +-- +-- Name: users on_user; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_user AFTER INSERT ON public.users FOR EACH ROW EXECUTE PROCEDURE public.handle_user(); + + +-- +-- Name: user_tips on_user_tip; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER on_user_tip AFTER INSERT ON public.user_tips FOR EACH ROW EXECUTE PROCEDURE public.handle_user_tip(); + + +-- +-- Name: aggregate_plays trg_aggregate_plays; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_aggregate_plays AFTER INSERT OR UPDATE ON public.aggregate_plays FOR EACH ROW EXECUTE PROCEDURE public.on_new_row(); + + +-- +-- Name: aggregate_user trg_aggregate_user; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_aggregate_user AFTER INSERT OR UPDATE ON public.aggregate_user FOR EACH ROW EXECUTE PROCEDURE public.on_new_row(); + + +-- +-- Name: follows trg_follows; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_follows AFTER INSERT OR UPDATE ON public.follows FOR EACH ROW EXECUTE PROCEDURE public.on_new_row(); + + +-- +-- Name: playlists trg_playlists; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_playlists AFTER INSERT OR UPDATE ON public.playlists FOR EACH ROW EXECUTE PROCEDURE public.on_new_row(); + + +-- +-- Name: reposts trg_reposts; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_reposts AFTER INSERT OR UPDATE ON public.reposts FOR EACH ROW EXECUTE PROCEDURE public.on_new_row(); + + +-- +-- Name: saves trg_saves; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_saves AFTER INSERT OR UPDATE ON public.saves FOR EACH ROW EXECUTE PROCEDURE public.on_new_row(); + + +-- +-- Name: tracks trg_tracks; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_tracks AFTER INSERT OR UPDATE ON public.tracks FOR EACH ROW EXECUTE PROCEDURE public.on_new_row(); + + +-- +-- Name: users trg_users; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER trg_users AFTER INSERT OR UPDATE ON public.users FOR EACH ROW EXECUTE PROCEDURE public.on_new_row(); + + +-- +-- Name: notification_group fk_notification_group_notification; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notification_group + ADD CONSTRAINT fk_notification_group_notification FOREIGN KEY (notification_id) REFERENCES public.notification(id); + + +-- +-- Name: follows follows_blockhash_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.follows + ADD CONSTRAINT follows_blockhash_fkey FOREIGN KEY (blockhash) REFERENCES public.blocks(blockhash); + + +-- +-- Name: follows follows_blocknumber_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.follows + ADD CONSTRAINT follows_blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES public.blocks(number); + + +-- +-- Name: notification notification_notification_group_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notification + ADD CONSTRAINT notification_notification_group_id_fkey FOREIGN KEY (notification_group_id) REFERENCES public.notification_group(id); + + +-- +-- Name: playlists playlists_blockhash_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playlists + ADD CONSTRAINT playlists_blockhash_fkey FOREIGN KEY (blockhash) REFERENCES public.blocks(blockhash); + + +-- +-- Name: playlists playlists_blocknumber_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.playlists + ADD CONSTRAINT playlists_blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES public.blocks(number); + + +-- +-- Name: reposts reposts_blockhash_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reposts + ADD CONSTRAINT reposts_blockhash_fkey FOREIGN KEY (blockhash) REFERENCES public.blocks(blockhash); + + +-- +-- Name: reposts reposts_blocknumber_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.reposts + ADD CONSTRAINT reposts_blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES public.blocks(number); + + +-- +-- Name: saves saves_blockhash_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.saves + ADD CONSTRAINT saves_blockhash_fkey FOREIGN KEY (blockhash) REFERENCES public.blocks(blockhash); + + +-- +-- Name: saves saves_blocknumber_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.saves + ADD CONSTRAINT saves_blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES public.blocks(number); + + +-- +-- Name: subscriptions subscriptions_blockhash_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.subscriptions + ADD CONSTRAINT subscriptions_blockhash_fkey FOREIGN KEY (blockhash) REFERENCES public.blocks(blockhash); + + +-- +-- Name: subscriptions subscriptions_blocknumber_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.subscriptions + ADD CONSTRAINT subscriptions_blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES public.blocks(number); + + +-- +-- Name: tracks tracks_blockhash_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tracks + ADD CONSTRAINT tracks_blockhash_fkey FOREIGN KEY (blockhash) REFERENCES public.blocks(blockhash); + + +-- +-- Name: tracks tracks_blocknumber_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.tracks + ADD CONSTRAINT tracks_blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES public.blocks(number); + + +-- +-- Name: ursm_content_nodes ursm_content_nodes_blockhash_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ursm_content_nodes + ADD CONSTRAINT ursm_content_nodes_blockhash_fkey FOREIGN KEY (blockhash) REFERENCES public.blocks(blockhash); + + +-- +-- Name: ursm_content_nodes ursm_content_nodes_blocknumber_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.ursm_content_nodes + ADD CONSTRAINT ursm_content_nodes_blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES public.blocks(number); + + +-- +-- Name: user_challenges user_challenges_challenge_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_challenges + ADD CONSTRAINT user_challenges_challenge_id_fkey FOREIGN KEY (challenge_id) REFERENCES public.challenges(id); + + +-- +-- Name: users users_blockhash_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_blockhash_fkey FOREIGN KEY (blockhash) REFERENCES public.blocks(blockhash); + + +-- +-- Name: users users_blocknumber_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_blocknumber_fkey FOREIGN KEY (blocknumber) REFERENCES public.blocks(number); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/comms/db/docker-initdb/01_changes.sql b/comms/db/docker-initdb/01_changes.sql new file mode 100644 index 00000000000..c47719e943c --- /dev/null +++ b/comms/db/docker-initdb/01_changes.sql @@ -0,0 +1,7 @@ +\connect audius_discovery + +alter table users drop constraint users_blockhash_fkey; +alter table users drop constraint users_blocknumber_fkey; + +alter table tracks drop constraint tracks_blockhash_fkey; +alter table tracks drop constraint tracks_blocknumber_fkey; diff --git a/comms/db/docker-initdb/02_dbs.sql b/comms/db/docker-initdb/02_dbs.sql new file mode 100644 index 00000000000..4dc1f097e39 --- /dev/null +++ b/comms/db/docker-initdb/02_dbs.sql @@ -0,0 +1,4 @@ +create database com1 WITH TEMPLATE audius_discovery; +create database com2 WITH TEMPLATE audius_discovery; +create database com3 WITH TEMPLATE audius_discovery; +create database com4 WITH TEMPLATE audius_discovery; diff --git a/comms/db/migrations/20221128164712_create_rpc_log_table.sql b/comms/db/migrations/20221128164712_create_rpc_log_table.sql new file mode 100644 index 00000000000..e402be08b9f --- /dev/null +++ b/comms/db/migrations/20221128164712_create_rpc_log_table.sql @@ -0,0 +1,12 @@ +-- migrate:up +create table if not exists rpc_log ( + -- id string primary key, -- todo: get this from rpc + jetstream_sequence integer not null, + jetstream_timestamp timestamp not null, + from_wallet text, + rpc json not null, + sig text not null +); + +-- migrate:down +drop table if exists rpc_log; diff --git a/comms/db/migrations/20221130042018_create_pubkey_table.sql b/comms/db/migrations/20221130042018_create_pubkey_table.sql new file mode 100644 index 00000000000..549912fc8d4 --- /dev/null +++ b/comms/db/migrations/20221130042018_create_pubkey_table.sql @@ -0,0 +1,8 @@ +-- migrate:up +create table user_pubkey ( + user_id int primary key, + pubkey_base64 text not null +) + +-- migrate:down +drop table if exists user_pubkey; diff --git a/comms/db/migrations/20221130171438_drop_pubkey_table.sql b/comms/db/migrations/20221130171438_drop_pubkey_table.sql new file mode 100644 index 00000000000..17cde0a06ea --- /dev/null +++ b/comms/db/migrations/20221130171438_drop_pubkey_table.sql @@ -0,0 +1,5 @@ +-- migrate:up +drop table if exists user_pubkey; + +-- migrate:down + diff --git a/comms/db/migrations/20221202144236_create_chat.sql b/comms/db/migrations/20221202144236_create_chat.sql new file mode 100644 index 00000000000..a3585f060a4 --- /dev/null +++ b/comms/db/migrations/20221202144236_create_chat.sql @@ -0,0 +1,46 @@ +-- migrate:up + +create table chat ( + chat_id text primary key, + created_at timestamp not null, + last_message_at timestamp not null +); + +create index chat_chat_id_idx on chat(chat_id); + +create table chat_member ( + chat_id text not null references chat(chat_id), + user_id int not null, + cleared_history_at timestamp, + + invited_by_user_id int not null, + invite_code text not null, + + -- invite... the shared secret for the chat... including ephemeral key + encrypted shared secret for invitee + + last_active_at timestamp, + unread_count int not null default 0, + + primary key (chat_id, user_id) +); + +create index chat_member_user_idx on chat_member(user_id); + +create table chat_message ( + message_id text primary key, + chat_id text not null, + user_id int not null, + created_at timestamp not null, -- jetstream_timestamp + ciphertext text not null, + + constraint fk_chat_member + foreign key (chat_id, user_id) + references chat_member(chat_id, user_id) +); + +-- migrate:down +drop table chat; +drop index chat_chat_id_idx; +drop table chat_member; +drop index chat_member_user_idx; +drop table chat_message; diff --git a/comms/db/migrations/20221210021301_create_chat_permissions.sql b/comms/db/migrations/20221210021301_create_chat_permissions.sql new file mode 100644 index 00000000000..6b80178f161 --- /dev/null +++ b/comms/db/migrations/20221210021301_create_chat_permissions.sql @@ -0,0 +1,9 @@ +-- migrate:up + +create table chat_permissions ( + user_id int primary key, + permits text default 'all' +); + +-- migrate:down +drop table chat_permissions; diff --git a/comms/db/migrations/20221212074228_create_chat_blocked_users.sql b/comms/db/migrations/20221212074228_create_chat_blocked_users.sql new file mode 100644 index 00000000000..26e217f4d49 --- /dev/null +++ b/comms/db/migrations/20221212074228_create_chat_blocked_users.sql @@ -0,0 +1,11 @@ +-- migrate:up +create table chat_blocked_users ( + blocker_user_id int not null, + blockee_user_id int not null, + created_at timestamp not null default current_timestamp, + + primary key (blocker_user_id, blockee_user_id) +); + +-- migrate:down +drop table chat_blocked_users; diff --git a/comms/db/migrations/20221212081249_create_chat_message_reactions.sql b/comms/db/migrations/20221212081249_create_chat_message_reactions.sql new file mode 100644 index 00000000000..6b92f981257 --- /dev/null +++ b/comms/db/migrations/20221212081249_create_chat_message_reactions.sql @@ -0,0 +1,12 @@ +-- migrate:up +create table chat_message_reactions ( + user_id int not null, + message_id text not null references chat_message(message_id), + reaction text not null, + created_at timestamp not null default current_timestamp, + updated_at timestamp not null default current_timestamp, + primary key (user_id, message_id) +) + +-- migrate:down +drop table chat_message_reactions; diff --git a/comms/db/models.go b/comms/db/models.go new file mode 100644 index 00000000000..b02f05a4c10 --- /dev/null +++ b/comms/db/models.go @@ -0,0 +1,979 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 + +package db + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "fmt" + "time" + + "github.com/tabbed/pqtype" +) + +type Challengetype string + +const ( + ChallengetypeBoolean Challengetype = "boolean" + ChallengetypeNumeric Challengetype = "numeric" + ChallengetypeAggregate Challengetype = "aggregate" + ChallengetypeTrending Challengetype = "trending" +) + +func (e *Challengetype) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = Challengetype(s) + case string: + *e = Challengetype(s) + default: + return fmt.Errorf("unsupported scan type for Challengetype: %T", src) + } + return nil +} + +type NullChallengetype struct { + Challengetype Challengetype + Valid bool // Valid is true if Challengetype is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullChallengetype) Scan(value interface{}) error { + if value == nil { + ns.Challengetype, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.Challengetype.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullChallengetype) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Challengetype, nil +} + +type Reposttype string + +const ( + ReposttypeTrack Reposttype = "track" + ReposttypePlaylist Reposttype = "playlist" + ReposttypeAlbum Reposttype = "album" +) + +func (e *Reposttype) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = Reposttype(s) + case string: + *e = Reposttype(s) + default: + return fmt.Errorf("unsupported scan type for Reposttype: %T", src) + } + return nil +} + +type NullReposttype struct { + Reposttype Reposttype + Valid bool // Valid is true if Reposttype is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullReposttype) Scan(value interface{}) error { + if value == nil { + ns.Reposttype, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.Reposttype.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullReposttype) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Reposttype, nil +} + +type Savetype string + +const ( + SavetypeTrack Savetype = "track" + SavetypePlaylist Savetype = "playlist" + SavetypeAlbum Savetype = "album" +) + +func (e *Savetype) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = Savetype(s) + case string: + *e = Savetype(s) + default: + return fmt.Errorf("unsupported scan type for Savetype: %T", src) + } + return nil +} + +type NullSavetype struct { + Savetype Savetype + Valid bool // Valid is true if Savetype is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullSavetype) Scan(value interface{}) error { + if value == nil { + ns.Savetype, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.Savetype.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullSavetype) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Savetype, nil +} + +type Skippedtransactionlevel string + +const ( + SkippedtransactionlevelNode Skippedtransactionlevel = "node" + SkippedtransactionlevelNetwork Skippedtransactionlevel = "network" +) + +func (e *Skippedtransactionlevel) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = Skippedtransactionlevel(s) + case string: + *e = Skippedtransactionlevel(s) + default: + return fmt.Errorf("unsupported scan type for Skippedtransactionlevel: %T", src) + } + return nil +} + +type NullSkippedtransactionlevel struct { + Skippedtransactionlevel Skippedtransactionlevel + Valid bool // Valid is true if Skippedtransactionlevel is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullSkippedtransactionlevel) Scan(value interface{}) error { + if value == nil { + ns.Skippedtransactionlevel, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.Skippedtransactionlevel.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullSkippedtransactionlevel) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Skippedtransactionlevel, nil +} + +type WalletChain string + +const ( + WalletChainEth WalletChain = "eth" + WalletChainSol WalletChain = "sol" +) + +func (e *WalletChain) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = WalletChain(s) + case string: + *e = WalletChain(s) + default: + return fmt.Errorf("unsupported scan type for WalletChain: %T", src) + } + return nil +} + +type NullWalletChain struct { + WalletChain WalletChain + Valid bool // Valid is true if WalletChain is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullWalletChain) Scan(value interface{}) error { + if value == nil { + ns.WalletChain, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.WalletChain.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullWalletChain) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.WalletChain, nil +} + +type AggregateDailyAppNameMetric struct { + ID int32 `db:"id" json:"id"` + ApplicationName string `db:"application_name" json:"application_name"` + Count int32 `db:"count" json:"count"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type AggregateDailyTotalUsersMetric struct { + ID int32 `db:"id" json:"id"` + Count int32 `db:"count" json:"count"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type AggregateDailyUniqueUsersMetric struct { + ID int32 `db:"id" json:"id"` + Count int32 `db:"count" json:"count"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + SummedCount sql.NullInt32 `db:"summed_count" json:"summed_count"` +} + +type AggregateIntervalPlay struct { + TrackID int32 `db:"track_id" json:"track_id"` + Genre sql.NullString `db:"genre" json:"genre"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + WeekListenCounts int64 `db:"week_listen_counts" json:"week_listen_counts"` + MonthListenCounts int64 `db:"month_listen_counts" json:"month_listen_counts"` +} + +type AggregateMonthlyAppNameMetric struct { + ID int32 `db:"id" json:"id"` + ApplicationName string `db:"application_name" json:"application_name"` + Count int32 `db:"count" json:"count"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type AggregateMonthlyPlay struct { + PlayItemID int32 `db:"play_item_id" json:"play_item_id"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + Count int32 `db:"count" json:"count"` +} + +type AggregateMonthlyTotalUsersMetric struct { + ID int32 `db:"id" json:"id"` + Count int32 `db:"count" json:"count"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type AggregateMonthlyUniqueUsersMetric struct { + ID int32 `db:"id" json:"id"` + Count int32 `db:"count" json:"count"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + SummedCount sql.NullInt32 `db:"summed_count" json:"summed_count"` +} + +type AggregatePlay struct { + PlayItemID int32 `db:"play_item_id" json:"play_item_id"` + Count sql.NullInt64 `db:"count" json:"count"` +} + +type AggregatePlaylist struct { + PlaylistID int32 `db:"playlist_id" json:"playlist_id"` + IsAlbum sql.NullBool `db:"is_album" json:"is_album"` + RepostCount sql.NullInt32 `db:"repost_count" json:"repost_count"` + SaveCount sql.NullInt32 `db:"save_count" json:"save_count"` +} + +type AggregateTrack struct { + TrackID int32 `db:"track_id" json:"track_id"` + RepostCount int32 `db:"repost_count" json:"repost_count"` + SaveCount int32 `db:"save_count" json:"save_count"` +} + +type AggregateUser struct { + UserID int32 `db:"user_id" json:"user_id"` + TrackCount sql.NullInt64 `db:"track_count" json:"track_count"` + PlaylistCount sql.NullInt64 `db:"playlist_count" json:"playlist_count"` + AlbumCount sql.NullInt64 `db:"album_count" json:"album_count"` + FollowerCount sql.NullInt64 `db:"follower_count" json:"follower_count"` + FollowingCount sql.NullInt64 `db:"following_count" json:"following_count"` + RepostCount sql.NullInt64 `db:"repost_count" json:"repost_count"` + TrackSaveCount sql.NullInt64 `db:"track_save_count" json:"track_save_count"` + SupporterCount int32 `db:"supporter_count" json:"supporter_count"` + SupportingCount int32 `db:"supporting_count" json:"supporting_count"` +} + +type AggregateUserTip struct { + SenderUserID int32 `db:"sender_user_id" json:"sender_user_id"` + ReceiverUserID int32 `db:"receiver_user_id" json:"receiver_user_id"` + Amount int64 `db:"amount" json:"amount"` +} + +type AlembicVersion struct { + VersionNum string `db:"version_num" json:"version_num"` +} + +type AppNameMetric struct { + ApplicationName string `db:"application_name" json:"application_name"` + Count int32 `db:"count" json:"count"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID int64 `db:"id" json:"id"` + Ip sql.NullString `db:"ip" json:"ip"` +} + +type AppNameMetricsAllTime struct { + Name string `db:"name" json:"name"` + Count int64 `db:"count" json:"count"` +} + +type AppNameMetricsTrailingMonth struct { + Name string `db:"name" json:"name"` + Count int64 `db:"count" json:"count"` +} + +type AppNameMetricsTrailingWeek struct { + Name string `db:"name" json:"name"` + Count int64 `db:"count" json:"count"` +} + +type AssociatedWallet struct { + ID int32 `db:"id" json:"id"` + UserID int32 `db:"user_id" json:"user_id"` + Wallet string `db:"wallet" json:"wallet"` + Blockhash string `db:"blockhash" json:"blockhash"` + Blocknumber int32 `db:"blocknumber" json:"blocknumber"` + IsCurrent bool `db:"is_current" json:"is_current"` + IsDelete bool `db:"is_delete" json:"is_delete"` + Chain WalletChain `db:"chain" json:"chain"` +} + +type AudioTransactionsHistory struct { + UserBank string `db:"user_bank" json:"user_bank"` + Slot int32 `db:"slot" json:"slot"` + Signature string `db:"signature" json:"signature"` + TransactionType string `db:"transaction_type" json:"transaction_type"` + Method string `db:"method" json:"method"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + TransactionCreatedAt time.Time `db:"transaction_created_at" json:"transaction_created_at"` + Change string `db:"change" json:"change"` + Balance string `db:"balance" json:"balance"` + TxMetadata sql.NullString `db:"tx_metadata" json:"tx_metadata"` +} + +type AudiusDataTx struct { + Signature string `db:"signature" json:"signature"` + Slot int32 `db:"slot" json:"slot"` +} + +type Block struct { + Blockhash string `db:"blockhash" json:"blockhash"` + Parenthash sql.NullString `db:"parenthash" json:"parenthash"` + IsCurrent sql.NullBool `db:"is_current" json:"is_current"` + Number sql.NullInt32 `db:"number" json:"number"` +} + +type BlocksCopy struct { + Blockhash string `db:"blockhash" json:"blockhash"` + Parenthash sql.NullString `db:"parenthash" json:"parenthash"` + IsCurrent sql.NullBool `db:"is_current" json:"is_current"` + Number sql.NullInt32 `db:"number" json:"number"` +} + +type Challenge struct { + ID string `db:"id" json:"id"` + Type Challengetype `db:"type" json:"type"` + Amount string `db:"amount" json:"amount"` + Active bool `db:"active" json:"active"` + StepCount sql.NullInt32 `db:"step_count" json:"step_count"` + StartingBlock sql.NullInt32 `db:"starting_block" json:"starting_block"` +} + +type ChallengeDisbursement struct { + ChallengeID string `db:"challenge_id" json:"challenge_id"` + UserID int32 `db:"user_id" json:"user_id"` + Specifier string `db:"specifier" json:"specifier"` + Signature string `db:"signature" json:"signature"` + Slot int32 `db:"slot" json:"slot"` + Amount string `db:"amount" json:"amount"` +} + +type ChallengeListenStreak struct { + UserID int32 `db:"user_id" json:"user_id"` + LastListenDate sql.NullTime `db:"last_listen_date" json:"last_listen_date"` + ListenStreak int32 `db:"listen_streak" json:"listen_streak"` +} + +type ChallengeProfileCompletion struct { + UserID int32 `db:"user_id" json:"user_id"` + ProfileDescription bool `db:"profile_description" json:"profile_description"` + ProfileName bool `db:"profile_name" json:"profile_name"` + ProfilePicture bool `db:"profile_picture" json:"profile_picture"` + ProfileCoverPhoto bool `db:"profile_cover_photo" json:"profile_cover_photo"` + Follows bool `db:"follows" json:"follows"` + Favorites bool `db:"favorites" json:"favorites"` + Reposts bool `db:"reposts" json:"reposts"` +} + +type Chat struct { + ChatID string `db:"chat_id" json:"chat_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LastMessageAt time.Time `db:"last_message_at" json:"last_message_at"` +} + +type ChatBlockedUser struct { + BlockerUserID int32 `db:"blocker_user_id" json:"blocker_user_id"` + BlockeeUserID int32 `db:"blockee_user_id" json:"blockee_user_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +type ChatMember struct { + ChatID string `db:"chat_id" json:"chat_id"` + UserID int32 `db:"user_id" json:"user_id"` + ClearedHistoryAt sql.NullTime `db:"cleared_history_at" json:"cleared_history_at"` + InvitedByUserID int32 `db:"invited_by_user_id" json:"invited_by_user_id"` + InviteCode string `db:"invite_code" json:"invite_code"` + LastActiveAt sql.NullTime `db:"last_active_at" json:"last_active_at"` + UnreadCount int32 `db:"unread_count" json:"unread_count"` +} + +type ChatMessage struct { + MessageID string `db:"message_id" json:"message_id"` + ChatID string `db:"chat_id" json:"chat_id"` + UserID int32 `db:"user_id" json:"user_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Ciphertext string `db:"ciphertext" json:"ciphertext"` +} + +type ChatMessageReaction struct { + UserID int32 `db:"user_id" json:"user_id"` + MessageID string `db:"message_id" json:"message_id"` + Reaction string `db:"reaction" json:"reaction"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type ChatPermission struct { + UserID int32 `db:"user_id" json:"user_id"` + Permits sql.NullString `db:"permits" json:"permits"` +} + +type EthBlock struct { + LastScannedBlock int32 `db:"last_scanned_block" json:"last_scanned_block"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type Follow struct { + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + FollowerUserID int32 `db:"follower_user_id" json:"follower_user_id"` + FolloweeUserID int32 `db:"followee_user_id" json:"followee_user_id"` + IsCurrent bool `db:"is_current" json:"is_current"` + IsDelete bool `db:"is_delete" json:"is_delete"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Txhash string `db:"txhash" json:"txhash"` + Slot sql.NullInt32 `db:"slot" json:"slot"` +} + +type HourlyPlayCount struct { + HourlyTimestamp time.Time `db:"hourly_timestamp" json:"hourly_timestamp"` + PlayCount int32 `db:"play_count" json:"play_count"` +} + +type IndexingCheckpoint struct { + Tablename string `db:"tablename" json:"tablename"` + LastCheckpoint int32 `db:"last_checkpoint" json:"last_checkpoint"` + Signature sql.NullString `db:"signature" json:"signature"` +} + +type Milestone struct { + ID int32 `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Threshold int32 `db:"threshold" json:"threshold"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + Slot sql.NullInt32 `db:"slot" json:"slot"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` +} + +type Notification struct { + ID int32 `db:"id" json:"id"` + Specifier string `db:"specifier" json:"specifier"` + GroupID string `db:"group_id" json:"group_id"` + NotificationGroupID sql.NullInt32 `db:"notification_group_id" json:"notification_group_id"` + Type string `db:"type" json:"type"` + Slot sql.NullInt32 `db:"slot" json:"slot"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + Data pqtype.NullRawMessage `db:"data" json:"data"` + UserIds []int32 `db:"user_ids" json:"user_ids"` +} + +type NotificationGroup struct { + ID int32 `db:"id" json:"id"` + NotificationID sql.NullInt32 `db:"notification_id" json:"notification_id"` + Slot sql.NullInt32 `db:"slot" json:"slot"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + UserID int32 `db:"user_id" json:"user_id"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` +} + +type Play struct { + ID int32 `db:"id" json:"id"` + UserID sql.NullInt32 `db:"user_id" json:"user_id"` + Source sql.NullString `db:"source" json:"source"` + PlayItemID int32 `db:"play_item_id" json:"play_item_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Slot sql.NullInt32 `db:"slot" json:"slot"` + Signature sql.NullString `db:"signature" json:"signature"` + City sql.NullString `db:"city" json:"city"` + Region sql.NullString `db:"region" json:"region"` + Country sql.NullString `db:"country" json:"country"` +} + +type Playlist struct { + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + PlaylistID int32 `db:"playlist_id" json:"playlist_id"` + PlaylistOwnerID int32 `db:"playlist_owner_id" json:"playlist_owner_id"` + IsAlbum bool `db:"is_album" json:"is_album"` + IsPrivate bool `db:"is_private" json:"is_private"` + PlaylistName sql.NullString `db:"playlist_name" json:"playlist_name"` + PlaylistContents json.RawMessage `db:"playlist_contents" json:"playlist_contents"` + PlaylistImageMultihash sql.NullString `db:"playlist_image_multihash" json:"playlist_image_multihash"` + IsCurrent bool `db:"is_current" json:"is_current"` + IsDelete bool `db:"is_delete" json:"is_delete"` + Description sql.NullString `db:"description" json:"description"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Upc sql.NullString `db:"upc" json:"upc"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + PlaylistImageSizesMultihash sql.NullString `db:"playlist_image_sizes_multihash" json:"playlist_image_sizes_multihash"` + Txhash string `db:"txhash" json:"txhash"` + LastAddedTo sql.NullTime `db:"last_added_to" json:"last_added_to"` + Slot sql.NullInt32 `db:"slot" json:"slot"` + MetadataMultihash sql.NullString `db:"metadata_multihash" json:"metadata_multihash"` +} + +type PlaylistRoute struct { + Slug string `db:"slug" json:"slug"` + TitleSlug string `db:"title_slug" json:"title_slug"` + CollisionID int32 `db:"collision_id" json:"collision_id"` + OwnerID int32 `db:"owner_id" json:"owner_id"` + PlaylistID int32 `db:"playlist_id" json:"playlist_id"` + IsCurrent bool `db:"is_current" json:"is_current"` + Blockhash string `db:"blockhash" json:"blockhash"` + Blocknumber int32 `db:"blocknumber" json:"blocknumber"` + Txhash string `db:"txhash" json:"txhash"` +} + +type PlaysArchive struct { + ID int32 `db:"id" json:"id"` + UserID sql.NullInt32 `db:"user_id" json:"user_id"` + Source sql.NullString `db:"source" json:"source"` + PlayItemID int32 `db:"play_item_id" json:"play_item_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Slot sql.NullInt32 `db:"slot" json:"slot"` + Signature sql.NullString `db:"signature" json:"signature"` + ArchivedAt sql.NullTime `db:"archived_at" json:"archived_at"` +} + +type Reaction struct { + ID int32 `db:"id" json:"id"` + Slot int32 `db:"slot" json:"slot"` + ReactionValue int32 `db:"reaction_value" json:"reaction_value"` + SenderWallet string `db:"sender_wallet" json:"sender_wallet"` + ReactionType string `db:"reaction_type" json:"reaction_type"` + ReactedTo string `db:"reacted_to" json:"reacted_to"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + TxSignature sql.NullString `db:"tx_signature" json:"tx_signature"` +} + +type RelatedArtist struct { + UserID int32 `db:"user_id" json:"user_id"` + RelatedArtistUserID int32 `db:"related_artist_user_id" json:"related_artist_user_id"` + Score float64 `db:"score" json:"score"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +type Remix struct { + ParentTrackID int32 `db:"parent_track_id" json:"parent_track_id"` + ChildTrackID int32 `db:"child_track_id" json:"child_track_id"` +} + +type Repost struct { + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + UserID int32 `db:"user_id" json:"user_id"` + RepostItemID int32 `db:"repost_item_id" json:"repost_item_id"` + RepostType Reposttype `db:"repost_type" json:"repost_type"` + IsCurrent bool `db:"is_current" json:"is_current"` + IsDelete bool `db:"is_delete" json:"is_delete"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Txhash string `db:"txhash" json:"txhash"` + Slot sql.NullInt32 `db:"slot" json:"slot"` +} + +type RewardManagerTx struct { + Signature string `db:"signature" json:"signature"` + Slot int32 `db:"slot" json:"slot"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +type RewardsManagerBackfillTx struct { + Signature string `db:"signature" json:"signature"` + Slot int32 `db:"slot" json:"slot"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +type RouteMetric struct { + RoutePath string `db:"route_path" json:"route_path"` + Version string `db:"version" json:"version"` + QueryString string `db:"query_string" json:"query_string"` + Count int32 `db:"count" json:"count"` + Timestamp time.Time `db:"timestamp" json:"timestamp"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID int64 `db:"id" json:"id"` + Ip sql.NullString `db:"ip" json:"ip"` +} + +type RouteMetricsAllTime struct { + UniqueCount int64 `db:"unique_count" json:"unique_count"` + Count int64 `db:"count" json:"count"` +} + +type RouteMetricsDayBucket struct { + UniqueCount int64 `db:"unique_count" json:"unique_count"` + Count int64 `db:"count" json:"count"` + Time int64 `db:"time" json:"time"` +} + +type RouteMetricsMonthBucket struct { + UniqueCount int64 `db:"unique_count" json:"unique_count"` + Count int64 `db:"count" json:"count"` + Time int64 `db:"time" json:"time"` +} + +type RouteMetricsTrailingMonth struct { + UniqueCount int64 `db:"unique_count" json:"unique_count"` + Count int64 `db:"count" json:"count"` +} + +type RouteMetricsTrailingWeek struct { + UniqueCount int64 `db:"unique_count" json:"unique_count"` + Count int64 `db:"count" json:"count"` +} + +type RpcLog struct { + JetstreamSequence int32 `db:"jetstream_sequence" json:"jetstream_sequence"` + JetstreamTimestamp time.Time `db:"jetstream_timestamp" json:"jetstream_timestamp"` + FromWallet sql.NullString `db:"from_wallet" json:"from_wallet"` + Rpc json.RawMessage `db:"rpc" json:"rpc"` + Sig string `db:"sig" json:"sig"` +} + +type Save struct { + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + UserID int32 `db:"user_id" json:"user_id"` + SaveItemID int32 `db:"save_item_id" json:"save_item_id"` + SaveType Savetype `db:"save_type" json:"save_type"` + IsCurrent bool `db:"is_current" json:"is_current"` + IsDelete bool `db:"is_delete" json:"is_delete"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Txhash string `db:"txhash" json:"txhash"` + Slot sql.NullInt32 `db:"slot" json:"slot"` +} + +type SchemaMigration struct { + Version string `db:"version" json:"version"` +} + +type SkippedTransaction struct { + ID int32 `db:"id" json:"id"` + Blocknumber int32 `db:"blocknumber" json:"blocknumber"` + Blockhash string `db:"blockhash" json:"blockhash"` + Txhash string `db:"txhash" json:"txhash"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Level Skippedtransactionlevel `db:"level" json:"level"` +} + +type SplTokenBackfillTx struct { + LastScannedSlot int32 `db:"last_scanned_slot" json:"last_scanned_slot"` + Signature string `db:"signature" json:"signature"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type SplTokenTx struct { + LastScannedSlot int32 `db:"last_scanned_slot" json:"last_scanned_slot"` + Signature string `db:"signature" json:"signature"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type Stem struct { + ParentTrackID int32 `db:"parent_track_id" json:"parent_track_id"` + ChildTrackID int32 `db:"child_track_id" json:"child_track_id"` +} + +type Subscription struct { + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + SubscriberID int32 `db:"subscriber_id" json:"subscriber_id"` + UserID int32 `db:"user_id" json:"user_id"` + IsCurrent bool `db:"is_current" json:"is_current"` + IsDelete bool `db:"is_delete" json:"is_delete"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Txhash string `db:"txhash" json:"txhash"` +} + +type SupporterRankUp struct { + Slot int32 `db:"slot" json:"slot"` + SenderUserID int32 `db:"sender_user_id" json:"sender_user_id"` + ReceiverUserID int32 `db:"receiver_user_id" json:"receiver_user_id"` + Rank int32 `db:"rank" json:"rank"` +} + +type TagTrackUser struct { + Tag interface{} `db:"tag" json:"tag"` + TrackID int32 `db:"track_id" json:"track_id"` + OwnerID int32 `db:"owner_id" json:"owner_id"` +} + +type Track struct { + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + TrackID int32 `db:"track_id" json:"track_id"` + IsCurrent bool `db:"is_current" json:"is_current"` + IsDelete bool `db:"is_delete" json:"is_delete"` + OwnerID int32 `db:"owner_id" json:"owner_id"` + Title sql.NullString `db:"title" json:"title"` + Length sql.NullInt32 `db:"length" json:"length"` + CoverArt sql.NullString `db:"cover_art" json:"cover_art"` + Tags sql.NullString `db:"tags" json:"tags"` + Genre sql.NullString `db:"genre" json:"genre"` + Mood sql.NullString `db:"mood" json:"mood"` + CreditsSplits sql.NullString `db:"credits_splits" json:"credits_splits"` + CreateDate sql.NullString `db:"create_date" json:"create_date"` + ReleaseDate sql.NullString `db:"release_date" json:"release_date"` + FileType sql.NullString `db:"file_type" json:"file_type"` + MetadataMultihash sql.NullString `db:"metadata_multihash" json:"metadata_multihash"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + TrackSegments json.RawMessage `db:"track_segments" json:"track_segments"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Description sql.NullString `db:"description" json:"description"` + Isrc sql.NullString `db:"isrc" json:"isrc"` + Iswc sql.NullString `db:"iswc" json:"iswc"` + License sql.NullString `db:"license" json:"license"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + CoverArtSizes sql.NullString `db:"cover_art_sizes" json:"cover_art_sizes"` + Download pqtype.NullRawMessage `db:"download" json:"download"` + IsUnlisted bool `db:"is_unlisted" json:"is_unlisted"` + FieldVisibility pqtype.NullRawMessage `db:"field_visibility" json:"field_visibility"` + RouteID sql.NullString `db:"route_id" json:"route_id"` + StemOf pqtype.NullRawMessage `db:"stem_of" json:"stem_of"` + RemixOf pqtype.NullRawMessage `db:"remix_of" json:"remix_of"` + Txhash string `db:"txhash" json:"txhash"` + Slot sql.NullInt32 `db:"slot" json:"slot"` + IsAvailable bool `db:"is_available" json:"is_available"` + IsPremium bool `db:"is_premium" json:"is_premium"` + PremiumConditions pqtype.NullRawMessage `db:"premium_conditions" json:"premium_conditions"` +} + +type TrackRoute struct { + Slug string `db:"slug" json:"slug"` + TitleSlug string `db:"title_slug" json:"title_slug"` + CollisionID int32 `db:"collision_id" json:"collision_id"` + OwnerID int32 `db:"owner_id" json:"owner_id"` + TrackID int32 `db:"track_id" json:"track_id"` + IsCurrent bool `db:"is_current" json:"is_current"` + Blockhash string `db:"blockhash" json:"blockhash"` + Blocknumber int32 `db:"blocknumber" json:"blocknumber"` + Txhash string `db:"txhash" json:"txhash"` +} + +type TrackTrendingScore struct { + TrackID int32 `db:"track_id" json:"track_id"` + Type string `db:"type" json:"type"` + Genre sql.NullString `db:"genre" json:"genre"` + Version string `db:"version" json:"version"` + TimeRange string `db:"time_range" json:"time_range"` + Score float64 `db:"score" json:"score"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +type TrendingParam struct { + TrackID int32 `db:"track_id" json:"track_id"` + Genre sql.NullString `db:"genre" json:"genre"` + OwnerID int32 `db:"owner_id" json:"owner_id"` + PlayCount sql.NullInt64 `db:"play_count" json:"play_count"` + OwnerFollowerCount sql.NullInt64 `db:"owner_follower_count" json:"owner_follower_count"` + RepostCount int32 `db:"repost_count" json:"repost_count"` + SaveCount int32 `db:"save_count" json:"save_count"` + RepostWeekCount int64 `db:"repost_week_count" json:"repost_week_count"` + RepostMonthCount int64 `db:"repost_month_count" json:"repost_month_count"` + RepostYearCount int64 `db:"repost_year_count" json:"repost_year_count"` + SaveWeekCount int64 `db:"save_week_count" json:"save_week_count"` + SaveMonthCount int64 `db:"save_month_count" json:"save_month_count"` + SaveYearCount int64 `db:"save_year_count" json:"save_year_count"` + Karma int64 `db:"karma" json:"karma"` +} + +type TrendingResult struct { + UserID int32 `db:"user_id" json:"user_id"` + ID sql.NullString `db:"id" json:"id"` + Rank int32 `db:"rank" json:"rank"` + Type string `db:"type" json:"type"` + Version string `db:"version" json:"version"` + Week time.Time `db:"week" json:"week"` +} + +type UrsmContentNode struct { + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + IsCurrent bool `db:"is_current" json:"is_current"` + CnodeSpID int32 `db:"cnode_sp_id" json:"cnode_sp_id"` + DelegateOwnerWallet string `db:"delegate_owner_wallet" json:"delegate_owner_wallet"` + OwnerWallet string `db:"owner_wallet" json:"owner_wallet"` + ProposerSpIds []int32 `db:"proposer_sp_ids" json:"proposer_sp_ids"` + Proposer1DelegateOwnerWallet string `db:"proposer_1_delegate_owner_wallet" json:"proposer_1_delegate_owner_wallet"` + Proposer2DelegateOwnerWallet string `db:"proposer_2_delegate_owner_wallet" json:"proposer_2_delegate_owner_wallet"` + Proposer3DelegateOwnerWallet string `db:"proposer_3_delegate_owner_wallet" json:"proposer_3_delegate_owner_wallet"` + Endpoint sql.NullString `db:"endpoint" json:"endpoint"` + Txhash string `db:"txhash" json:"txhash"` + Slot sql.NullInt32 `db:"slot" json:"slot"` +} + +type User struct { + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + UserID int32 `db:"user_id" json:"user_id"` + IsCurrent bool `db:"is_current" json:"is_current"` + Handle sql.NullString `db:"handle" json:"handle"` + Wallet sql.NullString `db:"wallet" json:"wallet"` + Name sql.NullString `db:"name" json:"name"` + ProfilePicture sql.NullString `db:"profile_picture" json:"profile_picture"` + CoverPhoto sql.NullString `db:"cover_photo" json:"cover_photo"` + Bio sql.NullString `db:"bio" json:"bio"` + Location sql.NullString `db:"location" json:"location"` + MetadataMultihash sql.NullString `db:"metadata_multihash" json:"metadata_multihash"` + CreatorNodeEndpoint sql.NullString `db:"creator_node_endpoint" json:"creator_node_endpoint"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + IsVerified bool `db:"is_verified" json:"is_verified"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + HandleLc sql.NullString `db:"handle_lc" json:"handle_lc"` + CoverPhotoSizes sql.NullString `db:"cover_photo_sizes" json:"cover_photo_sizes"` + ProfilePictureSizes sql.NullString `db:"profile_picture_sizes" json:"profile_picture_sizes"` + PrimaryID sql.NullInt32 `db:"primary_id" json:"primary_id"` + SecondaryIds []int32 `db:"secondary_ids" json:"secondary_ids"` + ReplicaSetUpdateSigner sql.NullString `db:"replica_set_update_signer" json:"replica_set_update_signer"` + HasCollectibles bool `db:"has_collectibles" json:"has_collectibles"` + Txhash string `db:"txhash" json:"txhash"` + PlaylistLibrary pqtype.NullRawMessage `db:"playlist_library" json:"playlist_library"` + IsDeactivated bool `db:"is_deactivated" json:"is_deactivated"` + Slot sql.NullInt32 `db:"slot" json:"slot"` + UserStorageAccount sql.NullString `db:"user_storage_account" json:"user_storage_account"` + UserAuthorityAccount sql.NullString `db:"user_authority_account" json:"user_authority_account"` + ArtistPickTrackID sql.NullInt32 `db:"artist_pick_track_id" json:"artist_pick_track_id"` +} + +type UserBalance struct { + UserID int32 `db:"user_id" json:"user_id"` + Balance string `db:"balance" json:"balance"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + AssociatedWalletsBalance string `db:"associated_wallets_balance" json:"associated_wallets_balance"` + Waudio sql.NullString `db:"waudio" json:"waudio"` + AssociatedSolWalletsBalance string `db:"associated_sol_wallets_balance" json:"associated_sol_wallets_balance"` +} + +type UserBalanceChange struct { + UserID int32 `db:"user_id" json:"user_id"` + Blocknumber int32 `db:"blocknumber" json:"blocknumber"` + CurrentBalance string `db:"current_balance" json:"current_balance"` + PreviousBalance string `db:"previous_balance" json:"previous_balance"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type UserBankAccount struct { + Signature string `db:"signature" json:"signature"` + EthereumAddress string `db:"ethereum_address" json:"ethereum_address"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + BankAccount string `db:"bank_account" json:"bank_account"` +} + +type UserBankBackfillTx struct { + Signature string `db:"signature" json:"signature"` + Slot int32 `db:"slot" json:"slot"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +type UserBankTx struct { + Signature string `db:"signature" json:"signature"` + Slot int32 `db:"slot" json:"slot"` + CreatedAt time.Time `db:"created_at" json:"created_at"` +} + +type UserChallenge struct { + ChallengeID string `db:"challenge_id" json:"challenge_id"` + UserID int32 `db:"user_id" json:"user_id"` + Specifier string `db:"specifier" json:"specifier"` + IsComplete bool `db:"is_complete" json:"is_complete"` + CurrentStepCount sql.NullInt32 `db:"current_step_count" json:"current_step_count"` + CompletedBlocknumber sql.NullInt32 `db:"completed_blocknumber" json:"completed_blocknumber"` +} + +type UserEvent struct { + ID int32 `db:"id" json:"id"` + Blockhash sql.NullString `db:"blockhash" json:"blockhash"` + Blocknumber sql.NullInt32 `db:"blocknumber" json:"blocknumber"` + IsCurrent bool `db:"is_current" json:"is_current"` + UserID int32 `db:"user_id" json:"user_id"` + Referrer sql.NullInt32 `db:"referrer" json:"referrer"` + IsMobileUser bool `db:"is_mobile_user" json:"is_mobile_user"` + Slot sql.NullInt32 `db:"slot" json:"slot"` +} + +type UserListeningHistory struct { + UserID int32 `db:"user_id" json:"user_id"` + ListeningHistory json.RawMessage `db:"listening_history" json:"listening_history"` +} + +type UserTip struct { + Slot int32 `db:"slot" json:"slot"` + Signature string `db:"signature" json:"signature"` + SenderUserID int32 `db:"sender_user_id" json:"sender_user_id"` + ReceiverUserID int32 `db:"receiver_user_id" json:"receiver_user_id"` + Amount int64 `db:"amount" json:"amount"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} diff --git a/comms/db/queries/get_chat_blocked_users.go b/comms/db/queries/get_chat_blocked_users.go new file mode 100644 index 00000000000..411ba1caa82 --- /dev/null +++ b/comms/db/queries/get_chat_blocked_users.go @@ -0,0 +1,37 @@ +package queries + +import ( + "context" + + "comms.audius.co/db" +) + +const chatBlock = ` +select blocker_user_id, blockee_user_id, created_at from chat_blocked_users where blocker_user_id = $1 and blockee_user_id = $2 +` + +type ChatBlockParams struct { + BlockerUserID int32 `db:"blocker_user_id" json:"blocker_user_id"` + BlockeeUserID int32 `db:"blockee_user_id" json:"blockee_user_id"` +} + +func ChatBlock(q db.Queryable, ctx context.Context, arg ChatBlockParams) (db.ChatBlockedUser, error) { + var block db.ChatBlockedUser + err := q.GetContext(ctx, &block, chatBlock, arg.BlockerUserID, arg.BlockeeUserID) + return block, err +} + +const countChatBlocks = ` +select count(*) from chat_blocked_users where (blocker_user_id = $1 and blockee_user_id = $2) or (blocker_user_id = $2 and blockee_user_id = $1) +` + +type CountChatBlocksParams struct { + User1 int32 `json:"user_1"` + User2 int32 `json:"user_2"` +} + +func CountChatBlocks(q db.Queryable, ctx context.Context, arg CountChatBlocksParams) (int64, error) { + var count int64 + err := q.GetContext(ctx, &count, countChatBlocks, arg.User1, arg.User2) + return count, err +} diff --git a/comms/db/queries/get_chat_members.go b/comms/db/queries/get_chat_members.go new file mode 100644 index 00000000000..ba554147ea3 --- /dev/null +++ b/comms/db/queries/get_chat_members.go @@ -0,0 +1,46 @@ +package queries + +import ( + "context" + "database/sql" + + "comms.audius.co/db" +) + +type ChatMembershipParams struct { + UserID int32 `db:"user_id" json:"user_id"` + ChatID string `db:"chat_id" json:"chat_id"` +} + +// Get a user's chat membership +const chatMembership = ` +select chat_id, user_id, cleared_history_at, invited_by_user_id, invite_code, last_active_at, unread_count from chat_member where user_id = $1 and chat_id = $2 +` + +func ChatMembership(q db.Queryable, ctx context.Context, arg ChatMembershipParams) (db.ChatMember, error) { + var member db.ChatMember + err := q.GetContext(ctx, &member, chatMembership, arg.UserID, arg.ChatID) + return member, err +} + +// Get all memberships in a chat +const chatMembers = ` +select chat_id, user_id, cleared_history_at, invited_by_user_id, invite_code, last_active_at, unread_count from chat_member where chat_id = $1 +` + +func ChatMembers(q db.Queryable, ctx context.Context, chatID string) ([]db.ChatMember, error) { + var members []db.ChatMember + err := q.SelectContext(ctx, &members, chatMembers, chatID) + return members, err +} + +// Get a user's last_active_at timestamp in a chat +const lastActiveAt = ` +select last_active_at from chat_member where chat_id = $1 and user_id = $2 +` + +func LastActiveAt(q db.Queryable, ctx context.Context, arg ChatMembershipParams) (sql.NullTime, error) { + var activeAt sql.NullTime + err := q.GetContext(ctx, &activeAt, lastActiveAt, arg.ChatID, arg.UserID) + return activeAt, err +} diff --git a/comms/db/queries/get_chat_messages.go b/comms/db/queries/get_chat_messages.go new file mode 100644 index 00000000000..08b5491b387 --- /dev/null +++ b/comms/db/queries/get_chat_messages.go @@ -0,0 +1,76 @@ +package queries + +import ( + "context" + "encoding/json" + "errors" + "time" + + "comms.audius.co/db" +) + +// Get a chat message +const chatMessage = ` +select message_id, chat_id, user_id, created_at, ciphertext from chat_message where chat_id = $1 and message_id = $2 +` + +type ChatMessageParams struct { + ChatID string `db:"chat_id" json:"chat_id"` + MessageID string `db:"message_id" json:"message_id"` +} + +func ChatMessage(q db.Queryable, ctx context.Context, arg ChatMessageParams) (db.ChatMessage, error) { + var message db.ChatMessage + err := q.GetContext(ctx, &message, chatMessage, arg.ChatID, arg.MessageID) + return message, err +} + +// Get chat messages and reactions +const chatMessagesAndReactions = ` +SELECT chat_message.message_id, chat_message.chat_id, chat_message.user_id, chat_message.created_at, chat_message.ciphertext, COALESCE(jsonb_agg(reactions) FILTER (WHERE reactions.message_id IS NOT NULL), '[]') AS reactions +FROM chat_message +JOIN chat_member ON chat_message.chat_id = chat_member.chat_id +LEFT JOIN chat_message_reactions reactions ON chat_message.message_id = reactions.message_id +WHERE chat_member.user_id = $1 AND chat_message.chat_id = $2 AND chat_message.created_at < $4 AND (chat_member.cleared_history_at IS NULL OR chat_message.created_at > chat_member.cleared_history_at) +GROUP BY chat_message.message_id +ORDER BY chat_message.created_at DESC +LIMIT $3 +` + +type ChatMessagesAndReactionsParams struct { + UserID int32 `db:"user_id" json:"user_id"` + ChatID string `db:"chat_id" json:"chat_id"` + Limit int32 `json:"limit"` + Cursor time.Time `json:"cursor"` +} + +type ChatMessageAndReactionsRow struct { + MessageID string `db:"message_id" json:"message_id"` + ChatID string `db:"chat_id" json:"chat_id"` + UserID int32 `db:"user_id" json:"user_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Ciphertext string `db:"ciphertext" json:"ciphertext"` + Reactions ReactionsSlice `json:"reactions"` +} + +type ReactionsSlice []db.ChatMessageReaction + +func (reactions *ReactionsSlice) Scan(value interface{}) error { + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + + return json.Unmarshal(bytes, reactions) +} + +func ChatMessagesAndReactions(q db.Queryable, ctx context.Context, arg ChatMessagesAndReactionsParams) ([]ChatMessageAndReactionsRow, error) { + var rows []ChatMessageAndReactionsRow + err := q.SelectContext(ctx, &rows, chatMessagesAndReactions, + arg.UserID, + arg.ChatID, + arg.Limit, + arg.Cursor, + ) + return rows, err +} diff --git a/comms/db/queries/get_chats.go b/comms/db/queries/get_chats.go new file mode 100644 index 00000000000..3041113409d --- /dev/null +++ b/comms/db/queries/get_chats.go @@ -0,0 +1,61 @@ +package queries + +import ( + "context" + "database/sql" + "time" + + "comms.audius.co/db" +) + +type UserChatRow struct { + ChatID string `db:"chat_id" json:"chat_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LastMessageAt time.Time `db:"last_message_at" json:"last_message_at"` + InviteCode string `db:"invite_code" json:"invite_code"` + LastActiveAt sql.NullTime `db:"last_active_at" json:"last_active_at"` + UnreadCount int32 `db:"unread_count" json:"unread_count"` + ClearedHistoryAt sql.NullTime `db:"cleared_history_at" json:"cleared_history_at"` +} + +// Get a chat with user-specific details +const userChat = ` +SELECT + chat.chat_id, + chat.created_at, + chat.last_message_at, + chat_member.invite_code, + chat_member.last_active_at, + chat_member.unread_count, + chat_member.cleared_history_at +FROM chat_member +JOIN chat ON chat.chat_id = chat_member.chat_id +WHERE chat_member.user_id = $1 AND chat_member.chat_id = $2 +` + +func UserChat(q db.Queryable, ctx context.Context, arg ChatMembershipParams) (UserChatRow, error) { + var chat UserChatRow + err := q.GetContext(ctx, &chat, userChat, arg.UserID, arg.ChatID) + return chat, err +} + +// Get all chats (with user-specific details) for the given user +const userChats = ` +SELECT + chat.chat_id, + chat.created_at, + chat.last_message_at, + chat_member.invite_code, + chat_member.last_active_at, + chat_member.unread_count, + chat_member.cleared_history_at +FROM chat_member +JOIN chat ON chat.chat_id = chat_member.chat_id +WHERE chat_member.user_id = $1 AND (chat_member.cleared_history_at IS NULL OR chat.last_message_at > chat_member.cleared_history_at) +` + +func UserChats(q db.Queryable, ctx context.Context, userID int32) ([]UserChatRow, error) { + var items []UserChatRow + err := q.SelectContext(ctx, &items, userChats, userID) + return items, err +} diff --git a/comms/db/queries/get_summaries.go b/comms/db/queries/get_summaries.go new file mode 100644 index 00000000000..e03d497d68c --- /dev/null +++ b/comms/db/queries/get_summaries.go @@ -0,0 +1,66 @@ +package queries + +import ( + "context" + "time" + + "comms.audius.co/db" +) + +type SummaryRow struct { + TotalCount int64 `json:"total_count"` + RemainingCount int64 `json:"remaining_count"` +} + +const chatMessagesSummary = ` +WITH messages AS ( + SELECT + chat_message.message_id, chat_message.chat_id, chat_message.user_id, chat_message.created_at, chat_message.ciphertext + FROM chat_message + JOIN chat_member ON chat_message.chat_id = chat_member.chat_id + WHERE chat_member.user_id = $1 AND chat_message.chat_id = $2 AND (chat_member.cleared_history_at IS NULL OR chat_message.created_at > chat_member.cleared_history_at) +) +SELECT + (SELECT COUNT(*) AS total_count FROM messages), + (SELECT COUNT(*) FROM messages WHERE created_at < $3) AS remaining_count +` + +type ChatMessagesSummaryParams struct { + UserID int32 `db:"user_id" json:"user_id"` + ChatID string `db:"chat_id" json:"chat_id"` + Cursor time.Time `json:"cursor"` +} + +func ChatMessagesSummary(q db.Queryable, ctx context.Context, arg ChatMessagesSummaryParams) (SummaryRow, error) { + var summary SummaryRow + err := q.GetContext(ctx, &summary, chatMessagesSummary, arg.UserID, arg.ChatID, arg.Cursor) + return summary, err +} + +const userChatsSummary = ` +WITH user_chats AS ( + SELECT + chat.chat_id, + chat.last_message_at + FROM chat_member + JOIN chat ON chat.chat_id = chat_member.chat_id + WHERE chat_member.user_id = $2 AND (chat_member.cleared_history_at IS NULL OR chat.last_message_at > chat_member.cleared_history_at) +) +SELECT + (SELECT COUNT(*) AS total_count FROM user_chats), + ( + SELECT COUNT(*) FROM user_chats + WHERE last_message_at < $1 + ) AS remaining_count +` + +type UserChatsSummaryParams struct { + Cursor time.Time `json:"cursor"` + UserID int32 `db:"user_id" json:"user_id"` +} + +func UserChatsSummary(q db.Queryable, ctx context.Context, arg UserChatsSummaryParams) (SummaryRow, error) { + var summary SummaryRow + err := q.GetContext(ctx, &summary, userChatsSummary, arg.Cursor, arg.UserID) + return summary, err +} diff --git a/comms/db/queries/get_users.go b/comms/db/queries/get_users.go new file mode 100644 index 00000000000..83ecbe7fb9d --- /dev/null +++ b/comms/db/queries/get_users.go @@ -0,0 +1,17 @@ +package queries + +import ( + "context" + + "comms.audius.co/db" +) + +const getUserIDFromWallet = ` +select user_id from users where is_current = TRUE and wallet = LOWER($1) +` + +func GetUserIDFromWallet(q db.Queryable, ctx context.Context, walletAddress string) (int32, error) { + var user_id int32 + err := q.GetContext(ctx, &user_id, getUserIDFromWallet, walletAddress) + return user_id, err +} diff --git a/comms/db/query.sql b/comms/db/query.sql new file mode 100644 index 00000000000..58a0a15f6db --- /dev/null +++ b/comms/db/query.sql @@ -0,0 +1,7 @@ +-- name: CreateUser :one +insert into users ( + user_id, handle, is_current +) values ( + $1, $2, true +) +returning *; diff --git a/comms/db/query.sql.go b/comms/db/query.sql.go new file mode 100644 index 00000000000..2b3e36845e2 --- /dev/null +++ b/comms/db/query.sql.go @@ -0,0 +1,65 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.16.0 +// source: query.sql + +package db + +import ( + "context" + "database/sql" + + "github.com/lib/pq" +) + +const createUser = `-- name: CreateUser :one +insert into users ( + user_id, handle, is_current +) values ( + $1, $2, true +) +returning blockhash, user_id, is_current, handle, wallet, name, profile_picture, cover_photo, bio, location, metadata_multihash, creator_node_endpoint, blocknumber, is_verified, created_at, updated_at, handle_lc, cover_photo_sizes, profile_picture_sizes, primary_id, secondary_ids, replica_set_update_signer, has_collectibles, txhash, playlist_library, is_deactivated, slot, user_storage_account, user_authority_account, artist_pick_track_id +` + +type CreateUserParams struct { + UserID int32 `db:"user_id" json:"user_id"` + Handle sql.NullString `db:"handle" json:"handle"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, arg.UserID, arg.Handle) + var i User + err := row.Scan( + &i.Blockhash, + &i.UserID, + &i.IsCurrent, + &i.Handle, + &i.Wallet, + &i.Name, + &i.ProfilePicture, + &i.CoverPhoto, + &i.Bio, + &i.Location, + &i.MetadataMultihash, + &i.CreatorNodeEndpoint, + &i.Blocknumber, + &i.IsVerified, + &i.CreatedAt, + &i.UpdatedAt, + &i.HandleLc, + &i.CoverPhotoSizes, + &i.ProfilePictureSizes, + &i.PrimaryID, + pq.Array(&i.SecondaryIds), + &i.ReplicaSetUpdateSigner, + &i.HasCollectibles, + &i.Txhash, + &i.PlaylistLibrary, + &i.IsDeactivated, + &i.Slot, + &i.UserStorageAccount, + &i.UserAuthorityAccount, + &i.ArtistPickTrackID, + ) + return i, err +} diff --git a/comms/db/sqlx_db.go b/comms/db/sqlx_db.go new file mode 100644 index 00000000000..328259d5a88 --- /dev/null +++ b/comms/db/sqlx_db.go @@ -0,0 +1,34 @@ +package db + +import ( + "context" + "database/sql" + + "github.com/jmoiron/sqlx" +) + +// Queryable includes all methods shared by sqlx.DB and sqlx.Tx, allowing +// either type to be used interchangeably. +type Queryable interface { + sqlx.Ext + sqlx.ExecerContext + sqlx.PreparerContext + sqlx.QueryerContext + sqlx.Preparer + + GetContext(context.Context, interface{}, string, ...interface{}) error + SelectContext(context.Context, interface{}, string, ...interface{}) error + Get(interface{}, string, ...interface{}) error + MustExecContext(context.Context, string, ...interface{}) sql.Result + PreparexContext(context.Context, string) (*sqlx.Stmt, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row + Select(interface{}, string, ...interface{}) error + QueryRow(string, ...interface{}) *sql.Row + PrepareNamedContext(context.Context, string) (*sqlx.NamedStmt, error) + PrepareNamed(string) (*sqlx.NamedStmt, error) + Preparex(string) (*sqlx.Stmt, error) + NamedExec(string, interface{}) (sql.Result, error) + NamedExecContext(context.Context, string, interface{}) (sql.Result, error) + MustExec(string, ...interface{}) sql.Result + NamedQuery(string, interface{}) (*sqlx.Rows, error) +} diff --git a/comms/docker-compose.yml b/comms/docker-compose.yml new file mode 100644 index 00000000000..c99ecf7d51d --- /dev/null +++ b/comms/docker-compose.yml @@ -0,0 +1,85 @@ +services: + comdb: + container_name: comdb + image: postgres + ports: + - "5454:5432" + environment: + POSTGRES_PASSWORD: postgres + volumes: + - ./db/docker-initdb:/docker-entrypoint-initdb.d + + com1: + container_name: com1 + build: . + environment: + audius_discprov_env: test + test_host: com1 + # audius_delegate_owner_wallet: '0x1c185053c2259f72fd023ED89B9b3EBbD841DA0F' + audius_delegate_private_key: "293589cdf207ed2f2253bb72b17bb7f2cfe399cdc34712b1d32908d969682238" + audius_db_url: "postgresql://postgres:postgres@comdb:5432/com1?sslmode=disable" + audius_comms_cluster: "true" + # audius_comms_replica_count: 3 # could be a future dynamic replica config count thing + volumes: + - ./comms-linux:/comms-linux + depends_on: + - comdb + ports: + - 4222:4222 + - 6222:6222 + - 8222:8222 + - 8925:8925 + + com2: + container_name: com2 + build: . + volumes: + - ./comms-linux:/comms-linux + environment: + audius_discprov_env: test + test_host: com2 + # audius_delegate_owner_wallet: '0x90b8d2655A7C268d0fA31758A714e583AE54489D' + audius_delegate_private_key: "1ca1082d2304d96c2e6a3e551226e72e2cb54fddfe69b946b0efc2d9b43c19fc" + audius_db_url: "postgresql://postgres:postgres@comdb:5432/com2?sslmode=disable" + audius_comms_cluster: "true" + depends_on: + - comdb + ports: + - 8926:8925 + + com3: + container_name: com3 + build: . + volumes: + - ./comms-linux:/comms-linux + environment: + audius_discprov_env: test + test_host: com3 + # audius_delegate_owner_wallet: '0xb7b9599EeB2FD9237C94cFf02d74368Bb2df959B' + audius_delegate_private_key: "12712efcf90774399e272f8fc89ef264058b4cdd7f7f86956052050cbfb4350c" + audius_db_url: "postgresql://postgres:postgres@comdb:5432/com3?sslmode=disable" + audius_comms_cluster: "true" + depends_on: + - comdb + ports: + - 8927:8925 + + com4: + container_name: com4 + build: . + volumes: + - ./comms-linux:/comms-linux + environment: + audius_discprov_env: test + test_host: com4 + audius_delegate_private_key: "2617e6258025c60b5aa270e02ff2247eefab37c7b463b2a870104862870ad3fb" + audius_db_url: "postgresql://postgres:postgres@comdb:5432/com4?sslmode=disable" + audius_comms_cluster: "true" + depends_on: + - comdb + ports: + - 8928:8925 +# volumes: +# data_cz1: +# data_cz2: +# data_cz3: diff --git a/comms/em/abi.go b/comms/em/abi.go new file mode 100644 index 00000000000..fb40e770eed --- /dev/null +++ b/comms/em/abi.go @@ -0,0 +1,193 @@ +package em + +import ( + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +var emAbiJson = ` +[ + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "name": "usedSignatures", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_userId", + "type": "uint256" + }, + { + "indexed": false, + "name": "_signer", + "type": "address" + }, + { + "indexed": false, + "name": "_entityType", + "type": "string" + }, + { + "indexed": false, + "name": "_entityId", + "type": "uint256" + }, + { + "indexed": false, + "name": "_metadata", + "type": "string" + }, + { + "indexed": false, + "name": "_action", + "type": "string" + } + ], + "name": "ManageEntity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "_userId", + "type": "uint256" + }, + { + "indexed": false, + "name": "_isVerified", + "type": "bool" + } + ], + "name": "ManageIsVerified", + "type": "event" + }, + { + "constant": false, + "inputs": [ + { + "name": "name", + "type": "string" + }, + { + "name": "version", + "type": "string" + }, + { + "name": "chainId", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_verifierAddress", + "type": "address" + }, + { + "name": "_networkId", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_userId", + "type": "uint256" + }, + { + "name": "_entityType", + "type": "string" + }, + { + "name": "_entityId", + "type": "uint256" + }, + { + "name": "_action", + "type": "string" + }, + { + "name": "_metadata", + "type": "string" + }, + { + "name": "_nonce", + "type": "bytes32" + }, + { + "name": "_subjectSig", + "type": "bytes" + } + ], + "name": "manageEntity", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_userId", + "type": "uint256" + }, + { + "name": "_isVerified", + "type": "bool" + } + ], + "name": "manageIsVerified", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] +` + +var ( + emAbi abi.ABI +) + +func init() { + var err error + emAbi, err = abi.JSON(strings.NewReader(emAbiJson)) + if err != nil { + panic(err) + } +} diff --git a/comms/em/chain_test.go b/comms/em/chain_test.go new file mode 100644 index 00000000000..fdee2cd0a10 --- /dev/null +++ b/comms/em/chain_test.go @@ -0,0 +1,114 @@ +package em + +import ( + "context" + "fmt" + "log" + "math/big" + "strings" + "testing" + + "comms.audius.co/config" + "comms.audius.co/db" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/assert" +) + +func TestEmChain(t *testing.T) { + t.Skip() + + config.IsStaging = true // dagron + + db.Dial() + + cf, err := NewCidFetcher() + assert.NoError(t, err) + + var relayEndpoint = "https://nethermind.staging.audius.co/" + var emContractAddress = "0x1Cd8a543596D499B9b6E7a6eC15ECd2B7857Fd64" + + client, err := ethclient.Dial(relayEndpoint) + if err != nil { + log.Fatal(err) + } + + query := ethereum.FilterQuery{ + FromBlock: big.NewInt(1000), + ToBlock: big.NewInt(500_000), + Addresses: []common.Address{ + common.HexToAddress(emContractAddress), + }, + } + + log.Println("querying") + logs, err := client.FilterLogs(context.Background(), query) + if err != nil { + log.Fatal(err) + } + + for _, txlog := range logs { + + ev, err := emAbi.EventByID(txlog.Topics[0]) + if err != nil { + log.Fatal("EventByID failed:", err) + } + + params := map[string]interface{}{} + err = ev.Inputs.UnpackIntoMap(params, txlog.Data) + if err != nil { + log.Fatal("UnpackIntoMap failed:", err) + } + + // fmt.Println(" \t", ev.Name, params) + if ev.Name != "ManageEntity" { + fmt.Println("skipping", ev.Name, params) + continue + } + + action := &EntityManagerAction{ + UserID: params["_userId"].(*big.Int).Int64(), + Action: params["_action"].(string), + EntityType: params["_entityType"].(string), + EntityID: params["_entityId"].(*big.Int).Int64(), + Metadata: params["_metadata"].(string), + Signer: params["_signer"].(common.Address).String(), + + blockNumber: int(txlog.BlockNumber), + blockhash: txlog.BlockHash.String(), + txHash: txlog.TxHash.String(), + txIndex: int(txlog.TxIndex), + } + + if action.EntityType == "UserReplicaSet" { + continue + } + + // annoying: you have to get the block to get the timestamp + // todo: collect into batches to batch fetch: + // block, metadata cid, user + entity rows + + // block, err := client.BlockByHash(context.Background(), txlog.BlockHash) + // assert.NoError(t, err) + // ts := time.Unix(int64(block.Time()), 0) + // action.Timestamp = ts + + // then just a matter of + // validate params + // do db updates + // some challenge bus stuff? + + if action.Metadata != "" && strings.HasPrefix(action.Metadata, "Qm") { + j, err := cf.Fetch(action.UserID, action.Metadata) + if err != nil { + config.Logger.Warn(err.Error()) + } else { + action.MetadataJSON = j + } + } + + processEntityManagerAction(action) + + } +} diff --git a/comms/em/cid.go b/comms/em/cid.go new file mode 100644 index 00000000000..03a990b9b7b --- /dev/null +++ b/comms/em/cid.go @@ -0,0 +1,47 @@ +package em + +import ( + "fmt" + "io" + "net/http" + + "comms.audius.co/config" + "comms.audius.co/peering" +) + +type CidFetcher struct { + sps []peering.ServiceNode +} + +func NewCidFetcher() (*CidFetcher, error) { + sps, err := peering.GetContentNodes() + if err != nil { + return nil, err + } + cf := &CidFetcher{ + sps: sps, + } + return cf, nil +} + +func (cf *CidFetcher) Fetch(userId int64, cid string) ([]byte, error) { + + // TODO: should lookup replica set for this userId + // fmt.Println(" fetch cid", userId, cid) + + for _, sp := range cf.sps { + u := sp.Endpoint + "/content/" + cid + resp, err := http.Get(u) + if err != nil { + config.Logger.Debug(u, "err", err) + continue + } + + body, err := io.ReadAll(resp.Body) + if resp.StatusCode == 200 { + return body, err + } + } + + return nil, fmt.Errorf("cid not found: %s", cid) +} diff --git a/comms/em/em.go b/comms/em/em.go new file mode 100644 index 00000000000..ad609ce7cdd --- /dev/null +++ b/comms/em/em.go @@ -0,0 +1,77 @@ +package em + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "time" +) + +type IdentityRelayMessage struct { + ContractRegistryKey string `json:"contractRegistryKey"` + ContractAddress string `json:"contractAddress"` + SenderAddress string `json:"senderAddress"` + EncodedABI string `json:"encodedABI"` +} + +type EntityManagerAction struct { + UserID int64 + Action string + EntityType string + EntityID int64 + Metadata string + + // the raw json fetched from the Metadata CID + MetadataJSON json.RawMessage + + // wallet address for UserID + // for "on chain" it's valid + // for "off chain" we need to do eip712 recovery + Signer string + + // either the blocktime, or the nats jetstream time + Timestamp time.Time + + // these are populated if it came from chain + blockNumber int + blockhash string + txHash string + txIndex int +} + +func (a EntityManagerAction) String() string { + return fmt.Sprintf("user=%d action=%s type=%s id=%d meta=%s at=%s", a.UserID, a.Action, a.EntityType, a.EntityID, a.Metadata, a.Timestamp) +} + +// this deals with the encoded abi that is the tx input +// if reading from the chain we can use the emitted log event +// but here we parse the encoded abi +func UnpackEntityManagerParams(txInput string) (map[string]interface{}, error) { + + // decode txInput method signature + decodedSig, err := hex.DecodeString(txInput[2:10]) + if err != nil { + return nil, err + } + + // recover Method from signature and ABI + method, err := emAbi.MethodById(decodedSig) + if err != nil { + return nil, err + } + + // decode txInput Payload + decodedData, err := hex.DecodeString(txInput[10:]) + if err != nil { + return nil, err + } + + m := map[string]interface{}{} + err = method.Inputs.UnpackIntoMap(m, decodedData) + if err != nil { + return nil, err + } + + return m, nil + +} diff --git a/comms/em/metadata.go b/comms/em/metadata.go new file mode 100644 index 00000000000..7ba4e634420 --- /dev/null +++ b/comms/em/metadata.go @@ -0,0 +1,88 @@ +package em + +type UserMetadata struct { + IsVerified bool `json:"is_verified"` + IsDeactivated bool `json:"is_deactivated"` + Name string `json:"name"` + Handle string `json:"handle"` + ProfilePicture interface{} `json:"profile_picture"` + ProfilePictureSizes string `json:"profile_picture_sizes"` + CoverPhoto interface{} `json:"cover_photo"` + CoverPhotoSizes string `json:"cover_photo_sizes"` + Bio string `json:"bio"` + Location string `json:"location"` + ArtistPickTrackID interface{} `json:"artist_pick_track_id"` + CreatorNodeEndpoint string `json:"creator_node_endpoint"` + AssociatedWallets struct { + ZeroX05A933CDcABF2FCF40FFc2Ce1B7FD63F74F16Cd2 struct { + Signature string `json:"signature"` + } `json:"0x05A933CDcABF2fCF40fFc2ce1b7fD63F74F16cd2"` + } `json:"associated_wallets"` + AssociatedSolWallets struct { + EGTKtB1GqwUTCocLPFuo56RootQTEp7R7PRQS7YTyTvf struct { + Signature string `json:"signature"` + } `json:"EGTKtB1gqwUTCocLPFuo56rootQTEp7r7PRQS7YTyTvf"` + } `json:"associated_sol_wallets"` + Collectibles interface{} `json:"collectibles"` + PlaylistLibrary struct { + Contents []struct { + PlaylistID int `json:"playlist_id"` + Type string `json:"type"` + } `json:"contents"` + } `json:"playlist_library"` + Events struct { + IsMobileUser bool `json:"is_mobile_user"` + } `json:"events"` + UserID int `json:"user_id"` +} + +type TrackMetadata struct { + OwnerID int `json:"owner_id"` + Title string `json:"title"` + Length interface{} `json:"length"` + CoverArt interface{} `json:"cover_art"` + CoverArtSizes string `json:"cover_art_sizes"` + Tags interface{} `json:"tags"` + Genre string `json:"genre"` + Mood interface{} `json:"mood"` + CreditsSplits interface{} `json:"credits_splits"` + CreatedAt interface{} `json:"created_at"` + CreateDate interface{} `json:"create_date"` + UpdatedAt interface{} `json:"updated_at"` + ReleaseDate string `json:"release_date"` + FileType interface{} `json:"file_type"` + TrackSegments []struct { + Multihash string `json:"multihash"` + Duration float64 `json:"duration"` + } `json:"track_segments"` + HasCurrentUserReposted bool `json:"has_current_user_reposted"` + FolloweeReposts []interface{} `json:"followee_reposts"` + FolloweeSaves []interface{} `json:"followee_saves"` + IsCurrent bool `json:"is_current"` + IsUnlisted bool `json:"is_unlisted"` + IsPremium bool `json:"is_premium"` + PremiumConditions interface{} `json:"premium_conditions"` + FieldVisibility struct { + Genre bool `json:"genre"` + Mood bool `json:"mood"` + Tags bool `json:"tags"` + Share bool `json:"share"` + PlayCount bool `json:"play_count"` + Remixes bool `json:"remixes"` + } `json:"field_visibility"` + RemixOf interface{} `json:"remix_of"` + RepostCount int `json:"repost_count"` + SaveCount int `json:"save_count"` + Description interface{} `json:"description"` + License string `json:"license"` + Isrc interface{} `json:"isrc"` + Iswc interface{} `json:"iswc"` + Download interface{} `json:"download"` + Artwork struct { + URL string `json:"url"` + File struct { + } `json:"file"` + Source string `json:"source"` + } `json:"artwork"` + StemOf interface{} `json:"stem_of"` +} diff --git a/comms/em/nats_test.go b/comms/em/nats_test.go new file mode 100644 index 00000000000..c216dce6962 --- /dev/null +++ b/comms/em/nats_test.go @@ -0,0 +1,90 @@ +package em + +import ( + "encoding/json" + "fmt" + "log" + "math/big" + "testing" + "time" + + "comms.audius.co/config" + "github.com/nats-io/nats.go" + "github.com/stretchr/testify/assert" +) + +var txInput = `0xd622c72d000000000000000000000000000000000000000000000000000000007dc490dc00000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000005ad00000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000160bc34971a7aa58d726c745d4dd989e046233e0a48e232c80dd9115f118b013ebd00000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000005547261636b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004536176650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041cc63c27f21b2ec8687e1f0168977400c379c2feaf8a4f8d9e9159ad9458a2de60bc3c0245d4bb2d8f2f69b9e547e80033e353c299f8ce05fb1781e1a7e87b74e1b00000000000000000000000000000000000000000000000000000000000000` + +func TestAbi(t *testing.T) { + params, err := UnpackEntityManagerParams(txInput) + assert.NoError(t, err) + fmt.Println(params) +} + +func TestNatsThing(t *testing.T) { + t.Skip() + + config.IsStaging = true // dagron + + cf, err := NewCidFetcher() + assert.NoError(t, err) + _ = cf + + // Connect to NATS + nc, err := nats.Connect(nats.DefaultURL) + assert.NoError(t, err) + + // Create JetStream Context + js, err := nc.JetStream(nats.PublishAsyncMaxPending(256)) + assert.NoError(t, err) + + // Simple Async Ephemeral Consumer + sub, err := js.SubscribeSync("audius.staging.>") + assert.NoError(t, err) + + for { + m, err := sub.NextMsg(time.Second) + if err != nil { + log.Println(err) + break + } + + var relay IdentityRelayMessage + json.Unmarshal(m.Data, &relay) + + params, err := UnpackEntityManagerParams(relay.EncodedABI) + assert.NoError(t, err) + // fmt.Println(relay.ContractAddress, params["_userId"], params["_action"], params["_entityType"], params["_entityId"], params["_metadata"]) + + action := EntityManagerAction{ + UserID: params["_userId"].(*big.Int).Int64(), + Action: params["_action"].(string), + EntityType: params["_entityType"].(string), + EntityID: params["_entityId"].(*big.Int).Int64(), + Metadata: params["_metadata"].(string), + + // todo: eip712 recover + verify this + Signer: relay.SenderAddress, + } + + natsMeta, err := m.Metadata() + if err != nil { + panic(err) + } + action.Timestamp = natsMeta.Timestamp + + // todo: combine with identitcal code in em_chain_test.go + if action.Metadata != "" && action.EntityType != "UserReplicaSet" { + j, err := cf.Fetch(action.UserID, action.Metadata) + if err != nil { + config.Logger.Warn(err.Error()) + } else { + action.MetadataJSON = j + } + } + + } + + sub.Unsubscribe() + +} diff --git a/comms/em/processor.go b/comms/em/processor.go new file mode 100644 index 00000000000..8e08a0de09d --- /dev/null +++ b/comms/em/processor.go @@ -0,0 +1,31 @@ +package em + +import ( + "comms.audius.co/config" + "comms.audius.co/db" +) + +func processEntityManagerAction(action *EntityManagerAction) { + logger := config.Logger.New("user_id", action.UserID, "action", action.Action+action.EntityType, "entity_id", action.EntityID) + + tx, err := db.Conn.Begin() + if err != nil { + panic(err) + } + defer tx.Rollback() + + fqmethod := action.Action + action.EntityType + + switch fqmethod { + // case "UpdateUser": + // case "CreateTrack", "UpdateTrack": + default: + logger.Info("no handler for: " + fqmethod) + } + + err = tx.Commit() + if err != nil { + panic(err) + } + +} diff --git a/comms/example/deno_client.ts b/comms/example/deno_client.ts new file mode 100644 index 00000000000..c368024142d --- /dev/null +++ b/comms/example/deno_client.ts @@ -0,0 +1,364 @@ +import * as secp from "npm:@noble/secp256k1"; +import * as aes from "npm:micro-aes-gcm"; +import { Address } from "npm:micro-eth-signer"; +import { keccak_256 } from "npm:@noble/hashes/sha3"; +import { base64 } from "npm:@scure/base"; +import * as cuid from "npm:cuid"; +import { + ChatSayParams, + ChatCreateParams, + ChatReadParams, + MutationMethod, + QueryMethod, +} from "../schema/schema.ts"; + +type KeyPair = { + privateKey: Uint8Array; + publicKey: Uint8Array; + walletAddress: string; +}; + +function generatePrivateKey(): KeyPair { + const privateKey = secp.utils.randomPrivateKey(); + const publicKey = secp.getPublicKey(privateKey); + const walletAddress = Address.fromPrivateKey(privateKey); + + return { + privateKey, + publicKey, + walletAddress, + }; +} + +// function printKeyPair(kp: KeyPair) { +// console.log("----"); +// console.log(" privateKey:", bytesToHex(kp.privateKey)); +// console.log(" publicKey:", bytesToHex(kp.publicKey)); +// console.log(" walletAddress:", kp.walletAddress); +// console.log("----"); +// } + +async function signedJson(obj: unknown, privateKey: Uint8Array) { + const payload = JSON.stringify(obj); + + const msgHash = keccak_256(payload); + const [sig, recid] = await secp.sign(msgHash, privateKey, { + recovered: true, + der: false, + }); + + const sigBytes = new Uint8Array(65); + sigBytes.set(sig, 0); + sigBytes[64] = recid; + + return [payload, base64.encode(sigBytes)]; +} + +// ------------------------------------------------------------------------------- + +type RPC = { + method: QueryMethod | MutationMethod; + params: Record; +}; + +class RPCClient { + host = "http://localhost:8925"; + + // cache of chat secrets (should be a promise for lookup) + private chatSecrets: Record> = {}; + + // cache of user pubkey lookups + userPubkeys: Record> = {}; + + constructor(public userId: number, private kp: KeyPair) {} + + publicKey() { + return this.kp.publicKey; + } + + // fetch stuff + mutate(rpc: RPC) { + const endpoint = `${this.host}/comms/mutate`; + return this.post(endpoint, rpc); + } + + query(rpc: RPC) { + const endpoint = `${this.host}/comms/query`; + return this.post(endpoint, rpc); + } + + // chat specific apis + async chatCreate(userIds: number[]) { + // ensure my userId is in the list + if (userIds.indexOf(this.userId) == -1) { + userIds.push(this.userId); + } + + const chatId = cuid.default(); + + // if we wanted to de-dupe chats the id could be derived from members + // but this makes testing harder atm + // userIds.sort(); + // const chatId = userIds.join(","); + + const chatSecret = secp.utils.randomPrivateKey(); + + // a stateful chat client should store `chatId: chatSecret` + // for reading + sending messages later + // might make sense to use react-query for this + this.chatSecrets[chatId] = Promise.resolve(chatSecret); + + // for each invited user: + // get pubkey + // build invite code + const invites = await Promise.all( + userIds.map(async (userId) => { + // todo: need to handle errors and filter userIds for those with valid pubkey + // todo: would also be nice to have a bulk endpoint here + const pubkey = await this.getPubkey(userId); + const inviteCode = base64.encode( + await this.makeInviteCode(pubkey, chatSecret) + ); + return { userId, inviteCode }; + }) + ); + + const params: ChatCreateParams = { + chatId, + invites, + }; + + await this.mutate({ + method: "chat.create", + params, + }); + + return chatId; + } + + async chatSay(chatId: string, message: string) { + // need to get my invite for chatId + // recover the chat secret + // use it to encrypt message + + const chatSecret = await this.getChatSecret(chatId); + + const params: ChatSayParams = { + chatId: chatId, + ciphertext: base64.encode(await aes.encrypt(chatSecret, message)), + }; + + this.mutate({ + method: "chat.say", + params, + }); + } + + async chatRead(chatId: string) { + const params: ChatReadParams = { + chatId: chatId + } + + this.mutate({ + method: "chat.read", + params + }) + } + + // key stuff + encryptFor(friendPublicKey: Uint8Array, payload: Uint8Array) { + const shared = this.sharedSecret(friendPublicKey); + return aes.encrypt(shared, payload); + } + + decryptFrom(friendPublicKey: Uint8Array, payload: Uint8Array) { + const shared = this.sharedSecret(friendPublicKey); + if (!shared) { + console.log("no shared secret from", friendPublicKey); + } + return aes.decrypt(shared, payload); + } + + async makeInviteCode(friendPubkey: Uint8Array, payload: Uint8Array) { + const encrypted = await this.encryptFor(friendPubkey, payload); + const packed = new Uint8Array(65 + encrypted.length); + packed.set(this.kp.publicKey); + packed.set(encrypted, 65); + return packed; + } + + readInviteCode(inviteCode: Uint8Array) { + const friend = inviteCode.slice(0, 65); + const code = inviteCode.slice(65); + return this.decryptFrom(friend, code); + } + + private sharedSecret(friendPublicKey: Uint8Array) { + const shared = secp.getSharedSecret( + this.kp.privateKey, + friendPublicKey, + true + ); + return shared.slice(shared.length - 32); + } + + private getPubkey(userId: number): Promise { + if (!this.userPubkeys[userId]) { + this.userPubkeys[userId] = this.fetchPubkey(userId); + } + return this.userPubkeys[userId]; + } + + private async fetchPubkey(userId: number) { + const resp = await fetch(`${this.host}/comms/pubkey/${userId}`); + const txt = await resp.text(); + return base64.decode(txt); + } + + private getChatSecret(chatId: string): Promise { + if (!this.chatSecrets[chatId]) { + this.chatSecrets[chatId] = this.query({ + method: "chat.get", + params: { chatId }, + }).then((data) => { + return this.readInviteCode(base64.decode(data.invite_code)); + }); + } + return this.chatSecrets[chatId]; + } + + private async post(endpoint: string, rpc: RPC) { + // "lax mode" hack because wallets are not in db + // normally the server would read sig, find wallet, find user + // this allows the client to simply specify a user ID + // since there's no easy way for deno client to "seed" db with users + // that it has the keypair for... but I have an idea about how to do that. + // + // also this is inlined to the JSON body (instead of being a header) because the nats processor uses it also. + // it could still be a header if the `mutate` endpoint took the HTTP header and copied it to NATS message header. + rpc.params.tempUserId = this.userId; + + const [payload, sigBase64] = await signedJson(rpc, this.kp.privateKey); + + const resp = await fetch(endpoint, { + method: "POST", + headers: { + "x-sig": sigBase64, + }, + body: payload, + }); + + if (resp.status != 200) { + const txt = await resp.text(); + const message = [endpoint, resp.status, txt].join(" "); + throw new Error(message); + } + + return await resp.json(); + } +} + +// ------------------------------------------------------------- + +// need two users with keys in the db that can be recovered / verified +// steve = 91 +// dave = 92 +// steve creates chat +// steve invites dave +// sends message +// check counts, etc. + +const steve = new RPCClient(91, generatePrivateKey()); +const dave = new RPCClient(92, generatePrivateKey()); +const bill = new RPCClient(93, generatePrivateKey()); + +// hack to socialize public keys between all our users +// since deno client doesn't have an easy way to create test users +const allUsers = [steve, dave, bill]; +for (const u of allUsers) { + for (const v of allUsers) { + u.userPubkeys[v.userId] = Promise.resolve(v.publicKey()); + } +} + +const chatId = await steve.chatCreate([91, 92]); +await new Promise((r) => setTimeout(r, 1000)); + +await steve.chatSay(chatId, "hello dave"); + +await dave.chatSay(chatId, "hi there steve"); + +await dave.chatSay(chatId, "hello???"); + +await new Promise((r) => setTimeout(r, 1000)); + +await steve.chatRead(chatId) + +await new Promise((r) => setTimeout(r, 1000)); + +await dave.chatSay(chatId, "heyyyyyyyyy"); + +await new Promise((r) => setTimeout(r, 500)); + +// list latest chat for users +for (const client of [steve, dave]) { + const userId = client.userId; + console.log(`------------- ${userId}`); + + const threads = await client.query({ + method: "chat.list", + params: {}, + }); + const lastThread = threads[threads.length - 1]; + + if (lastThread.chat_id !== chatId) { + console.log("got different thread", chatId); + } + console.log(`${userId} lastThread`, lastThread); + + // the thread invite_code is packed structure with: (invitor_public_key, encrypted_shared_secret) + // use the invitor_public_key to get a secp256k1 shared secret + // use that shared secret to decrypt the chat shared secret + const sharedSecret2 = await client.readInviteCode( + base64.decode(lastThread.invite_code) + ); + + console.log("invite", lastThread.invite_code.length); + + const messages = await client.query({ + method: "chat.messages", + params: { + chatId: chatId, + }, + }); + + for (const message of messages) { + // use sharedSecret2 recovered from invite_code to decrypt messages + const decrypted = await aes.decrypt( + sharedSecret2, + base64.decode(message.ciphertext) + ); + const cleartext = new TextDecoder().decode(decrypted); + + console.log(message.user_id, ":", cleartext); + } +} + +// test validator by sending an invalid payload +try { + await dave.mutate({ + method: "chat.say", + params: { + foo: "bar", + }, + }); +} catch (e) { + console.log("OK validator rejection: ", e.message); +} + +// test validator: non-member trys to chat in a thread +try { + await bill.chatSay(chatId, "bill here crashing the party"); +} catch (e) { + console.log("OK validator rejection: ", e.message); +} diff --git a/comms/go.mod b/comms/go.mod new file mode 100644 index 00000000000..ad34d8d0641 --- /dev/null +++ b/comms/go.mod @@ -0,0 +1,66 @@ +module comms.audius.co + +go 1.19 + +require ( + github.com/ethereum/go-ethereum v1.10.26 + github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac + github.com/labstack/echo/v4 v4.9.1 + github.com/lib/pq v1.10.7 + github.com/nats-io/nats-server/v2 v2.9.7 + github.com/nats-io/nats.go v1.19.0 + github.com/nats-io/nkeys v0.3.0 + github.com/spf13/cast v1.5.0 + github.com/stretchr/testify v1.8.1 + github.com/tidwall/pretty v1.2.1 + golang.org/x/sync v0.1.0 +) + +require ( + github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/go-ole/go-ole v1.2.1 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.13.0 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.1 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.12.0 // indirect + github.com/jackc/pgx/v4 v4.17.2 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/klauspost/compress v1.15.11 // indirect + github.com/labstack/gommon v0.4.0 // indirect + github.com/mattn/go-colorable v0.1.11 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/minio/highwayhash v1.0.2 // indirect + github.com/nats-io/jwt/v2 v2.3.0 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/speps/go-hashids/v2 v2.0.1 // indirect + github.com/tabbed/pqtype v0.1.1 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tklauser/go-sysconf v0.3.5 // indirect + github.com/tklauser/numcpus v0.2.2 // indirect + github.com/urfave/cli/v2 v2.20.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect + golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect + golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect + golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/guregu/null.v4 v4.0.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/comms/go.sum b/comms/go.sum new file mode 100644 index 00000000000..4c292ac2cb6 --- /dev/null +++ b/comms/go.sum @@ -0,0 +1,313 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +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/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +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= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= +github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s= +github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= +github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= +github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac h1:n1DqxAo4oWPMvH1+v+DLYlMCecgumhhgnxAPdqDIFHI= +github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +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.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= +github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= +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/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 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +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.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= +github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +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.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= +github.com/jackc/pgtype v1.12.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.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= +github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= +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/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +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.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.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +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/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y= +github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +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/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/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-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +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-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/nats-io/jwt/v2 v2.3.0 h1:z2mA1a7tIf5ShggOFlR1oBPgd6hGqcDYsISxZByUzdI= +github.com/nats-io/jwt/v2 v2.3.0/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= +github.com/nats-io/nats-server/v2 v2.9.7 h1:VBlfq7xvv/72v0mzGZ2rgsDzUoVyX2Xhssl9XpKDue0= +github.com/nats-io/nats-server/v2 v2.9.7/go.mod h1:AB6hAnGZDlYfqb7CTAm66ZKMZy9DpfierY1/PbpvI2g= +github.com/nats-io/nats.go v1.19.0 h1:H6j8aBnTQFoVrTGB6Xjd903UMdE7jz6DS4YkmAqgZ9Q= +github.com/nats-io/nats.go v1.19.0/go.mod h1:tLqubohF7t4z3du1QDPYJIQQyhb4wl6DhjxEajSI7UA= +github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= +github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +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/speps/go-hashids/v2 v2.0.1 h1:ViWOEqWES/pdOSq+C1SLVa8/Tnsd52XC34RY7lt7m4g= +github.com/speps/go-hashids/v2 v2.0.1/go.mod h1:47LKunwvDZki/uRVD6NImtyk712yFzIs3UF3KlHohGw= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57NRNuZ2d3rmvB3pcmbu7O1RS3m8WRx7ilrg= +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/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/tabbed/pqtype v0.1.1 h1:PhEcb9JZ8jr7SUjJDFjRPxny0M8fkXZrxn/a9yQfoZg= +github.com/tabbed/pqtype v0.1.1/go.mod h1:HLt2kLJPcUhODQkYn3mJkMHXVsuv3Z2n5NZEeKXL0Uk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4= +github.com/urfave/cli/v2 v2.20.2 h1:dKA0LUjznZpwmmbrc0pOgcLTEilnHeM8Av9Yng77gHM= +github.com/urfave/cli/v2 v2.20.2/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +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-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= +golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/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-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-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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/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/text v0.3.0/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.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-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +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-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= +gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/comms/internal/pubkeystore/abi.go b/comms/internal/pubkeystore/abi.go new file mode 100644 index 00000000000..268e7e6e729 --- /dev/null +++ b/comms/internal/pubkeystore/abi.go @@ -0,0 +1,88 @@ +package pubkeystore + +import ( + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +var addUserAbiJson = ` +[ + { + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_handle", + "type": "bytes16" + }, + { + "name": "_nonce", + "type": "bytes32" + }, + { + "name": "_subjectSig", + "type": "bytes" + } + ], + "name": "addUser", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_userId", + "type": "uint256" + }, + { + "name": "_entityType", + "type": "string" + }, + { + "name": "_entityId", + "type": "uint256" + }, + { + "name": "_action", + "type": "string" + }, + { + "name": "_metadata", + "type": "string" + }, + { + "name": "_nonce", + "type": "bytes32" + }, + { + "name": "_subjectSig", + "type": "bytes" + } + ], + "name": "manageEntity", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } +] +` + +var addUserAbi abi.ABI + +func init() { + var err error + addUserAbi, err = abi.JSON(strings.NewReader(addUserAbiJson)) + if err != nil { + panic(err) + } +} diff --git a/comms/internal/pubkeystore/recover.go b/comms/internal/pubkeystore/recover.go new file mode 100644 index 00000000000..ec76ed51227 --- /dev/null +++ b/comms/internal/pubkeystore/recover.go @@ -0,0 +1,178 @@ +package pubkeystore + +import ( + "context" + "errors" + "fmt" + "math/big" + "strings" + + "comms.audius.co/config" + "comms.audius.co/db" + "comms.audius.co/peering" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/signer/core/apitypes" +) + +var ( + poaClient *ethclient.Client + + // on staging, and soon prod + // we will need two clients: + // a POA client for older users + // an audius chain client for newer users + finalPoaBlock int64 = 0 + audiusChainClient *ethclient.Client +) + +func Dial() error { + var err error + endpoint := "https://poa-gateway.audius.co" + + if config.IsStaging { + endpoint = "https://legacy-poa-gateway.staging.audius.co" + + // should get dynamically from + // https://identityservice.staging.audius.co/health_check/poa + finalPoaBlock = 30000000 + + audiusChainClient, err = ethclient.Dial("https://poa-gateway.staging.audius.co") + if err != nil { + return err + } + } + + poaClient, err = ethclient.Dial(endpoint) + return err +} + +func RecoverUserPublicKeyBase64(ctx context.Context, userId int) (string, error) { + var err error + + logger := config.Logger.New("module", "pubkeystore", "userId", userId) + + conn := db.Conn + + kv, err := peering.JetstreamClient.KeyValue(config.PubkeystoreBucketName) + if err != nil { + return "", err + } + + key := fmt.Sprintf("userId=%d", userId) + + // first check a "pubkey cache" for a hit + // see: https://github.com/AudiusProject/audius-docker-compose/blob/nats/discovery-provider/clusterizer/src/recover.ts#L65 + if got, err := kv.Get(key); err == nil { + logger.Debug("pubkey cache hit") + return string(got.Value()), nil + } + + query := ` + select wallet, blocknumber, txhash + from users where user_id = $1 and is_current in (true, false) + order by blocknumber asc; + ` + + rows, err := conn.QueryContext(ctx, query, userId) + if err != nil { + return "", err + } + defer rows.Close() + + var wallet string + var blocknumber int64 + var txhash string + + var pubkeyBase64 string + + for rows.Next() { + err = rows.Scan(&wallet, &blocknumber, &txhash) + if err != nil { + continue + } + + // a couple possible situations: + // - addUser on POA + // - entity manager on POA + // - entity manager on audius chain + + if finalPoaBlock != 0 && blocknumber > finalPoaBlock { + // EM on Audius Chain + pubkeyBase64, err = recoverEntityManagerPubkey(audiusChainClient, txhash, wallet) + if err == nil { + break + } + } else { + // try EM on POA + if strings.HasPrefix(txhash, "0x") { + pubkeyBase64, err = recoverEntityManagerPubkey(poaClient, txhash, wallet) + if err == nil { + break + } + } + + // try addUser on POA + pubkeyBase64, err = findAddUserTransaction(ctx, big.NewInt(blocknumber)) + if err == nil { + break + } + } + + } + + // success + if err == nil && pubkeyBase64 != "" { + _, err = kv.PutString(key, pubkeyBase64) + if err != nil { + logger.Warn("kv set failed", "err", err) + } + + logger.Debug("recovered pubkey OK") + return pubkeyBase64, nil + } + + logger.Warn("failed to recover pubkey for user", "err", err) + return "", errors.New("could not recover pubkey") +} + +func unpackTransactionInput(txData []byte) (map[string]interface{}, error) { + method, err := addUserAbi.MethodById(txData) + if err != nil { + return nil, err + } + + m := map[string]interface{}{} + err = method.Inputs.UnpackIntoMap(m, txData[4:]) + if err != nil { + return nil, err + } + + return m, nil +} + +// taken from: +// https://gist.github.com/APTy/f2a6864a97889793c587635b562c7d72#file-main-go +func recoverPublicKey(signature []byte, typedData apitypes.TypedData) ([]byte, error) { + + domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map()) + if err != nil { + return nil, fmt.Errorf("eip712domain hash struct: %w", err) + } + + typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message) + if err != nil { + return nil, fmt.Errorf("primary type hash struct: %w", err) + } + + // add magic string prefix + rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash))) + sighash := crypto.Keccak256(rawData) + + // update the recovery id + // https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442 + signature[64] -= 27 + + return crypto.Ecrecover(sighash, signature) + +} diff --git a/comms/internal/pubkeystore/recover_add_user.go b/comms/internal/pubkeystore/recover_add_user.go new file mode 100644 index 00000000000..11201e000c9 --- /dev/null +++ b/comms/internal/pubkeystore/recover_add_user.go @@ -0,0 +1,114 @@ +package pubkeystore + +import ( + "context" + "encoding/base64" + "errors" + "math/big" + + "comms.audius.co/config" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" +) + +func findAddUserTransaction(ctx context.Context, blockNumber *big.Int) (string, error) { + var chainId int64 = 99 + verifyingContract := "0x981c44040cb6150a2b8a7f63fb182760505bf666" + + if config.IsStaging { + chainId = 77 + verifyingContract = "0x39d26a6a138ddf8b447d651d5d3883644d277251" + } + + block, err := poaClient.BlockByNumber(ctx, blockNumber) + if err != nil { + return "", err + } + + var typedData = apitypes.TypedData{ + Types: apitypes.Types{ + "EIP712Domain": []apitypes.Type{ + { + Name: "name", + Type: "string", + }, + { + Name: "version", + Type: "string", + }, + { + Name: "chainId", + Type: "uint256", + }, + { + Name: "verifyingContract", + Type: "address", + }, + }, + "AddUserRequest": []apitypes.Type{ + { + Name: "handle", + Type: "bytes16", + }, + { + Name: "nonce", + Type: "bytes32", + }, + }, + }, + Domain: apitypes.TypedDataDomain{ + Name: "User Factory", + Version: "1", + ChainId: math.NewHexOrDecimal256(chainId), + VerifyingContract: verifyingContract, + }, + PrimaryType: "AddUserRequest", + Message: map[string]interface{}{}, + } + + // try each transaction in this block to find the one that checks out + // skip any that fail + for _, tx := range block.Transactions() { + + params, err := unpackTransactionInput(tx.Data()) + if err != nil { + continue + } + + // we have loaded both ABIs in our ABI instance + // so it's possible to successfully decode a EM tx when we are looking for an addUser + // if the params contains _entityId it is not the tx we are looking for + if _, ok := params["_entityId"]; ok { + continue + } + + owner := params["_owner"].(common.Address) + handle := params["_handle"].([16]byte) + nonce := params["_nonce"].([32]byte) + subjectSig := params["_subjectSig"].([]byte) + + typedData.Message["handle"] = handle[:] + typedData.Message["nonce"] = nonce[:] + + pubkeyBytes, err := recoverPublicKey(subjectSig, typedData) + if err != nil { + continue + } + + pubkey, err := crypto.UnmarshalPubkey(pubkeyBytes) + if err != nil { + continue + } + address := crypto.PubkeyToAddress(*pubkey) + + if address == owner { + // success + return base64.StdEncoding.EncodeToString(pubkeyBytes), nil + } + + } + + return "", errors.New("not found") +} diff --git a/comms/internal/pubkeystore/recover_entity_manager.go b/comms/internal/pubkeystore/recover_entity_manager.go new file mode 100644 index 00000000000..ca3e31349b6 --- /dev/null +++ b/comms/internal/pubkeystore/recover_entity_manager.go @@ -0,0 +1,131 @@ +package pubkeystore + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/signer/core/apitypes" +) + +func recoverEntityManagerPubkey(ethClient *ethclient.Client, txhash string, wallet string) (string, error) { + ctx := context.Background() + + tx, _, err := ethClient.TransactionByHash(ctx, common.HexToHash(txhash)) + if err != nil { + return "", err + } + + chainId, err := ethClient.ChainID(ctx) + if err != nil { + return "", err + } + + params, err := unpackTransactionInput(tx.Data()) + if err != nil { + return "", err + } + + // since both ABIs are loaded... check that we have an entity manager tx + // and not a UserReplicaSet... which is signed by a different keypair + entityType, ok := params["_entityType"] + if !ok { + return "", errors.New("not an entity manager txn") + } + if entityType == "UserReplicaSet" { + return "", errors.New("tx not signed by user keypair") + } + + nonce := params["_nonce"].([32]byte) + subjectSig := params["_subjectSig"].([]byte) + + var typedData = apitypes.TypedData{ + Types: apitypes.Types{ + "EIP712Domain": []apitypes.Type{ + { + Name: "name", + Type: "string", + }, + { + Name: "version", + Type: "string", + }, + { + Name: "chainId", + Type: "uint256", + }, + { + Name: "verifyingContract", + Type: "address", + }, + }, + "ManageEntity": []apitypes.Type{ + { + Name: "userId", + Type: "uint", + }, + { + Name: "entityType", + Type: "string", + }, + { + Name: "entityId", + Type: "uint", + }, + { + Name: "action", + Type: "string", + }, + { + Name: "metadata", + Type: "string", + }, + { + Name: "nonce", + Type: "bytes32", + }, + }, + }, + Domain: apitypes.TypedDataDomain{ + Name: "Entity Manager", + Version: "1", + ChainId: math.NewHexOrDecimal256(chainId.Int64()), + VerifyingContract: tx.To().Hex(), + }, + PrimaryType: "ManageEntity", + Message: map[string]interface{}{ + "userId": params["_userId"].(*big.Int).String(), + "entityType": params["_entityType"], + "entityId": params["_entityId"].(*big.Int).String(), + "action": params["_action"], + "metadata": params["_metadata"], + "nonce": nonce[:], + }, + } + + pubkeyBytes, err := recoverPublicKey(subjectSig, typedData) + if err != nil { + return "", err + } + + pubkey, err := crypto.UnmarshalPubkey(pubkeyBytes) + if err != nil { + return "", err + } + + address := crypto.PubkeyToAddress(*pubkey) + + if !strings.EqualFold(address.String(), wallet) { + fmt.Println(params, address, wallet) + return "", errors.New("wallets don't match") + } + + return base64.StdEncoding.EncodeToString(pubkeyBytes), nil +} diff --git a/comms/internal/pubkeystore/recover_test.go b/comms/internal/pubkeystore/recover_test.go new file mode 100644 index 00000000000..1aa027ab2c9 --- /dev/null +++ b/comms/internal/pubkeystore/recover_test.go @@ -0,0 +1,53 @@ +package pubkeystore + +import ( + "context" + "fmt" + "math/big" + "testing" + + "comms.audius.co/config" + "github.com/stretchr/testify/assert" +) + +func TestRecovery(t *testing.T) { + t.Skip() + + var err error + + // dagron + config.IsStaging = true + err = Dial() + assert.NoError(t, err) + + // addUser on POA + { + + blocknumber := big.NewInt(14412789) + + pk, err := findAddUserTransaction(context.Background(), blocknumber) + assert.NoError(t, err) + fmt.Println(pk) + } + + // EM on POA + { + txhash := "0x784b1cbd5dbead07ea78fc0dba65cccd0582be6ec41413a8c73546a3dcaa0c28" + wallet := "0xd3a17ed773a5479097df5b41b15f798affa6040f" + + pk, err := recoverEntityManagerPubkey(poaClient, txhash, wallet) + assert.NoError(t, err) + fmt.Println(pk) + } + + // EM on audius chain + { + txhash := "0xec9daecc8269b9629baff0e53abaaa8e1bced65fb3fa48aa1950bfe8ada4f075" + wallet := "0xbb70390859ce84afc5d47c2eea6c89462faa6c7e" + + pk, err := recoverEntityManagerPubkey(audiusChainClient, txhash, wallet) + assert.NoError(t, err) + fmt.Println(pk) + } + +} diff --git a/comms/internal/rpcz/apply.go b/comms/internal/rpcz/apply.go new file mode 100644 index 00000000000..194d246ae1f --- /dev/null +++ b/comms/internal/rpcz/apply.go @@ -0,0 +1,165 @@ +package rpcz + +import ( + "context" + "encoding/json" + + "comms.audius.co/config" + "comms.audius.co/db" + "comms.audius.co/db/queries" + "comms.audius.co/misc" + "comms.audius.co/schema" + "github.com/nats-io/nats.go" +) + +// Validates + Applys a NATS message + +func Apply(msg *nats.Msg) { + var err error + logger := config.Logger.New() + + // get seq + meta, err := msg.Metadata() + if err != nil { + logger.Info("invalid nats message", err) + return + } + logger = logger.New("seq", meta.Sequence.Stream) + + // recover wallet + user + signatureHeader := msg.Header.Get(config.SigHeader) + wallet, err := misc.RecoverWallet(msg.Data, signatureHeader) + if err != nil { + logger.Warn("unable to recover wallet, skipping") + return + } + + userId, err := queries.GetUserIDFromWallet(db.Conn, context.Background(), wallet) + if err != nil { + logger.Warn("wallet not found: " + err.Error()) + return + } + logger = logger.New("wallet", wallet, "userId", userId) + + // parse raw rpc + var rawRpc schema.RawRPC + err = json.Unmarshal(msg.Data, &rawRpc) + if err != nil { + logger.Info(err.Error()) + return + } + + // call any validator + err = Validate(userId, rawRpc) + if err != nil { + logger.Info(err.Error()) + return + } + + for attempt := 1; attempt < 5; attempt++ { + + logger = logger.New("attempt", attempt) + + if err != nil { + logger.Warn(err.Error()) + } + + // write to db + tx := db.Conn.MustBegin() + if err != nil { + continue + } + + _, err = tx.Exec("insert into rpc_log (jetstream_sequence, jetstream_timestamp, from_wallet, rpc, sig) values($1, $2, $3, $4, $5)", meta.Sequence.Stream, meta.Timestamp, wallet, msg.Data, signatureHeader) + if err != nil { + continue + } + + switch schema.RPCMethod(rawRpc.Method) { + case schema.RPCMethodChatCreate: + var params schema.ChatCreateRPCParams + err = json.Unmarshal(rawRpc.Params, ¶ms) + if err != nil { + continue + } + err = chatCreate(tx, userId, meta.Timestamp, params) + case schema.RPCMethodChatDelete: + var params schema.ChatDeleteRPCParams + err = json.Unmarshal(rawRpc.Params, ¶ms) + if err != nil { + continue + } + err = chatDelete(tx, userId, params.ChatID, meta.Timestamp) + case schema.RPCMethodChatMessage: + var params schema.ChatMessageRPCParams + err = json.Unmarshal(rawRpc.Params, ¶ms) + if err != nil { + continue + } + err = chatSendMessage(tx, userId, params.ChatID, params.MessageID, meta.Timestamp, params.Message) + case schema.RPCMethodChatReact: + var params schema.ChatReactRPCParams + err = json.Unmarshal(rawRpc.Params, ¶ms) + if err != nil { + continue + } + err = chatReactMessage(tx, userId, params.MessageID, params.Reaction, meta.Timestamp) + case schema.RPCMethodChatRead: + var params schema.ChatReadRPCParams + err = json.Unmarshal(rawRpc.Params, ¶ms) + if err != nil { + continue + } + // do nothing if last active at >= message timestamp + lastActive, err := queries.LastActiveAt(tx, context.Background(), queries.ChatMembershipParams{ + ChatID: params.ChatID, + UserID: userId, + }) + if err != nil { + continue + } + if !lastActive.Valid || meta.Timestamp.After(lastActive.Time) { + err = chatReadMessages(tx, userId, params.ChatID, meta.Timestamp) + } + case schema.RPCMethodChatPermit: + var params schema.ChatPermitRPCParams + err = json.Unmarshal(rawRpc.Params, ¶ms) + if err != nil { + continue + } + err = chatSetPermissions(tx, userId, params.Permit) + case schema.RPCMethodChatBlock: + var params schema.ChatBlockRPCParams + err = json.Unmarshal(rawRpc.Params, ¶ms) + if err != nil { + continue + } + blockeeUserId, err := misc.DecodeHashId(params.UserID) + if err != nil { + continue + } + err = chatBlock(tx, userId, int32(blockeeUserId), meta.Timestamp) + case schema.RPCMethodChatUnblock: + var params schema.ChatUnblockRPCParams + err = json.Unmarshal(rawRpc.Params, ¶ms) + if err != nil { + continue + } + unblockedUserId, err := misc.DecodeHashId(params.UserID) + if err != nil { + continue + } + err = chatUnblock(tx, userId, int32(unblockedUserId)) + default: + logger.Warn("no handler for ", rawRpc.Method) + } + + err = tx.Commit() + if err != nil { + continue + } else { + break + } + } + +} diff --git a/comms/internal/rpcz/chat.go b/comms/internal/rpcz/chat.go new file mode 100644 index 00000000000..8bf44dec410 --- /dev/null +++ b/comms/internal/rpcz/chat.go @@ -0,0 +1,97 @@ +package rpcz + +import ( + "time" + + "comms.audius.co/misc" + "comms.audius.co/schema" + "github.com/jmoiron/sqlx" +) + +func chatCreate(tx *sqlx.Tx, userId int32, ts time.Time, params schema.ChatCreateRPCParams) error { + var err error + + _, err = tx.Exec("insert into chat (chat_id, created_at, last_message_at) values ($1, now(), now())", params.ChatID) + if err != nil { + return err + } + + for _, invite := range params.Invites { + invitedUserId, err := misc.DecodeHashId(invite.UserID) + if err != nil { + return err + } + // invited_by_user_id could also be the other user pubkey + // this would save client from having to look up pubkey in separate request + _, err = tx.Exec("insert into chat_member (chat_id, invited_by_user_id, invite_code, user_id) values ($1, $2, $3, $4)", + params.ChatID, userId, invite.InviteCode, invitedUserId) + if err != nil { + return err + } + } + + return err +} + +func chatDelete(tx *sqlx.Tx, userId int32, chatId string, messageTimestamp time.Time) error { + _, err := tx.Exec("update chat_member set cleared_history_at = $1 where chat_id = $2 and user_id = $3", messageTimestamp, chatId, userId) + return err +} + +func chatSendMessage(tx *sqlx.Tx, userId int32, chatId string, messageId string, messageTimestamp time.Time, ciphertext string) error { + var err error + + _, err = tx.Exec("insert into chat_message (message_id, chat_id, user_id, created_at, ciphertext) values ($1, $2, $3, $4, $5)", + messageId, chatId, userId, messageTimestamp, ciphertext) + if err != nil { + return err + } + + // update chat's last_message_at + _, err = tx.Exec("update chat set last_message_at = $1 where chat_id = $2", messageTimestamp, chatId) + if err != nil { + return err + } + + // sending a message implicitly marks activity for sender... + err = chatReadMessages(tx, userId, chatId, messageTimestamp) + if err != nil { + return err + } + + // update counts for non-sender (this could be a stored proc too) + _, err = tx.Exec("update chat_member set unread_count = unread_count + 1 where chat_id = $1 and user_id != $2 and (last_active_at is null or last_active_at < $3)", + chatId, userId, messageTimestamp) + + return err +} + +func chatReactMessage(tx *sqlx.Tx, userId int32, messageId string, reaction string, messageTimestamp time.Time) error { + _, err := tx.Exec("insert into chat_message_reactions (user_id, message_id, reaction, created_at, updated_at) values ($1, $2, $3, $4, $4) on conflict (user_id, message_id) do update set reaction = $3, updated_at = $4", userId, messageId, reaction, messageTimestamp) + if err != nil { + return err + } + + return err +} + +func chatReadMessages(tx *sqlx.Tx, userId int32, chatId string, readTimestamp time.Time) error { + _, err := tx.Exec("update chat_member set unread_count = 0, last_active_at = $1 where chat_id = $2 and user_id = $3", + readTimestamp, chatId, userId) + return err +} + +func chatSetPermissions(tx *sqlx.Tx, userId int32, permit schema.ChatPermission) error { + _, err := tx.Exec("insert into chat_permissions (user_id, permits) values ($1, $2) on conflict (user_id) do update set permits = $2", userId, permit) + return err +} + +func chatBlock(tx *sqlx.Tx, userId int32, blockeeUserId int32, messageTimestamp time.Time) error { + _, err := tx.Exec("insert into chat_blocked_users (blocker_user_id, blockee_user_id, created_at) values ($1, $2, $3) on conflict do nothing", userId, blockeeUserId, messageTimestamp) + return err +} + +func chatUnblock(tx *sqlx.Tx, userId int32, unblockedUserId int32) error { + _, err := tx.Exec("delete from chat_blocked_users where blocker_user_id = $1 and blockee_user_id = $2", userId, unblockedUserId) + return err +} diff --git a/comms/internal/rpcz/chat_block_test.go b/comms/internal/rpcz/chat_block_test.go new file mode 100644 index 00000000000..590a64f9f05 --- /dev/null +++ b/comms/internal/rpcz/chat_block_test.go @@ -0,0 +1,102 @@ +package rpcz + +import ( + "fmt" + "testing" + "time" + + "comms.audius.co/db" + "comms.audius.co/misc" + "comms.audius.co/schema" + "github.com/stretchr/testify/assert" +) + +func TestChatBlocking(t *testing.T) { + var err error + + // reset tables under test + _, err = db.Conn.Exec("truncate chat_blocked_users cascade") + assert.NoError(t, err) + _, err = db.Conn.Exec("truncate chat cascade") + assert.NoError(t, err) + + tx := db.Conn.MustBegin() + + // TODO test queries + + assertBlocked := func(blockerUserId int, blockeeUserId int, timestamp time.Time, expected int) { + row := tx.QueryRow("select count(*) from chat_blocked_users where blocker_user_id = $1 and blockee_user_id = $2 and created_at = $3", blockerUserId, blockeeUserId, timestamp) + var count int + err = row.Scan(&count) + assert.NoError(t, err) + assert.Equal(t, expected, count) + } + + // validate 91 can block 92 + { + hashUserId, err := misc.EncodeHashId(92) + assert.NoError(t, err) + exampleRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"user_id": "%s"}`, hashUserId)), + } + + chatBlock := string(schema.RPCMethodChatBlock) + + err = Validators[chatBlock](tx, 91, exampleRpc) + assert.NoError(t, err) + } + + // user 91 blocks 92 + messageTs := time.Now() + err = chatBlock(tx, 91, 92, messageTs) + assert.NoError(t, err) + assertBlocked(91, 92, messageTs, 1) + + // assert no update if duplicate block requests + duplicateMessageTs := time.Now() + err = chatBlock(tx, 91, 92, duplicateMessageTs) + assert.NoError(t, err) + assertBlocked(91, 92, messageTs, 1) + assertBlocked(91, 92, duplicateMessageTs, 0) + + // validate 91 and 92 cannot create a chat with each other + { + chatId := "chat1" + user91HashId, err := misc.EncodeHashId(91) + assert.NoError(t, err) + user92HashId, err := misc.EncodeHashId(92) + assert.NoError(t, err) + + exampleRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "invites": [{"user_id": "%s", "invite_code": "1"}, {"user_id": "%s", "invite_code": "2"}]}`, chatId, user91HashId, user92HashId)), + } + + chatCreate := string(schema.RPCMethodChatCreate) + + err = Validators[chatCreate](tx, 91, exampleRpc) + assert.ErrorContains(t, err, "Cannot create a chat with a user you have blocked or user who has blocked you") + } + + // validate 91 and 92 cannot message each other + { + // Assume chat was already created before blocking + chatId := "chat1" + SetUpChatWithMembers(t, tx, chatId, 91, 92) + + exampleRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message_id": "1", "message": "test"}`, chatId)), + } + + chatMessage := string(schema.RPCMethodChatMessage) + + err = Validators[chatMessage](tx, 91, exampleRpc) + assert.ErrorContains(t, err, "Cannot sent messages to users you have blocked or users who have blocked you") + } + + // user 91 unblocks 92 + err = chatUnblock(tx, 91, 92) + assert.NoError(t, err) + assertBlocked(91, 92, messageTs, 0) + + tx.Commit() +} diff --git a/comms/internal/rpcz/chat_delete_test.go b/comms/internal/rpcz/chat_delete_test.go new file mode 100644 index 00000000000..dadb8de5ba2 --- /dev/null +++ b/comms/internal/rpcz/chat_delete_test.go @@ -0,0 +1,65 @@ +package rpcz + +import ( + "database/sql" + "fmt" + "testing" + "time" + + "comms.audius.co/db" + "comms.audius.co/schema" + "github.com/stretchr/testify/assert" +) + +func TestChatDeletion(t *testing.T) { + var err error + + // reset tables under test + _, err = db.Conn.Exec("truncate chat cascade") + assert.NoError(t, err) + + tx := db.Conn.MustBegin() + + // TODO test queries + + chatId := "chat1" + SetUpChatWithMembers(t, tx, chatId, 91, 92) + + assertDeleted := func(chatId string, userId int, expectDeleted bool) { + row := tx.QueryRow("select cleared_history_at from chat_member where chat_id = $1 and user_id = $2", chatId, userId) + var clearedHistoryAt sql.NullTime + err = row.Scan(&clearedHistoryAt) + assert.NoError(t, err) + if expectDeleted { + assert.True(t, clearedHistoryAt.Valid) + } else { + assert.False(t, clearedHistoryAt.Valid) + } + } + + // validate 91 and 92 can delete their chats + { + exampleRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s"}`, chatId)), + } + + chatDelete := string(schema.RPCMethodChatDelete) + + err = Validators[chatDelete](tx, 91, exampleRpc) + assert.NoError(t, err) + + err = Validators[chatDelete](tx, 93, exampleRpc) + assert.ErrorIs(t, err, sql.ErrNoRows) + } + + // 91 deletes the chat + deleteTs := time.Now() + err = chatDelete(tx, 91, chatId, deleteTs) + assert.NoError(t, err) + assertDeleted(chatId, 91, true) + + // chat is not deleted for 92 + assertDeleted(chatId, 92, false) + + tx.Commit() +} diff --git a/comms/internal/rpcz/chat_permissions_test.go b/comms/internal/rpcz/chat_permissions_test.go new file mode 100644 index 00000000000..867ba5d4731 --- /dev/null +++ b/comms/internal/rpcz/chat_permissions_test.go @@ -0,0 +1,54 @@ +package rpcz + +import ( + "fmt" + "testing" + + "comms.audius.co/db" + "comms.audius.co/schema" + "github.com/stretchr/testify/assert" +) + +func TestChatPermissions(t *testing.T) { + var err error + + // reset tables under test + _, err = db.Conn.Exec("truncate chat_permissions cascade") + assert.NoError(t, err) + + tx := db.Conn.MustBegin() + + assertPermissions := func(userId int32, permits schema.ChatPermission, expected int) { + row := tx.QueryRow("select count(*) from chat_permissions where user_id = $1 and permits = $2", userId, permits) + var count int + err = row.Scan(&count) + assert.NoError(t, err) + assert.Equal(t, expected, count) + } + + // validate 91 can set permissions + { + exampleRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"permit": "all"}`)), + } + + chatPermit := string(schema.RPCMethodChatPermit) + + err = Validators[chatPermit](tx, 91, exampleRpc) + assert.NoError(t, err) + } + + // 91 sets chat permissions to followees only + userId := int32(91) + err = chatSetPermissions(tx, userId, schema.Followees) + assert.NoError(t, err) + assertPermissions(userId, schema.Followees, 1) + + // 91 changes chat permissions to none + err = chatSetPermissions(tx, userId, schema.None) + assert.NoError(t, err) + assertPermissions(userId, schema.Followees, 0) + assertPermissions(userId, schema.None, 1) + + tx.Commit() +} diff --git a/comms/internal/rpcz/chat_test.go b/comms/internal/rpcz/chat_test.go new file mode 100644 index 00000000000..6a92f3dba23 --- /dev/null +++ b/comms/internal/rpcz/chat_test.go @@ -0,0 +1,133 @@ +package rpcz + +import ( + "database/sql" + "fmt" + "log" + "os" + "testing" + "time" + + "comms.audius.co/db" + "comms.audius.co/schema" + "github.com/stretchr/testify/assert" +) + +// this runs before all tests (not a per-test setup / teardown) +func TestMain(m *testing.M) { + // setup + os.Setenv("audius_db_url", "postgresql://postgres:postgres@localhost:5454/comtest?sslmode=disable") + err := db.Dial() + if err != nil { + log.Fatal(err) + } + + // run tests + code := m.Run() + + // teardown code here... + os.Exit(code) +} + +func TestChat(t *testing.T) { + var err error + // ctx := context.Background() + // query := db.New(db.Conn) + + chatId := "chat1" + + // reset tables under test + _, err = db.Conn.Exec("truncate chat cascade") + assert.NoError(t, err) + + tx := db.Conn.MustBegin() + + SetUpChatWithMembers(t, tx, chatId, 91, 92) + + // validate 91 and 92 can both send messages in this chat + { + exampleRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "test123"}`, chatId)), + } + + chatSay := string(schema.RPCMethodChatMessage) + + err = Validators[chatSay](tx, 91, exampleRpc) + assert.NoError(t, err) + + err = Validators[chatSay](tx, 93, exampleRpc) + assert.ErrorIs(t, err, sql.ErrNoRows) + } + + // 91 sends 92 a message + messageTs := time.Now() + messageId := "1" + err = chatSendMessage(tx, 91, chatId, messageId, messageTs, "hello 92") + assert.NoError(t, err) + + // assertUnreadCount helper fun in a closure + assertUnreadCount := func(chatId string, userId int, expected int) { + unreadCount := 0 + err := tx.Get(&unreadCount, "select unread_count from chat_member where chat_id = $1 and user_id = $2", chatId, userId) + assert.NoError(t, err) + assert.Equal(t, expected, unreadCount) + } + + assertReaction := func(userId int, messageId string, expected string) { + var reaction string + err := tx.Get(&reaction, "select reaction from chat_message_reactions where user_id = $1 and message_id = $2", userId, messageId) + assert.NoError(t, err) + assert.Equal(t, expected, reaction) + } + + // assert sender has no unread messages + assertUnreadCount(chatId, 91, 0) + + // assert 92 has one unread message + assertUnreadCount(chatId, 92, 1) + + // 92 reads message + chatReadMessages(tx, 92, chatId, time.Now()) + + // assert 92 has zero unread messages + assertUnreadCount(chatId, 92, 0) + + // 92 sends a reply to 91 + replyTs := time.Now() + replyMessageId := "2" + err = chatSendMessage(tx, 92, chatId, replyMessageId, replyTs, "oh hey there 91 thanks for your message") + assert.NoError(t, err) + + // the tables have turned! + assertUnreadCount(chatId, 92, 0) + assertUnreadCount(chatId, 91, 1) + + // validate 91 and 92 can both send reactions in this chat + { + exampleRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message_id": "%s", "reaction": "heart"}`, chatId, replyMessageId)), + } + + chatReact := string(schema.RPCMethodChatReact) + + err = Validators[chatReact](tx, 91, exampleRpc) + assert.NoError(t, err) + + err = Validators[chatReact](tx, 93, exampleRpc) + assert.ErrorIs(t, err, sql.ErrNoRows) + } + + // 91 reacts to 92's message + reactTs := time.Now() + reaction := "fire" + err = chatReactMessage(tx, 91, replyMessageId, reaction, reactTs) + assertReaction(91, replyMessageId, reaction) + + // 91 changes reaction to 92's old message + changedReactTs := time.Now() + newReaction := "heart" + err = chatReactMessage(tx, 91, replyMessageId, newReaction, changedReactTs) + assertReaction(91, replyMessageId, newReaction) + + tx.Commit() +} diff --git a/comms/internal/rpcz/test_utils.go b/comms/internal/rpcz/test_utils.go new file mode 100644 index 00000000000..3ef72faa9e3 --- /dev/null +++ b/comms/internal/rpcz/test_utils.go @@ -0,0 +1,22 @@ +package rpcz + +import ( + "testing" + "time" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" +) + +func SetUpChatWithMembers(t *testing.T, tx *sqlx.Tx, chatId string, user1 int, user2 int) { + var err error + + // create chat + // - should create chat and initial self invite in one tx + _, err = tx.Exec("insert into chat (chat_id, created_at, last_message_at) values ($1, $2, $2)", chatId, time.Now()) + assert.NoError(t, err) + + // insert two members + _, err = tx.Exec("insert into chat_member (chat_id, invited_by_user_id, invite_code, user_id) values ($1, $2, $1, $2), ($1, $2, $1, $3)", chatId, 91, 92) + assert.NoError(t, err) +} diff --git a/comms/internal/rpcz/validator.go b/comms/internal/rpcz/validator.go new file mode 100644 index 00000000000..9573898dc38 --- /dev/null +++ b/comms/internal/rpcz/validator.go @@ -0,0 +1,258 @@ +package rpcz + +import ( + "context" + "encoding/json" + "errors" + + "comms.audius.co/config" + "comms.audius.co/db" + "comms.audius.co/db/queries" + "comms.audius.co/misc" + "comms.audius.co/schema" + "github.com/jmoiron/sqlx" +) + +type validatorFunc func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error + +func Validate(userId int32, rawRpc schema.RawRPC) error { + + validator := Validators[rawRpc.Method] + if validator == nil { + config.Logger.Debug("no validator for " + rawRpc.Method) + return nil + } + + return validator(nil, userId, rawRpc) +} + +var Validators = map[string]validatorFunc{ + // Mutations + string(schema.RPCMethodChatCreate): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { + // validate rpc.params valid + var params schema.ChatCreateRPCParams + err := json.Unmarshal(rpc.Params, ¶ms) + if err != nil { + return err + } + + // validate chatId does not already exist + query := "select count(*) from chat where chat_id = $1;" + var chatCount int + if tx != nil { + err = tx.Get(&chatCount, query, params.ChatID) + } else { + err = db.Conn.Get(&chatCount, query, params.ChatID) + } + if err != nil { + return err + } + if chatCount != 0 { + return errors.New("Chat already exists") + } + + // for 1-1 DMs: validate chat members are not a pair + var q db.Queryable + q = db.Conn + if tx != nil { + q = tx + } + + if len(params.Invites) == 2 { + user1, err := misc.DecodeHashId(params.Invites[0].UserID) + if err != nil { + return err + } + user2, err := misc.DecodeHashId(params.Invites[1].UserID) + if err != nil { + return err + } + blockedCount, err := queries.CountChatBlocks(q, context.Background(), queries.CountChatBlocksParams{ + User1: int32(user1), + User2: int32(user2), + }) + if err != nil { + return err + } + if blockedCount > 0 { + return errors.New("Cannot create a chat with a user you have blocked or user who has blocked you") + } + } + + // TODO check receiving invitee's permission settings + + return nil + }, + string(schema.RPCMethodChatDelete): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { + var q db.Queryable + q = db.Conn + if tx != nil { + q = tx + } + + // validate rpc.params valid + var params schema.ChatDeleteRPCParams + err := json.Unmarshal(rpc.Params, ¶ms) + if err != nil { + return err + } + + // validate userId is a member of chatId in good standing + _, err = validateChatMembership(q, userId, params.ChatID) + if err != nil { + return err + } + + return nil + }, + string(schema.RPCMethodChatMessage): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { + var q db.Queryable + q = db.Conn + if tx != nil { + q = tx + } + + // validate rpc.params valid + var params schema.ChatMessageRPCParams + err := json.Unmarshal(rpc.Params, ¶ms) + if err != nil { + return err + } + + // validate userId is a member of chatId in good standing + _, err = validateChatMembership(q, userId, params.ChatID) + if err != nil { + return err + } + + // TODO check receiver's permission settings + + // for 1-1 DMs: validate chat members are not a pair + chatMembers, err := queries.ChatMembers(q, context.Background(), params.ChatID) + if len(chatMembers) == 2 { + blockedCount, err := queries.CountChatBlocks(q, context.Background(), queries.CountChatBlocksParams{ + User1: chatMembers[0].UserID, + User2: chatMembers[1].UserID, + }) + if err != nil { + return err + } + if blockedCount > 0 { + return errors.New("Cannot sent messages to users you have blocked or users who have blocked you") + } + } + + return nil + }, + string(schema.RPCMethodChatReact): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { + var q db.Queryable + q = db.Conn + if tx != nil { + q = tx + } + + // validate rpc.params valid + var params schema.ChatReactRPCParams + err := json.Unmarshal(rpc.Params, ¶ms) + if err != nil { + return err + } + + // validate userId is a member of chatId in good standing + _, err = validateChatMembership(q, userId, params.ChatID) + if err != nil { + return err + } + + // validate message exists in chat + _, err = queries.ChatMessage(q, context.Background(), queries.ChatMessageParams{ + ChatID: params.ChatID, + MessageID: params.MessageID, + }) + if err != nil { + return err + } + + return nil + }, + string(schema.RPCMethodChatRead): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { + var q db.Queryable + q = db.Conn + if tx != nil { + q = tx + } + + // validate rpc.params valid + var params schema.ChatReadRPCParams + err := json.Unmarshal(rpc.Params, ¶ms) + if err != nil { + return err + } + + // validate userId is a member of chatId in good standing + _, err = validateChatMembership(q, userId, params.ChatID) + if err != nil { + return err + } + + return nil + }, + string(schema.RPCMethodChatPermit): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { + // validate rpc.params valid + var params schema.ChatPermitRPCParams + err := json.Unmarshal(rpc.Params, ¶ms) + if err != nil { + return err + } + + return nil + }, + string(schema.RPCMethodChatBlock): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { + // validate rpc.params valid + var params schema.ChatBlockRPCParams + err := json.Unmarshal(rpc.Params, ¶ms) + if err != nil { + return err + } + + return nil + }, + string(schema.RPCMethodChatUnblock): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { + var q db.Queryable + q = db.Conn + if tx != nil { + q = tx + } + + // validate rpc.params valid + var params schema.ChatBlockRPCParams + err := json.Unmarshal(rpc.Params, ¶ms) + if err != nil { + return err + } + + // validate that params.UserID is currently blocked by userId + blockeeUserId, err := misc.DecodeHashId(params.UserID) + if err != nil { + return err + } + _, err = queries.ChatBlock(q, context.Background(), queries.ChatBlockParams{ + BlockerUserID: int32(userId), + BlockeeUserID: int32(blockeeUserId), + }) + if err != nil { + return err + } + + return nil + }, +} + +// Helpers +func validateChatMembership(q db.Queryable, userId int32, chatId string) (db.ChatMember, error) { + member, err := queries.ChatMembership(q, context.Background(), queries.ChatMembershipParams{ + UserID: userId, + ChatID: chatId, + }) + return member, err +} diff --git a/comms/main.go b/comms/main.go new file mode 100644 index 00000000000..944263429d5 --- /dev/null +++ b/comms/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "time" + + "comms.audius.co/config" + "comms.audius.co/db" + "comms.audius.co/internal/pubkeystore" + "comms.audius.co/peering" + "comms.audius.co/server" + "golang.org/x/sync/errgroup" +) + +func main() { + config.Init() + + // dial datasources in parallel + g := errgroup.Group{} + g.Go(func() error { + return db.Dial() + }) + g.Go(func() error { + return pubkeystore.Dial() + }) + if err := g.Wait(); err != nil { + log.Fatal(err) + } + + // run migrations + out, err := exec.Command("dbmate", "--wait", "--no-dump-schema", "--url", os.Getenv("audius_db_url"), "up").Output() + if err != nil { + log.Fatal("dbmate:", err) + } + fmt.Println("dbmate:", string(out)) + + // start solicit... + // TODO: re-enable nkeys + go func() { + time.Sleep(500 * time.Millisecond) + + for { + peering.Solicit() + time.Sleep(time.Minute * 5) + } + }() + + server.Start() +} diff --git a/comms/misc/hashids_util.go b/comms/misc/hashids_util.go new file mode 100644 index 00000000000..d625de11aaf --- /dev/null +++ b/comms/misc/hashids_util.go @@ -0,0 +1,25 @@ +package misc + +import ( + "github.com/speps/go-hashids/v2" +) + +var hashIdUtil *hashids.HashID + +func init() { + hd := hashids.NewData() + hd.Salt = "azowernasdfoia" + hd.MinLength = 5 + hashIdUtil, _ = hashids.NewWithData(hd) +} + +func DecodeHashId(id string) (int, error) { + ids, err := hashIdUtil.DecodeWithError(id) + if err != nil { + return 0, err + } + return ids[0], err +} +func EncodeHashId(id int) (string, error) { + return hashIdUtil.Encode([]int{ id }) +} \ No newline at end of file diff --git a/comms/misc/pretty_print.go b/comms/misc/pretty_print.go new file mode 100644 index 00000000000..5a651abd835 --- /dev/null +++ b/comms/misc/pretty_print.go @@ -0,0 +1,15 @@ +package misc + +import ( + "encoding/json" + "fmt" +) + +func PrettyPrint(obj interface{}) { + b, err := json.MarshalIndent(obj, "", "\t") + if err != nil { + fmt.Println("PrettyPrint failed:", err, obj) + return + } + fmt.Println(string(b)) +} diff --git a/comms/misc/recover_wallet.go b/comms/misc/recover_wallet.go new file mode 100644 index 00000000000..9ccdbb4d598 --- /dev/null +++ b/comms/misc/recover_wallet.go @@ -0,0 +1,28 @@ +package misc + +import ( + "encoding/base64" + "errors" + + "github.com/ethereum/go-ethereum/crypto" +) + +// todo: de-duplicate from server.go +func RecoverWallet(payload []byte, sigHex string) (wallet string, err error) { + // sig, err := hexutil.Decode(sigHex) + sig, err := base64.StdEncoding.DecodeString(sigHex) + + if err != nil { + err = errors.New("bad sig header: " + err.Error()) + return + } + + // recover + hash := crypto.Keccak256Hash(payload) + pubkey, err := crypto.SigToPub(hash[:], sig) + if err != nil { + return + } + wallet = crypto.PubkeyToAddress(*pubkey).Hex() + return +} diff --git a/comms/peering/info.go b/comms/peering/info.go new file mode 100644 index 00000000000..f1f7da28396 --- /dev/null +++ b/comms/peering/info.go @@ -0,0 +1,30 @@ +package peering + +import ( + "fmt" + "strings" + + "comms.audius.co/config" +) + +type Info struct { + Address string + Nkey string + IP string + NatsRoute string + // todo: public key for shared secret stuff? +} + +func MyInfo() (*Info, error) { + info := &Info{ + Address: config.WalletAddress, + Nkey: config.NkeyPublic, + IP: config.IP, + NatsRoute: fmt.Sprintf("nats://%s:%s@%s:6222", config.NatsClusterUsername, config.NatsClusterPassword, config.IP), + } + return info, nil +} + +func WalletEquals(a, b string) bool { + return strings.ToLower(a) == strings.ToLower(b) +} diff --git a/comms/peering/info_test.go b/comms/peering/info_test.go new file mode 100644 index 00000000000..b209f2b2887 --- /dev/null +++ b/comms/peering/info_test.go @@ -0,0 +1,21 @@ +package peering + +import ( + "os" + "testing" + + "comms.audius.co/config" + "github.com/stretchr/testify/assert" +) + +func TestInfo(t *testing.T) { + os.Setenv("test_host", "1.2.3.4") + os.Setenv("audius_delegate_private_key", "293589cdf207ed2f2253bb72b17bb7f2cfe399cdc34712b1d32908d969682238") + + config.Init() + info, err := MyInfo() + assert.NoError(t, err) + assert.Equal(t, "0x1c185053c2259f72fd023ED89B9b3EBbD841DA0F", info.Address) + assert.Equal(t, "UDQ7FQ4GSZKLLCDTHWPFBDSWJQKNT5S7CA7NGUS7RQZZOANZFIRIJII2", info.Nkey) + assert.Equal(t, "1.2.3.4", info.IP) +} diff --git a/comms/peering/nats.go b/comms/peering/nats.go new file mode 100644 index 00000000000..f16f28ab2b1 --- /dev/null +++ b/comms/peering/nats.go @@ -0,0 +1,239 @@ +package peering + +import ( + "errors" + "fmt" + "log" + "net/url" + "sync" + "time" + + "comms.audius.co/config" + "comms.audius.co/internal/rpcz" + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" +) + +// NATS CLIENTS +var ( + NatsClient *nats.Conn + JetstreamClient nats.JetStreamContext +) + +type NatsManager struct { + natsServer *server.Server + mu sync.Mutex +} + +func (manager *NatsManager) StartNats(peerMap map[string]*Info) { + if config.NatsClusterUsername == "" { + log.Fatal("config.NatsClusterUsername not set") + } + + manager.mu.Lock() + defer manager.mu.Unlock() + + routes := []*url.URL{} + nkeys := []*server.NkeyUser{} + + for _, info := range peerMap { + if info == nil || info.NatsRoute == "" { + continue + } + fmt.Println("nats route: ", info.NatsRoute) + route, err := url.Parse(info.NatsRoute) + if err != nil { + config.Logger.Warn("invalid nats route url: " + info.NatsRoute) + continue + } + user := &server.NkeyUser{ + Nkey: info.Nkey, + } + + routes = append(routes, route) + nkeys = append(nkeys, user) + } + + opts := &server.Options{ + ServerName: config.WalletAddress, + HTTPPort: 8222, + Logtime: true, + // Debug: true, + + JetStream: true, + } + + if config.NatsReplicaCount < 2 { + config.Logger.Info("starting NATS in standalone mode", "peer count", len(routes)) + } else { + config.Logger.Info("starting NATS in cluster mode... ") + + opts.Cluster = server.ClusterOpts{ + Name: "comms", + Host: "0.0.0.0", + Port: 6222, + Username: config.NatsClusterUsername, + Password: config.NatsClusterPassword, + // NoAdvertise: true, + } + + opts.Routes = routes + opts.Nkeys = nkeys + + } + + // this is kinda jank... probably want a better way to check if natsServer is initialized and health + // but will do for now + if manager.natsServer != nil { + if err := manager.natsServer.ReloadOptions(opts); err != nil { + config.Logger.Warn("error in nats ReloadOptions", "err", err) + } + return + } + + // Initialize new server with options + var err error + manager.natsServer, err = server.NewServer(opts) + if err != nil { + panic(err) + } + + // Start the server via goroutine + manager.natsServer.ConfigureLogger() + go manager.natsServer.Start() + + manager.setupNatsClient() + + manager.setupJetstream() + +} + +func (manager *NatsManager) setupNatsClient() { + + for attempt := 1; attempt < 20; attempt++ { + var err error + + if !manager.natsServer.ReadyForConnections(5 * time.Second) { + config.Logger.Info("nats routing not ready") + continue + } + + // todo: this needs to have multiple URLs for peers + // importantly... if this nats is not reachable we NEED to user a peer url + natsUrl := manager.natsServer.ClientURL() + + if config.NatsUseNkeys { + nkeySign := func(nonce []byte) ([]byte, error) { + return config.NkeyPair.Sign(nonce) + } + NatsClient, err = nats.Connect(natsUrl, nats.Nkey(config.NkeyPublic, nkeySign)) + } else { + NatsClient, err = nats.Connect(natsUrl) + } + + if err != nil { + config.Logger.Info("nats client dail failed", "attempt", attempt, "err", err) + time.Sleep(time.Second * 3) + continue + } else { + break + } + } + +} + +func (manager *NatsManager) setupJetstream() { + natsServer := manager.natsServer + nc := NatsClient + var jsc nats.JetStreamContext + + // TEMP: hardcoded stream config + tempStreamName := "audius" + tempStreamSubject := "audius.>" + + var err error + for i := 1; i < 1000; i++ { + + if i > 1 { + config.Logger.Warn(err.Error(), "attempt", i) + time.Sleep(time.Second * 3) + } + + // wait for server + jetstream to be ready + if !natsServer.JetStreamIsCurrent() { + err = errors.New("jetstream not current") + continue + } + + config.Logger.Info("jetstream is ready", + "js_clustered", natsServer.JetStreamIsClustered(), + "js_leader", natsServer.JetStreamIsLeader(), + "js_current", natsServer.JetStreamIsCurrent(), + // "js_peers", natsServer.JetStreamClusterPeers(), + ) + + jsc, err = nc.JetStream(nats.PublishAsyncMaxPending(256)) + if err != nil { + continue + } + + var streamInfo *nats.StreamInfo + + if natsServer.JetStreamIsLeader() { + streamInfo, err = jsc.AddStream(&nats.StreamConfig{ + Name: tempStreamName, + Subjects: []string{tempStreamSubject}, + Replicas: config.NatsReplicaCount, + // DenyDelete: true, + // DenyPurge: true, + }) + if err != nil { + continue + } + } else { + // wait for stream to exist + streamInfo, err = jsc.StreamInfo(tempStreamName) + if err != nil { + continue + } + } + + config.Logger.Info("Stream OK", + "name", streamInfo.Config.Name, + "created", streamInfo.Created, + "replicas", streamInfo.Config.Replicas) + break + + } + + // TEMP: Subscribe to the subject for the demo + // this is the "processor" for DM messages... which just inserts them into comm log table for now + // it assumes that nats message has the signature header + // but this is not the case for identity relay messages, for instance, which should have their own consumer + // also, should be a pull consumer with explicit ack. + // matrix-org/dendrite codebase has some nice examples to follow... + for { + sub, err := jsc.Subscribe(tempStreamSubject, rpcz.Apply, nats.Durable(config.WalletAddress)) + if err != nil { + config.Logger.Warn("error creating consumer", "err", err) + time.Sleep(time.Second * 5) + } else { + config.Logger.Info("sub OK", "sub", sub.Subject) + break + } + } + + // create kv buckets + _, err = jsc.CreateKeyValue(&nats.KeyValueConfig{ + Bucket: config.PubkeystoreBucketName, + Replicas: config.NatsReplicaCount, + }) + if err != nil { + log.Fatal("CreateKeyValue failed", err) + } + + // finally "expose" this as public var + // the server checks if this is non-nil to know if it's ready + // todo: THIS IS NOT SAFE... should be something like peering.GetJetstream with a mutex + JetstreamClient = jsc +} diff --git a/comms/peering/solicit.go b/comms/peering/solicit.go new file mode 100644 index 00000000000..f738d45674a --- /dev/null +++ b/comms/peering/solicit.go @@ -0,0 +1,131 @@ +package peering + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "net/http" + "sync" + + "comms.audius.co/config" + "github.com/ethereum/go-ethereum/crypto" +) + +var manager = &NatsManager{} + +// todo: this should probably live in a struct +var ( + mu sync.Mutex + peersByWallet = map[string]*Info{} +) + +func Solicit() { + config.Logger.Info("solicit begin") + + sps, err := GetDiscoveryNodes() + if err != nil { + config.Logger.Error("solicit failed: " + err.Error()) + return + } + + var wg sync.WaitGroup + + for _, sp := range sps { + sp := sp + wg.Add(1) + go func() { + u := sp.Endpoint + "/comms/exchange" + info, err := solicitServer(u) + if err != nil { + config.Logger.Warn("get info failed", "endpoint", sp.Endpoint, "err", err) + } else { + mu.Lock() + peersByWallet[info.Address] = info + mu.Unlock() + } + wg.Done() + }() + } + + wg.Wait() + + manager.StartNats(peersByWallet) + config.Logger.Info("solicit done") + +} + +func AddPeer(info *Info) { + mu.Lock() + defer mu.Unlock() + + if existing, ok := peersByWallet[info.Address]; ok && info.IP == existing.IP { + config.Logger.Info("peer already known", "wallet", existing.Address) + } else { + config.Logger.Info("adding peer", "wallet", info.Address) + peersByWallet[info.Address] = info + // trying to pre-emptively add peer causes routing to cycle endlessly + // at least in the docker env + // manager.StartNats(peersByWallet) + } + +} + +func ListPeers() []Info { + mu.Lock() + defer mu.Unlock() + peers := make([]Info, 0, len(peersByWallet)) + for _, i := range peersByWallet { + peers = append(peers, *i) + } + return peers +} + +func solicitServer(endpoint string) (*Info, error) { + + // sign request + myInfo, err := MyInfo() + if err != nil { + return nil, err + } + + resp, err := PostSignedJSON(endpoint, myInfo) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // get response peer info + dec := json.NewDecoder(resp.Body) + var info *Info + err = dec.Decode(&info) + if err != nil { + return nil, err + } + + return info, nil +} + +func PostSignedJSON(endpoint string, obj interface{}) (*http.Response, error) { + payload, err := json.Marshal(obj) + if err != nil { + return nil, err + } + + hash := crypto.Keccak256Hash(payload) + signature, err := crypto.Sign(hash.Bytes(), config.PrivateKey) + if err != nil { + return nil, err + } + + // send request + req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload)) + if err != nil { + return nil, err + } + // req.Header.Set(config.SigHeader, hexutil.Encode(signature)) + + sigBase64 := base64.StdEncoding.EncodeToString(signature) + req.Header.Set(config.SigHeader, sigBase64) + + return http.DefaultClient.Do(req) +} diff --git a/comms/peering/sps.go b/comms/peering/sps.go new file mode 100644 index 00000000000..b8369ade2cb --- /dev/null +++ b/comms/peering/sps.go @@ -0,0 +1,130 @@ +package peering + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" + + "comms.audius.co/config" +) + +type ServiceNode struct { + ID string + SPID string + Endpoint string + DelegateOwnerWallet string +} + +func GetDiscoveryNodes() ([]ServiceNode, error) { + if config.Env == "standalone" { + return []ServiceNode{}, nil + } + if os.Getenv("test_host") != "" { + return testDiscoveryNodes, nil + } + + // todo: some caching + return queryServiceNodes(false, config.IsStaging) +} + +func GetContentNodes() ([]ServiceNode, error) { + // todo: some caching + return queryServiceNodes(true, config.IsStaging) +} + +var testDiscoveryNodes = []ServiceNode{ + { + Endpoint: "http://com1:8925", + DelegateOwnerWallet: "0x1c185053c2259f72fd023ED89B9b3EBbD841DA0F", + }, + { + Endpoint: "http://com2:8925", + DelegateOwnerWallet: "0x90b8d2655A7C268d0fA31758A714e583AE54489D", + }, + { + Endpoint: "http://com3:8925", + DelegateOwnerWallet: "0xb7b9599EeB2FD9237C94cFf02d74368Bb2df959B", + }, + { + Endpoint: "http://com4:8925", + DelegateOwnerWallet: "0xfa4f42633Cb0c72Aa35D3D1A3566abb7142c7b16", + }, +} + +var ( + prodEndpoint = `https://api.thegraph.com/subgraphs/name/audius-infra/audius-network-mainnet` + + stagingEndpoint = `https://api.thegraph.com/subgraphs/name/audius-infra/audius-network-goerli` + + gql = ` + query ServiceProviders($type: String) { + serviceNodes(where: {isRegistered: true, type: $type}) { + id + spId + endpoint + delegateOwnerWallet + } + } + ` +) + +func queryServiceNodes(isContent, isStaging bool) ([]ServiceNode, error) { + + endpoint := prodEndpoint + if isStaging { + endpoint = stagingEndpoint + } + + nodeType := "discovery-node" + if isContent { + nodeType = "content-node" + } + + input := map[string]interface{}{ + "query": gql, + "variables": map[string]interface{}{ + "type": nodeType, + }, + } + + output := struct { + Data struct { + ServiceNodes []ServiceNode + } + }{} + + err := postJson(endpoint, input, &output) + if err != nil { + return nil, err + } + + return output.Data.ServiceNodes, nil +} + +var httpClient = &http.Client{ + Timeout: time.Minute, +} + +func postJson(endpoint string, body interface{}, dest interface{}) error { + buf, err := json.Marshal(body) + if err != nil { + return err + } + + resp, err := httpClient.Post(endpoint, "application/json", bytes.NewReader(buf)) + if err != nil { + return err + } + + if resp.StatusCode != 200 { + txt, _ := io.ReadAll(resp.Body) + return fmt.Errorf("postJson: %d %s %s", resp.StatusCode, endpoint, txt) + } + + dec := json.NewDecoder(resp.Body) + return dec.Decode(&dest) +} diff --git a/comms/peering/sps_test.go b/comms/peering/sps_test.go new file mode 100644 index 00000000000..eb4e1dd2467 --- /dev/null +++ b/comms/peering/sps_test.go @@ -0,0 +1,39 @@ +package peering + +import ( + "fmt" + "testing" +) + +func TestPeers(t *testing.T) { + // t.Skip() + + input := map[string]interface{}{ + "query": gql, + "variables": map[string]interface{}{ + "type": "discovery-node", + }, + } + + output := struct { + Data struct { + ServiceNodes []ServiceNode + } + }{} + + err := postJson(prodEndpoint, input, &output) + if err != nil { + panic(err) + } + + for _, sp := range output.Data.ServiceNodes { + fmt.Println(sp) + } + + // cnById := map[string]string{} + // for _, sp := range output.Data.ServiceNodes { + // cnById[sp.SPID] = sp.Endpoint + // } + + // fmt.Println(cnById) +} diff --git a/comms/schema/raw_rpc.go b/comms/schema/raw_rpc.go new file mode 100644 index 00000000000..1de641bd5e3 --- /dev/null +++ b/comms/schema/raw_rpc.go @@ -0,0 +1,11 @@ +package schema + +import ( + "encoding/json" +) + +type RawRPC struct { + ID string + Method string + Params json.RawMessage +} diff --git a/comms/schema/schema.go b/comms/schema/schema.go new file mode 100644 index 00000000000..5f4af1741ed --- /dev/null +++ b/comms/schema/schema.go @@ -0,0 +1,237 @@ +package schema + +type ChatCreateRPC struct { + Method ChatCreateRPCMethod `json:"method"` + Params ChatCreateRPCParams `json:"params"` +} + +type ChatCreateRPCParams struct { + ChatID string `json:"chat_id"` + Invites []PurpleInvite `json:"invites"` +} + +type PurpleInvite struct { + InviteCode string `json:"invite_code"` + UserID string `json:"user_id"` +} + +type ChatDeleteRPC struct { + Method ChatDeleteRPCMethod `json:"method"` + Params ChatDeleteRPCParams `json:"params"` +} + +type ChatDeleteRPCParams struct { + ChatID string `json:"chat_id"` +} + +type ChatInviteRPC struct { + Method ChatInviteRPCMethod `json:"method"` + Params ChatInviteRPCParams `json:"params"` +} + +type ChatInviteRPCParams struct { + ChatID string `json:"chat_id"` + Invites []FluffyInvite `json:"invites"` +} + +type FluffyInvite struct { + InviteCode string `json:"invite_code"` + UserID string `json:"user_id"` +} + +type ChatMessageRPC struct { + Method ChatMessageRPCMethod `json:"method"` + Params ChatMessageRPCParams `json:"params"` +} + +type ChatMessageRPCParams struct { + ChatID string `json:"chat_id"` + Message string `json:"message"` + MessageID string `json:"message_id"` + ParentMessageID *string `json:"parent_message_id,omitempty"` +} + +type ChatReactRPC struct { + Method ChatReactRPCMethod `json:"method"` + Params ChatReactRPCParams `json:"params"` +} + +type ChatReactRPCParams struct { + ChatID string `json:"chat_id"` + MessageID string `json:"message_id"` + Reaction string `json:"reaction"` +} + +type ChatReadRPC struct { + Method ChatReadRPCMethod `json:"method"` + Params ChatReadRPCParams `json:"params"` +} + +type ChatReadRPCParams struct { + ChatID string `json:"chat_id"` +} + +type ChatBlockRPC struct { + Method ChatBlockRPCMethod `json:"method"` + Params ChatBlockRPCParams `json:"params"` +} + +type ChatBlockRPCParams struct { + UserID string `json:"user_id"` +} + +type ChatUnblockRPC struct { + Method ChatUnblockRPCMethod `json:"method"` + Params ChatUnblockRPCParams `json:"params"` +} + +type ChatUnblockRPCParams struct { + UserID string `json:"user_id"` +} + +type ChatPermitRPC struct { + Method ChatPermitRPCMethod `json:"method"` + Params ChatPermitRPCParams `json:"params"` +} + +type ChatPermitRPCParams struct { + Permit ChatPermission `json:"permit"` +} + +type RPCPayload struct { + Method RPCMethod `json:"method"` + Params RPCPayloadParams `json:"params"` +} + +type RPCPayloadParams struct { + ChatID *string `json:"chat_id,omitempty"` + Invites []TentacledInvite `json:"invites,omitempty"` + Message *string `json:"message,omitempty"` + MessageID *string `json:"message_id,omitempty"` + ParentMessageID *string `json:"parent_message_id,omitempty"` + Reaction *string `json:"reaction,omitempty"` + UserID *string `json:"user_id,omitempty"` + Permit *ChatPermission `json:"permit,omitempty"` +} + +type TentacledInvite struct { + InviteCode string `json:"invite_code"` + UserID string `json:"user_id"` +} + +type UserChat struct { + ChatID string `json:"chat_id"` + ChatMembers []ChatMember `json:"chat_members"` + ClearedHistoryAt string `json:"cleared_history_at"` + InviteCode string `json:"invite_code"` + LastMessageAt string `json:"last_message_at"` + LastReadAt string `json:"last_read_at"` + UnreadMessageCount float64 `json:"unread_message_count"` +} + +type ChatMember struct { + UserID string `json:"user_id"` +} + +type ChatMessage struct { + CreatedAt string `json:"created_at"` + Message string `json:"message"` + MessageID string `json:"message_id"` + Reactions []Reaction `json:"reactions"` + SenderUserID string `json:"sender_user_id"` +} + +type Reaction struct { + CreatedAt string `json:"created_at"` + Reaction string `json:"reaction"` + UserID string `json:"user_id"` +} + +type ChatInvite struct { + InviteCode string `json:"invite_code"` + UserID string `json:"user_id"` +} + +type CommsResponse struct { + Data interface{} `json:"data"` + Health Health `json:"health"` + Summary *Summary `json:"summary,omitempty"` +} + +type Health struct { + IsHealthy bool `json:"is_healthy"` +} + +type Summary struct { + NextCursor string `json:"next_cursor"` + PrevCursor string `json:"prev_cursor"` + RemainingCount float64 `json:"remaining_count"` + TotalCount float64 `json:"total_count"` +} + +type ChatCreateRPCMethod string +const ( + MethodChatCreate ChatCreateRPCMethod = "chat.create" +) + +type ChatDeleteRPCMethod string +const ( + MethodChatDelete ChatDeleteRPCMethod = "chat.delete" +) + +type ChatInviteRPCMethod string +const ( + MethodChatInvite ChatInviteRPCMethod = "chat.invite" +) + +type ChatMessageRPCMethod string +const ( + MethodChatMessage ChatMessageRPCMethod = "chat.message" +) + +type ChatReactRPCMethod string +const ( + MethodChatReact ChatReactRPCMethod = "chat.react" +) + +type ChatReadRPCMethod string +const ( + MethodChatRead ChatReadRPCMethod = "chat.read" +) + +type ChatBlockRPCMethod string +const ( + MethodChatBlock ChatBlockRPCMethod = "chat.block" +) + +type ChatUnblockRPCMethod string +const ( + MethodChatUnblock ChatUnblockRPCMethod = "chat.unblock" +) + +type ChatPermitRPCMethod string +const ( + MethodChatPermit ChatPermitRPCMethod = "chat.permit" +) + +// Defines who the user allows to message them +type ChatPermission string +const ( + All ChatPermission = "all" + Followees ChatPermission = "followees" + None ChatPermission = "none" + Tippers ChatPermission = "tippers" +) + +type RPCMethod string +const ( + RPCMethodChatBlock RPCMethod = "chat.block" + RPCMethodChatCreate RPCMethod = "chat.create" + RPCMethodChatDelete RPCMethod = "chat.delete" + RPCMethodChatInvite RPCMethod = "chat.invite" + RPCMethodChatMessage RPCMethod = "chat.message" + RPCMethodChatPermit RPCMethod = "chat.permit" + RPCMethodChatReact RPCMethod = "chat.react" + RPCMethodChatRead RPCMethod = "chat.read" + RPCMethodChatUnblock RPCMethod = "chat.unblock" +) diff --git a/comms/schema/schema.ts b/comms/schema/schema.ts new file mode 100644 index 00000000000..ee70498f034 --- /dev/null +++ b/comms/schema/schema.ts @@ -0,0 +1,157 @@ +// NOTE: No imports allowed - quicktype is not yet able to track imports! + +export type ChatCreateRPC = { + method: 'chat.create' + params: { + chat_id: string + invites: Array<{ + user_id: string + invite_code: string + }> + } +} + +export type ChatDeleteRPC = { + method: 'chat.delete' + params: { + chat_id: string + } +} + +export type ChatInviteRPC = { + method: 'chat.invite' + params: { + chat_id: string + invites: Array<{ + user_id: string + invite_code: string + }> + } +} + +export type ChatMessageRPC = { + method: 'chat.message' + params: { + chat_id: string + message_id: string + message: string + parent_message_id?: string + } +} + +export type ChatReactRPC = { + method: 'chat.react' + params: { + chat_id: string + message_id: string + reaction: string + } +} + +export type ChatReadRPC = { + method: 'chat.read' + params: { + chat_id: string + } +} + +export type ChatBlockRPC = { + method: 'chat.block' + params: { + user_id: string + } +} + +export type ChatUnblockRPC = { + method: 'chat.unblock' + params: { + user_id: string + } +} + +export type ChatPermitRPC = { + method: 'chat.permit' + params: { + permit: ChatPermission + } +} + +export type RPCPayload = + | ChatCreateRPC + | ChatDeleteRPC + | ChatInviteRPC + | ChatMessageRPC + | ChatReactRPC + | ChatReadRPC + | ChatBlockRPC + | ChatUnblockRPC + | ChatPermitRPC + +export type RPCMethod = RPCPayload['method'] + +export type UserChat = { + // User agnostic + chat_id: string + last_message_at: string + chat_members: Array<{ user_id: string }> + + // User specific + invite_code: string + unread_message_count: number + last_read_at: string + cleared_history_at: string +} + +export type ChatMessage = { + message_id: string + sender_user_id: string + created_at: string + message: string + reactions: Array<{ + user_id: string + created_at: string + reaction: string + }> +} + +export type ChatInvite = { + user_id: string + invite_code: string +} + +/** + * Defines who the user allows to message them + */ +export enum ChatPermission { + /** + * Messages are allowed for everyone + */ + ALL = 'all', + /** + * Messages are only allowed for users that have tipped me + */ + TIPPERS = 'tippers', + /** + * Messages are only allowed for users I follow + */ + FOLLOWEES = 'followees', + /** + * Messages are not allowed + */ + NONE = 'none' +} + +export type CommsResponse = { + health: { + is_healthy: boolean + } + summary?: { + prev_cursor: string + next_cursor: string + remaining_count: number + total_count: number + } + // Overridden in client types but left as any for the server. + // quicktype/golang doesn't do well with union types + data: any +} diff --git a/comms/server/response_mapper.go b/comms/server/response_mapper.go new file mode 100644 index 00000000000..ff702048f9c --- /dev/null +++ b/comms/server/response_mapper.go @@ -0,0 +1,75 @@ +package server + +import ( + "time" + + "comms.audius.co/db" + "comms.audius.co/db/queries" + "comms.audius.co/misc" + "comms.audius.co/schema" +) + +func Map[T, U any](ts []T, f func(T) U) []U { + us := make([]U, len(ts)) + for i := range ts { + us[i] = f(ts[i]) + } + return us +} + +func ToChatMemberResponse(member db.ChatMember) schema.ChatMember { + encodedUserId, _ := misc.EncodeHashId(int(member.UserID)) + memberData := schema.ChatMember{ + UserID: encodedUserId, + } + return memberData +} + +func ToChatResponse(chat queries.UserChatRow, members []db.ChatMember) schema.UserChat { + chatData := schema.UserChat{ + ChatID: chat.ChatID, + LastMessageAt: chat.LastMessageAt.Format(time.RFC3339Nano), + InviteCode: chat.InviteCode, + LastReadAt: chat.LastActiveAt.Time.Format(time.RFC3339Nano), + UnreadMessageCount: float64(chat.UnreadCount), + ChatMembers: Map(members, ToChatMemberResponse), + } + if chat.ClearedHistoryAt.Valid { + chatData.ClearedHistoryAt = chat.ClearedHistoryAt.Time.Format(time.RFC3339Nano) + } + return chatData +} + +func ToSummaryResponse(cursor string, summary queries.SummaryRow) schema.Summary { + responseSummary := schema.Summary{ + TotalCount: float64(summary.TotalCount), + RemainingCount: float64(summary.RemainingCount), + NextCursor: cursor, + } + return responseSummary +} + +func ToReactionsResponse(reactions queries.ReactionsSlice) []schema.Reaction { + var reactionsData []schema.Reaction + for _, reaction := range reactions { + encodedSenderId, _ := misc.EncodeHashId(int(reaction.UserID)) + reactionsData = append(reactionsData, schema.Reaction{ + CreatedAt: reaction.CreatedAt.Format(time.RFC3339Nano), + Reaction: reaction.Reaction, + UserID: encodedSenderId, + }) + } + return reactionsData +} + +func ToMessageResponse(message queries.ChatMessageAndReactionsRow) schema.ChatMessage { + encodedSenderId, _ := misc.EncodeHashId(int(message.UserID)) + messageData := schema.ChatMessage{ + MessageID: message.MessageID, + SenderUserID: encodedSenderId, + Message: message.Ciphertext, + CreatedAt: message.CreatedAt.Format(time.RFC3339Nano), + Reactions: ToReactionsResponse(message.Reactions), + } + return messageData +} diff --git a/comms/server/server.go b/comms/server/server.go new file mode 100644 index 00000000000..0f9e4425b26 --- /dev/null +++ b/comms/server/server.go @@ -0,0 +1,363 @@ +package server + +import ( + "encoding/base64" + "encoding/json" + "errors" + "io" + "net/http" + "strconv" + "time" + + "comms.audius.co/config" + "comms.audius.co/db" + "comms.audius.co/db/queries" + "comms.audius.co/internal/pubkeystore" + "comms.audius.co/internal/rpcz" + "comms.audius.co/misc" + "comms.audius.co/peering" + "comms.audius.co/schema" + "github.com/ethereum/go-ethereum/crypto" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/nats-io/nats.go" +) + +var ( + logger = config.Logger +) + +func GetHealthStatus() schema.Health { + return schema.Health{ + IsHealthy: true, + } +} + +func readSignedRequest(c echo.Context) (payload []byte, wallet string, err error) { + if c.Request().Method == "GET" { + payload = []byte(c.Request().URL.Path) + } else if c.Request().Method == "POST" { + payload, err = io.ReadAll(c.Request().Body) + } else { + err = errors.New("unsupported request type") + } + if err != nil { + return + } + + sigHex := c.Request().Header.Get(config.SigHeader) + sig, err := base64.StdEncoding.DecodeString(sigHex) + if err != nil { + err = errors.New("bad sig header: " + err.Error()) + return + } + + // recover + hash := crypto.Keccak256Hash(payload) + pubkey, err := crypto.SigToPub(hash[:], sig) + if err != nil { + return + } + wallet = crypto.PubkeyToAddress(*pubkey).Hex() + return +} + +func createServer() *echo.Echo { + e := echo.New() + e.HideBanner = true + e.Debug = true + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.CORS()) + + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "comms are UP... but this is /... see /comms") + }) + + g := e.Group("/comms") + + g.GET("", func(c echo.Context) error { + return c.String(http.StatusOK, "comms are UP") + }) + + g.GET("/pubkey/:id", func(c echo.Context) error { + id, err := misc.DecodeHashId(c.Param("id")) + if err != nil { + return c.String(400, "bad id parameter: "+err.Error()) + } + + pubkey, err := pubkeystore.RecoverUserPublicKeyBase64(c.Request().Context(), id) + if err != nil { + return err + } + + return c.String(200, pubkey) + }) + + // this is a WIP endpoint that matches identity relay + // we could use a cloudflare worker to "tee" identity requests into NATS + g.POST("/relay", func(c echo.Context) error { + // todo: do EIP-712 verification here... + // skip if bad... + payload, err := io.ReadAll(c.Request().Body) + if err != nil { + return err + } + + // subject := "audius.comms.demo" + subject := "audius.staging.relay" + + if peering.JetstreamClient == nil { + return c.String(500, "jetstream not ready") + } + + // Publish data to the subject + msg := nats.NewMsg(subject) + msg.Header.Add(config.SigHeader, c.Request().Header.Get(config.SigHeader)) + msg.Data = payload + ok, err := peering.JetstreamClient.PublishMsg(msg) + if err != nil { + logger.Warn(string(payload), "err", err) + return c.String(500, err.Error()) + } + + logger.Debug(string(payload), "seq", ok.Sequence, "relay", true) + return c.String(200, "ok") + }) + + g.GET("/chats", func(c echo.Context) error { + ctx := c.Request().Context() + _, wallet, err := readSignedRequest(c) + if err != nil { + return c.String(400, "bad request: "+err.Error()) + } + + userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) + if err != nil { + return c.String(400, "wallet not found: "+err.Error()) + } + chats, err := queries.UserChats(db.Conn, ctx, userId) + if err != nil { + return err + } + responseData := make([]schema.UserChat, len(chats)) + for i := range chats { + members, err := queries.ChatMembers(db.Conn, ctx, chats[i].ChatID) + if err != nil { + return err + } + responseData[i] = ToChatResponse(chats[i], members) + } + cursorPos := time.Now() + if len(chats) > 0 { + lastChat := chats[len(chats)-1] + cursorPos = lastChat.LastMessageAt + } + summary, err := queries.UserChatsSummary(db.Conn, ctx, queries.UserChatsSummaryParams{UserID: userId, Cursor: cursorPos}) + if err != nil { + return err + } + responseSummary := ToSummaryResponse(cursorPos.Format(time.RFC3339Nano), summary) + response := schema.CommsResponse{ + Health: GetHealthStatus(), + Data: responseData, + Summary: &responseSummary, + } + return c.JSON(200, response) + }) + + g.GET("/chats/:id", func(c echo.Context) error { + ctx := c.Request().Context() + _, wallet, err := readSignedRequest(c) + if err != nil { + return c.String(400, "bad request: "+err.Error()) + } + + userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) + if err != nil { + return c.String(400, "wallet not found: "+err.Error()) + } + chat, err := queries.UserChat(db.Conn, ctx, queries.ChatMembershipParams{UserID: int32(userId), ChatID: c.Param("id")}) + if err != nil { + return err + } + logger.Debug("chat", "userId", userId, "chatId", c.Param("id"), "chat.chatId", chat.ChatID) + members, err := queries.ChatMembers(db.Conn, ctx, chat.ChatID) + if err != nil { + return err + } + response := schema.CommsResponse{ + Health: GetHealthStatus(), + Data: ToChatResponse(chat, members), + } + return c.JSON(200, response) + }) + + g.GET("/chats/:id/messages", func(c echo.Context) error { + ctx := c.Request().Context() + _, wallet, err := readSignedRequest(c) + if err != nil { + return c.String(400, "bad request: "+err.Error()) + } + + userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) + if err != nil { + return c.String(400, "wallet not found: "+err.Error()) + } + + params := queries.ChatMessagesAndReactionsParams{UserID: int32(userId), ChatID: c.Param("id"), Cursor: time.Now(), Limit: 50} + if c.QueryParam("cursor") != "" { + cursor, err := time.Parse(time.RFC3339Nano, c.QueryParam("cursor")) + if err != nil { + return err + } + params.Cursor = cursor + } + if c.QueryParam("limit") != "" { + limit, err := strconv.Atoi(c.QueryParam("limit")) + if err != nil { + return err + } + params.Limit = int32(limit) + } + + messages, err := queries.ChatMessagesAndReactions(db.Conn, ctx, params) + if err != nil { + return err + } + + cursorPos := params.Cursor + if len(messages) > 0 { + cursorPos = messages[len(messages)-1].CreatedAt + } + summary, err := queries.ChatMessagesSummary(db.Conn, ctx, queries.ChatMessagesSummaryParams{UserID: userId, ChatID: c.Param("id"), Cursor: cursorPos}) + if err != nil { + return err + } + responseSummary := ToSummaryResponse(cursorPos.Format(time.RFC3339Nano), summary) + response := schema.CommsResponse{ + Health: GetHealthStatus(), + Data: Map(messages, ToMessageResponse), + Summary: &responseSummary, + } + return c.JSON(200, response) + }) + + // this is the "mutation" RPC encpoint + // it will forward RPC to NATS. + g.POST("/mutate", func(c echo.Context) error { + payload, wallet, err := readSignedRequest(c) + if err != nil { + return c.JSON(400, "bad request: "+err.Error()) + } + + // unmarshal RPC and call validator + var rawRpc schema.RawRPC + err = json.Unmarshal(payload, &rawRpc) + if err != nil { + return c.JSON(400, "bad request: "+err.Error()) + } + + userId, err := queries.GetUserIDFromWallet(db.Conn, c.Request().Context(), wallet) + if err != nil { + return c.String(400, "wallet not found: "+err.Error()) + } + + // call validator + rpcz.Validate(userId, rawRpc) + + subject := "audius.dms.demo" + if peering.JetstreamClient == nil { + return c.JSON(500, "jetstream not ready") + } + + // Publish data to the subject + msg := nats.NewMsg(subject) + msg.Header.Add(config.SigHeader, c.Request().Header.Get(config.SigHeader)) + msg.Data = payload + ok, err := peering.JetstreamClient.PublishMsg(msg) + if err != nil { + logger.Warn(string(payload), "wallet", wallet, "err", err) + return c.JSON(500, err.Error()) + } + + logger.Debug(string(payload), "seq", ok.Sequence, "wallet", wallet, "relay", true) + return c.JSON(200, "ok") + }) + + // exchange is called by peer discovery providers + // for the sake of peering + // after validating request came from a registered peer + // this server should respond with connection info + g.POST("/exchange", func(c echo.Context) error { + discoveryNodes, err := peering.GetDiscoveryNodes() + if err != nil { + return err + } + + payload, senderAddress, err := readSignedRequest(c) + if err != nil { + return c.String(400, "bad request: "+err.Error()) + } + + var theirInfo peering.Info + err = json.Unmarshal(payload, &theirInfo) + if err != nil { + return c.String(http.StatusBadRequest, "bad json") + } + + // check senderAddress is in known list + if !peering.WalletEquals(theirInfo.Address, senderAddress) { + return c.String(400, "recovered wallet doesn't match payload wallet") + } + knownWallet := false + for _, sp := range discoveryNodes { + if peering.WalletEquals(senderAddress, sp.DelegateOwnerWallet) { + knownWallet = true + } + } + if !knownWallet { + return c.String(400, "recovered wallet not a registered service provider") + } + + // maybe we proactively add their info + // maybe something like: go peering.AddPeer(theirInfo) + // we'd want to do this because they might not have reverse proxy working + // or might not have ports open + // and they'd want to connect as client to our nats instance + // and we want to make sure they can connect right away + + // maybe also we sign our response + // maybe we try to move some of this signing stuff out of the http handler + // and http request code + + peering.AddPeer(&theirInfo) + + myInfo, err := peering.MyInfo() + if err != nil { + return err + } + return c.JSON(200, myInfo) + }) + + // debug endpoint to list known peers + g.GET("/peers", func(c echo.Context) error { + peers := peering.ListPeers() + // redact private info + for idx, peer := range peers { + peer.IP = "" + peer.NatsRoute = "" + peers[idx] = peer + } + return c.JSON(200, peers) + }) + + return e +} + +func Start() { + e := createServer() + e.Logger.Fatal(e.Start(":8925")) +} diff --git a/comms/server/server_test.go b/comms/server/server_test.go new file mode 100644 index 00000000000..3f2c4c326d3 --- /dev/null +++ b/comms/server/server_test.go @@ -0,0 +1,56 @@ +package server + +import ( + "fmt" + "log" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" +) + +// func TestServer(t *testing.T) { +// e := createServer() +// req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) +// req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) +// rec := httptest.NewRecorder() +// c := e.NewContext(req, rec) + +// // Assertions +// if assert.NoError(t, h.createUser(c)) { +// assert.Equal(t, http.StatusCreated, rec.Code) +// assert.Equal(t, userJSON, rec.Body.String()) +// } +// } + +func TestSig(t *testing.T) { + privateKey, err := crypto.GenerateKey() + assert.NoError(t, err) + + address := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + fmt.Println(address) + + // sign + data := []byte("hello") + hash := crypto.Keccak256Hash(data) + + signature, err := crypto.Sign(hash.Bytes(), privateKey) + if err != nil { + log.Fatal(err) + } + + // fmt.Println(hexutil.Encode(signature)) + + // recover + sigPublicKey, err := crypto.SigToPub(hash.Bytes(), signature) + if err != nil { + log.Fatal(err) + } + publicKeyBytes := crypto.FromECDSAPub(sigPublicKey) + + fmt.Println(hexutil.Encode(publicKeyBytes)) + address2 := crypto.PubkeyToAddress(*sigPublicKey).Hex() + fmt.Println(address2) + +} diff --git a/comms/sqlc.yaml b/comms/sqlc.yaml new file mode 100644 index 00000000000..31efaae9bd1 --- /dev/null +++ b/comms/sqlc.yaml @@ -0,0 +1,25 @@ +version: "2" + +overrides: + go: + rename: + safe: "Save" + +sql: + - engine: "postgresql" + queries: "db/query.sql" + schema: "db/schema.sql" + + gen: + go: + package: "db" + out: "db" + emit_json_tags: true + emit_db_tags: true + overrides: + - column: "agg.last_message_at" + go_type: "database/sql.NullTime" + nullable: true + + # sql_package: "pgx/v4" + # emit_pointers_for_null_types: true From fb3076479f5d2336a2406ff292a8a746e7c1c4c1 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Tue, 20 Dec 2022 11:46:22 -0500 Subject: [PATCH 02/10] circleci comms build setup --- .circleci/config.yml | 8 ++++++++ comms/Dockerfile | 20 +++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1403917ac09..81d0d34336f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1361,6 +1361,8 @@ workflows: name: creator - noop: name: discovery + - noop: + name: comms - noop: name: identity @@ -1781,6 +1783,12 @@ workflows: repo: discovery-provider requires: - discovery + + - docker-build-and-push-updated: + name: build-comms + repo: comms + requires: + - comms - test-identity: name: test-identity diff --git a/comms/Dockerfile b/comms/Dockerfile index 8d2d308a7e3..a56cb266cb1 100644 --- a/comms/Dockerfile +++ b/comms/Dockerfile @@ -3,20 +3,22 @@ FROM golang:latest # install in-container tools: dbmate, nats cli -RUN curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 && \ +RUN cd /tmp && \ + curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 && \ chmod +x /usr/local/bin/dbmate && \ - CGO_ENABLED=0 go install github.com/nats-io/natscli/nats@latest + curl -fsSL -o nats.deb https://github.com/nats-io/natscli/releases/download/v0.0.35/nats-0.0.35-amd64.deb &&\ + apt install ./nats.deb WORKDIR /app -# COPY go.mod ./ -# COPY go.sum ./ -# RUN go mod download -# COPY . ./ -# RUN CGO_ENABLED=0 GOOS=linux go build -o /comms-linux +COPY go.mod ./ +COPY go.sum ./ +RUN go mod download +COPY . ./ +RUN CGO_ENABLED=0 GOOS=linux go build -o /comms-linux -COPY build/comms-linux-amd64 /comms-linux -COPY db/migrations ./db/migrations +# COPY build/comms-linux-amd64 /comms-linux +# COPY db/migrations ./db/migrations EXPOSE 8925 From 7f8f00aab3569b3f9ccbb5b6cfbabaa7d35b0582 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Tue, 20 Dec 2022 12:27:14 -0500 Subject: [PATCH 03/10] make migrations idempotent --- .../20221128164712_create_rpc_log_table.sql | 2 +- .../20221130042018_create_pubkey_table.sql | 2 +- .../migrations/20221202144236_create_chat.sql | 18 ++++++++---------- .../20221210021301_create_chat_permissions.sql | 4 ++-- ...0221212074228_create_chat_blocked_users.sql | 4 ++-- ...212081249_create_chat_message_reactions.sql | 4 ++-- comms/main.go | 4 ++-- 7 files changed, 18 insertions(+), 20 deletions(-) diff --git a/comms/db/migrations/20221128164712_create_rpc_log_table.sql b/comms/db/migrations/20221128164712_create_rpc_log_table.sql index e402be08b9f..2001c7af8d6 100644 --- a/comms/db/migrations/20221128164712_create_rpc_log_table.sql +++ b/comms/db/migrations/20221128164712_create_rpc_log_table.sql @@ -9,4 +9,4 @@ create table if not exists rpc_log ( ); -- migrate:down -drop table if exists rpc_log; +drop table if exists rpc_log cascade; diff --git a/comms/db/migrations/20221130042018_create_pubkey_table.sql b/comms/db/migrations/20221130042018_create_pubkey_table.sql index 549912fc8d4..118c2f05853 100644 --- a/comms/db/migrations/20221130042018_create_pubkey_table.sql +++ b/comms/db/migrations/20221130042018_create_pubkey_table.sql @@ -5,4 +5,4 @@ create table user_pubkey ( ) -- migrate:down -drop table if exists user_pubkey; +drop table if exists user_pubkey cascade; diff --git a/comms/db/migrations/20221202144236_create_chat.sql b/comms/db/migrations/20221202144236_create_chat.sql index a3585f060a4..17a64dff641 100644 --- a/comms/db/migrations/20221202144236_create_chat.sql +++ b/comms/db/migrations/20221202144236_create_chat.sql @@ -1,14 +1,14 @@ -- migrate:up -create table chat ( +create table if not exists chat ( chat_id text primary key, created_at timestamp not null, last_message_at timestamp not null ); -create index chat_chat_id_idx on chat(chat_id); +create index if not exists chat_chat_id_idx on chat(chat_id); -create table chat_member ( +create table if not exists chat_member ( chat_id text not null references chat(chat_id), user_id int not null, cleared_history_at timestamp, @@ -24,9 +24,9 @@ create table chat_member ( primary key (chat_id, user_id) ); -create index chat_member_user_idx on chat_member(user_id); +create index if not exists chat_member_user_idx on chat_member(user_id); -create table chat_message ( +create table if not exists chat_message ( message_id text primary key, chat_id text not null, user_id int not null, @@ -39,8 +39,6 @@ create table chat_message ( ); -- migrate:down -drop table chat; -drop index chat_chat_id_idx; -drop table chat_member; -drop index chat_member_user_idx; -drop table chat_message; +drop table if exists chat cascade; +drop table if exists chat_member cascade; +drop table if exists chat_message cascade; diff --git a/comms/db/migrations/20221210021301_create_chat_permissions.sql b/comms/db/migrations/20221210021301_create_chat_permissions.sql index 6b80178f161..ce5c7fe4b4a 100644 --- a/comms/db/migrations/20221210021301_create_chat_permissions.sql +++ b/comms/db/migrations/20221210021301_create_chat_permissions.sql @@ -1,9 +1,9 @@ -- migrate:up -create table chat_permissions ( +create table if not exists chat_permissions ( user_id int primary key, permits text default 'all' ); -- migrate:down -drop table chat_permissions; +drop table if exists chat_permissions cascade; diff --git a/comms/db/migrations/20221212074228_create_chat_blocked_users.sql b/comms/db/migrations/20221212074228_create_chat_blocked_users.sql index 26e217f4d49..43b3898cfdc 100644 --- a/comms/db/migrations/20221212074228_create_chat_blocked_users.sql +++ b/comms/db/migrations/20221212074228_create_chat_blocked_users.sql @@ -1,5 +1,5 @@ -- migrate:up -create table chat_blocked_users ( +create table if not exists chat_blocked_users ( blocker_user_id int not null, blockee_user_id int not null, created_at timestamp not null default current_timestamp, @@ -8,4 +8,4 @@ create table chat_blocked_users ( ); -- migrate:down -drop table chat_blocked_users; +drop table if exists chat_blocked_users cascade; diff --git a/comms/db/migrations/20221212081249_create_chat_message_reactions.sql b/comms/db/migrations/20221212081249_create_chat_message_reactions.sql index 6b92f981257..43ac0d6965d 100644 --- a/comms/db/migrations/20221212081249_create_chat_message_reactions.sql +++ b/comms/db/migrations/20221212081249_create_chat_message_reactions.sql @@ -1,5 +1,5 @@ -- migrate:up -create table chat_message_reactions ( +create table if not exists chat_message_reactions ( user_id int not null, message_id text not null references chat_message(message_id), reaction text not null, @@ -9,4 +9,4 @@ create table chat_message_reactions ( ) -- migrate:down -drop table chat_message_reactions; +drop table if exists chat_message_reactions cascade; diff --git a/comms/main.go b/comms/main.go index 944263429d5..6008ac1b469 100644 --- a/comms/main.go +++ b/comms/main.go @@ -31,9 +31,9 @@ func main() { } // run migrations - out, err := exec.Command("dbmate", "--wait", "--no-dump-schema", "--url", os.Getenv("audius_db_url"), "up").Output() + out, err := exec.Command("dbmate", "--wait", "--no-dump-schema", "--url", os.Getenv("audius_db_url"), "up").CombinedOutput() if err != nil { - log.Fatal("dbmate:", err) + log.Fatal("dbmate error: ", err, string(out)) } fmt.Println("dbmate:", string(out)) From ee44c1f387100793493400b0b6fc61fbdf003a33 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Wed, 21 Dec 2022 15:07:34 -0500 Subject: [PATCH 04/10] bring in Michelle's changes --- comms/db/queries/get_chat_messages.go | 46 +++- comms/db/queries/get_chats.go | 1 + comms/db/queries/get_summaries.go | 4 +- comms/internal/rpcz/chat_test.go | 20 -- comms/internal/rpcz/main_test.go | 25 ++ comms/main.go | 6 +- comms/server/main_test.go | 25 ++ comms/server/response_mapper.go | 6 +- comms/server/server.go | 248 ++++++++++---------- comms/server/server_test.go | 326 ++++++++++++++++++++++++++ 10 files changed, 550 insertions(+), 157 deletions(-) create mode 100644 comms/internal/rpcz/main_test.go create mode 100644 comms/server/main_test.go diff --git a/comms/db/queries/get_chat_messages.go b/comms/db/queries/get_chat_messages.go index 08b5491b387..b173601142d 100644 --- a/comms/db/queries/get_chat_messages.go +++ b/comms/db/queries/get_chat_messages.go @@ -33,7 +33,7 @@ JOIN chat_member ON chat_message.chat_id = chat_member.chat_id LEFT JOIN chat_message_reactions reactions ON chat_message.message_id = reactions.message_id WHERE chat_member.user_id = $1 AND chat_message.chat_id = $2 AND chat_message.created_at < $4 AND (chat_member.cleared_history_at IS NULL OR chat_message.created_at > chat_member.cleared_history_at) GROUP BY chat_message.message_id -ORDER BY chat_message.created_at DESC +ORDER BY chat_message.created_at DESC, chat_message.message_id LIMIT $3 ` @@ -45,17 +45,29 @@ type ChatMessagesAndReactionsParams struct { } type ChatMessageAndReactionsRow struct { - MessageID string `db:"message_id" json:"message_id"` - ChatID string `db:"chat_id" json:"chat_id"` - UserID int32 `db:"user_id" json:"user_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - Ciphertext string `db:"ciphertext" json:"ciphertext"` - Reactions ReactionsSlice `json:"reactions"` + MessageID string `db:"message_id" json:"message_id"` + ChatID string `db:"chat_id" json:"chat_id"` + UserID int32 `db:"user_id" json:"user_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + Ciphertext string `db:"ciphertext" json:"ciphertext"` + Reactions Reactions `json:"reactions"` } -type ReactionsSlice []db.ChatMessageReaction +type JSONTime struct { + time.Time +} + +type ChatMessageReactionRow struct { + UserID int32 `db:"user_id" json:"user_id"` + MessageID string `db:"message_id" json:"message_id"` + Reaction string `db:"reaction" json:"reaction"` + CreatedAt JSONTime `db:"created_at" json:"created_at"` + UpdatedAt JSONTime `db:"updated_at" json:"updated_at"` +} + +type Reactions []ChatMessageReactionRow -func (reactions *ReactionsSlice) Scan(value interface{}) error { +func (reactions *Reactions) Scan(value interface{}) error { bytes, ok := value.([]byte) if !ok { return errors.New("type assertion to []byte failed") @@ -64,6 +76,22 @@ func (reactions *ReactionsSlice) Scan(value interface{}) error { return json.Unmarshal(bytes, reactions) } +// Override JSONB timestamp unmarshaling since the postgres driver +// does not convert timestamp strings in JSON -> time.Time +func (t *JSONTime) UnmarshalJSON(b []byte) error { + timeformat := "2006-01-02T15:04:05.999999" + var timestamp string + err := json.Unmarshal(b, ×tamp) + if err != nil { + return err + } + t.Time, err = time.Parse(timeformat, timestamp) + if err != nil { + return err + } + return nil +} + func ChatMessagesAndReactions(q db.Queryable, ctx context.Context, arg ChatMessagesAndReactionsParams) ([]ChatMessageAndReactionsRow, error) { var rows []ChatMessageAndReactionsRow err := q.SelectContext(ctx, &rows, chatMessagesAndReactions, diff --git a/comms/db/queries/get_chats.go b/comms/db/queries/get_chats.go index 3041113409d..4aade5622ee 100644 --- a/comms/db/queries/get_chats.go +++ b/comms/db/queries/get_chats.go @@ -52,6 +52,7 @@ SELECT FROM chat_member JOIN chat ON chat.chat_id = chat_member.chat_id WHERE chat_member.user_id = $1 AND (chat_member.cleared_history_at IS NULL OR chat.last_message_at > chat_member.cleared_history_at) +ORDER BY chat.last_message_at DESC, chat.chat_id ` func UserChats(q db.Queryable, ctx context.Context, userID int32) ([]UserChatRow, error) { diff --git a/comms/db/queries/get_summaries.go b/comms/db/queries/get_summaries.go index e03d497d68c..12fa7ef4dcb 100644 --- a/comms/db/queries/get_summaries.go +++ b/comms/db/queries/get_summaries.go @@ -8,8 +8,8 @@ import ( ) type SummaryRow struct { - TotalCount int64 `json:"total_count"` - RemainingCount int64 `json:"remaining_count"` + TotalCount int64 `db:"total_count" json:"total_count"` + RemainingCount int64 `db:"remaining_count" json:"remaining_count"` } const chatMessagesSummary = ` diff --git a/comms/internal/rpcz/chat_test.go b/comms/internal/rpcz/chat_test.go index 6a92f3dba23..1061d87cdea 100644 --- a/comms/internal/rpcz/chat_test.go +++ b/comms/internal/rpcz/chat_test.go @@ -3,8 +3,6 @@ package rpcz import ( "database/sql" "fmt" - "log" - "os" "testing" "time" @@ -13,26 +11,8 @@ import ( "github.com/stretchr/testify/assert" ) -// this runs before all tests (not a per-test setup / teardown) -func TestMain(m *testing.M) { - // setup - os.Setenv("audius_db_url", "postgresql://postgres:postgres@localhost:5454/comtest?sslmode=disable") - err := db.Dial() - if err != nil { - log.Fatal(err) - } - - // run tests - code := m.Run() - - // teardown code here... - os.Exit(code) -} - func TestChat(t *testing.T) { var err error - // ctx := context.Background() - // query := db.New(db.Conn) chatId := "chat1" diff --git a/comms/internal/rpcz/main_test.go b/comms/internal/rpcz/main_test.go new file mode 100644 index 00000000000..3bb2b711ca7 --- /dev/null +++ b/comms/internal/rpcz/main_test.go @@ -0,0 +1,25 @@ +package rpcz + +import ( + "log" + "os" + "testing" + + "comms.audius.co/db" +) + +// this runs before all tests (not a per-test setup / teardown) +func TestMain(m *testing.M) { + // setup + os.Setenv("audius_db_url", "postgresql://postgres:postgres@localhost:5454/comtest?sslmode=disable") + err := db.Dial() + if err != nil { + log.Fatal(err) + } + + // run tests + code := m.Run() + + // teardown code here... + os.Exit(code) +} diff --git a/comms/main.go b/comms/main.go index 6008ac1b469..80993fd6181 100644 --- a/comms/main.go +++ b/comms/main.go @@ -31,11 +31,11 @@ func main() { } // run migrations - out, err := exec.Command("dbmate", "--wait", "--no-dump-schema", "--url", os.Getenv("audius_db_url"), "up").CombinedOutput() + out, err := exec.Command("dbmate", "--no-dump-schema", "--url", os.Getenv("audius_db_url"), "up").CombinedOutput() if err != nil { - log.Fatal("dbmate error: ", err, string(out)) + log.Fatalf("dbmate: %s %s \n", err, out) } - fmt.Println("dbmate:", string(out)) + fmt.Println("dbmate: ", string(out)) // start solicit... // TODO: re-enable nkeys diff --git a/comms/server/main_test.go b/comms/server/main_test.go new file mode 100644 index 00000000000..9d750d93e79 --- /dev/null +++ b/comms/server/main_test.go @@ -0,0 +1,25 @@ +package server + +import ( + "log" + "os" + "testing" + + "comms.audius.co/db" +) + +// this runs before all tests (not a per-test setup / teardown) +func TestMain(m *testing.M) { + // setup + os.Setenv("audius_db_url", "postgresql://postgres:postgres@localhost:5454/comtest?sslmode=disable") + err := db.Dial() + if err != nil { + log.Fatal(err) + } + + // run tests + code := m.Run() + + // teardown code here... + os.Exit(code) +} diff --git a/comms/server/response_mapper.go b/comms/server/response_mapper.go index ff702048f9c..7966e0a9da0 100644 --- a/comms/server/response_mapper.go +++ b/comms/server/response_mapper.go @@ -30,10 +30,12 @@ func ToChatResponse(chat queries.UserChatRow, members []db.ChatMember) schema.Us ChatID: chat.ChatID, LastMessageAt: chat.LastMessageAt.Format(time.RFC3339Nano), InviteCode: chat.InviteCode, - LastReadAt: chat.LastActiveAt.Time.Format(time.RFC3339Nano), UnreadMessageCount: float64(chat.UnreadCount), ChatMembers: Map(members, ToChatMemberResponse), } + if chat.LastActiveAt.Valid { + chatData.LastReadAt = chat.LastActiveAt.Time.Format(time.RFC3339Nano) + } if chat.ClearedHistoryAt.Valid { chatData.ClearedHistoryAt = chat.ClearedHistoryAt.Time.Format(time.RFC3339Nano) } @@ -49,7 +51,7 @@ func ToSummaryResponse(cursor string, summary queries.SummaryRow) schema.Summary return responseSummary } -func ToReactionsResponse(reactions queries.ReactionsSlice) []schema.Reaction { +func ToReactionsResponse(reactions queries.Reactions) []schema.Reaction { var reactionsData []schema.Reaction for _, reaction := range reactions { encodedSenderId, _ := misc.EncodeHashId(int(reaction.UserID)) diff --git a/comms/server/server.go b/comms/server/server.go index 0f9e4425b26..263811d2e38 100644 --- a/comms/server/server.go +++ b/comms/server/server.go @@ -27,7 +27,12 @@ var ( logger = config.Logger ) -func GetHealthStatus() schema.Health { +func Start() { + e := createServer() + e.Logger.Fatal(e.Start(":8925")) +} + +func getHealthStatus() schema.Health { return schema.Health{ IsHealthy: true, } @@ -62,6 +67,124 @@ func readSignedRequest(c echo.Context) (payload []byte, wallet string, err error return } +func getChats(c echo.Context) error { + ctx := c.Request().Context() + _, wallet, err := readSignedRequest(c) + if err != nil { + return c.String(400, "bad request: "+err.Error()) + } + + userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) + if err != nil { + return c.String(400, "wallet not found: "+err.Error()) + } + chats, err := queries.UserChats(db.Conn, ctx, userId) + if err != nil { + return err + } + responseData := make([]schema.UserChat, len(chats)) + for i := range chats { + members, err := queries.ChatMembers(db.Conn, ctx, chats[i].ChatID) + if err != nil { + return err + } + responseData[i] = ToChatResponse(chats[i], members) + } + cursorPos := time.Now().UTC() + if len(chats) > 0 { + lastChat := chats[len(chats)-1] + cursorPos = lastChat.LastMessageAt + } + summary, err := queries.UserChatsSummary(db.Conn, ctx, queries.UserChatsSummaryParams{UserID: userId, Cursor: cursorPos}) + if err != nil { + return err + } + responseSummary := ToSummaryResponse(cursorPos.Format(time.RFC3339Nano), summary) + response := schema.CommsResponse{ + Health: getHealthStatus(), + Data: responseData, + Summary: &responseSummary, + } + return c.JSON(200, response) +} + +func getChat(c echo.Context) error { + ctx := c.Request().Context() + _, wallet, err := readSignedRequest(c) + if err != nil { + return c.String(400, "bad request: "+err.Error()) + } + + userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) + if err != nil { + return c.String(400, "wallet not found: "+err.Error()) + } + chat, err := queries.UserChat(db.Conn, ctx, queries.ChatMembershipParams{UserID: int32(userId), ChatID: c.Param("id")}) + if err != nil { + return err + } + logger.Debug("chat", "userId", userId, "chatId", c.Param("id"), "chat.chatId", chat.ChatID) + members, err := queries.ChatMembers(db.Conn, ctx, chat.ChatID) + if err != nil { + return err + } + response := schema.CommsResponse{ + Health: getHealthStatus(), + Data: ToChatResponse(chat, members), + } + return c.JSON(200, response) +} + +func getMessages(c echo.Context) error { + ctx := c.Request().Context() + _, wallet, err := readSignedRequest(c) + if err != nil { + return c.String(400, "bad request: "+err.Error()) + } + + userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) + if err != nil { + return c.String(400, "wallet not found: "+err.Error()) + } + + params := queries.ChatMessagesAndReactionsParams{UserID: int32(userId), ChatID: c.Param("id"), Cursor: time.Now().UTC(), Limit: 50} + if c.QueryParam("cursor") != "" { + cursor, err := time.Parse(time.RFC3339Nano, c.QueryParam("cursor")) + if err != nil { + return err + } + params.Cursor = cursor + } + if c.QueryParam("limit") != "" { + limit, err := strconv.Atoi(c.QueryParam("limit")) + if err != nil { + return err + } + params.Limit = int32(limit) + } + + messages, err := queries.ChatMessagesAndReactions(db.Conn, ctx, params) + if err != nil { + return err + } + + cursorPos := params.Cursor + if len(messages) > 0 { + cursorPos = messages[len(messages)-1].CreatedAt + } + summary, err := queries.ChatMessagesSummary(db.Conn, ctx, queries.ChatMessagesSummaryParams{UserID: userId, ChatID: c.Param("id"), Cursor: cursorPos}) + if err != nil { + return err + } + responseSummary := ToSummaryResponse(cursorPos.Format(time.RFC3339Nano), summary) + response := schema.CommsResponse{ + Health: getHealthStatus(), + Data: Map(messages, ToMessageResponse), + Summary: &responseSummary, + } + return c.JSON(200, response) +} + func createServer() *echo.Echo { e := echo.New() e.HideBanner = true @@ -127,123 +250,11 @@ func createServer() *echo.Echo { return c.String(200, "ok") }) - g.GET("/chats", func(c echo.Context) error { - ctx := c.Request().Context() - _, wallet, err := readSignedRequest(c) - if err != nil { - return c.String(400, "bad request: "+err.Error()) - } - - userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) - if err != nil { - return c.String(400, "wallet not found: "+err.Error()) - } - chats, err := queries.UserChats(db.Conn, ctx, userId) - if err != nil { - return err - } - responseData := make([]schema.UserChat, len(chats)) - for i := range chats { - members, err := queries.ChatMembers(db.Conn, ctx, chats[i].ChatID) - if err != nil { - return err - } - responseData[i] = ToChatResponse(chats[i], members) - } - cursorPos := time.Now() - if len(chats) > 0 { - lastChat := chats[len(chats)-1] - cursorPos = lastChat.LastMessageAt - } - summary, err := queries.UserChatsSummary(db.Conn, ctx, queries.UserChatsSummaryParams{UserID: userId, Cursor: cursorPos}) - if err != nil { - return err - } - responseSummary := ToSummaryResponse(cursorPos.Format(time.RFC3339Nano), summary) - response := schema.CommsResponse{ - Health: GetHealthStatus(), - Data: responseData, - Summary: &responseSummary, - } - return c.JSON(200, response) - }) + g.GET("/chats", getChats) - g.GET("/chats/:id", func(c echo.Context) error { - ctx := c.Request().Context() - _, wallet, err := readSignedRequest(c) - if err != nil { - return c.String(400, "bad request: "+err.Error()) - } + g.GET("/chats/:id", getChat) - userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) - if err != nil { - return c.String(400, "wallet not found: "+err.Error()) - } - chat, err := queries.UserChat(db.Conn, ctx, queries.ChatMembershipParams{UserID: int32(userId), ChatID: c.Param("id")}) - if err != nil { - return err - } - logger.Debug("chat", "userId", userId, "chatId", c.Param("id"), "chat.chatId", chat.ChatID) - members, err := queries.ChatMembers(db.Conn, ctx, chat.ChatID) - if err != nil { - return err - } - response := schema.CommsResponse{ - Health: GetHealthStatus(), - Data: ToChatResponse(chat, members), - } - return c.JSON(200, response) - }) - - g.GET("/chats/:id/messages", func(c echo.Context) error { - ctx := c.Request().Context() - _, wallet, err := readSignedRequest(c) - if err != nil { - return c.String(400, "bad request: "+err.Error()) - } - - userId, err := queries.GetUserIDFromWallet(db.Conn, ctx, wallet) - if err != nil { - return c.String(400, "wallet not found: "+err.Error()) - } - - params := queries.ChatMessagesAndReactionsParams{UserID: int32(userId), ChatID: c.Param("id"), Cursor: time.Now(), Limit: 50} - if c.QueryParam("cursor") != "" { - cursor, err := time.Parse(time.RFC3339Nano, c.QueryParam("cursor")) - if err != nil { - return err - } - params.Cursor = cursor - } - if c.QueryParam("limit") != "" { - limit, err := strconv.Atoi(c.QueryParam("limit")) - if err != nil { - return err - } - params.Limit = int32(limit) - } - - messages, err := queries.ChatMessagesAndReactions(db.Conn, ctx, params) - if err != nil { - return err - } - - cursorPos := params.Cursor - if len(messages) > 0 { - cursorPos = messages[len(messages)-1].CreatedAt - } - summary, err := queries.ChatMessagesSummary(db.Conn, ctx, queries.ChatMessagesSummaryParams{UserID: userId, ChatID: c.Param("id"), Cursor: cursorPos}) - if err != nil { - return err - } - responseSummary := ToSummaryResponse(cursorPos.Format(time.RFC3339Nano), summary) - response := schema.CommsResponse{ - Health: GetHealthStatus(), - Data: Map(messages, ToMessageResponse), - Summary: &responseSummary, - } - return c.JSON(200, response) - }) + g.GET("/chats/:id/messages", getMessages) // this is the "mutation" RPC encpoint // it will forward RPC to NATS. @@ -356,8 +367,3 @@ func createServer() *echo.Echo { return e } - -func Start() { - e := createServer() - e.Logger.Fatal(e.Start(":8925")) -} diff --git a/comms/server/server_test.go b/comms/server/server_test.go index 3f2c4c326d3..85d7b3ba104 100644 --- a/comms/server/server_test.go +++ b/comms/server/server_test.go @@ -1,10 +1,21 @@ package server import ( + "bytes" + "crypto/ecdsa" + "encoding/base64" + "encoding/json" "fmt" "log" + "net/http" + "net/http/httptest" "testing" + "time" + "comms.audius.co/config" + "comms.audius.co/db" + "comms.audius.co/misc" + "comms.audius.co/schema" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" @@ -52,5 +63,320 @@ func TestSig(t *testing.T) { fmt.Println(hexutil.Encode(publicKeyBytes)) address2 := crypto.PubkeyToAddress(*sigPublicKey).Hex() fmt.Println(address2) + assert.Equal(t, address, address2) +} + +func TestGetChats(t *testing.T) { + var err error + + // Generate user keys + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + wallet1 := crypto.PubkeyToAddress(privateKey1.PublicKey).Hex() + + privateKey2, err := crypto.GenerateKey() + assert.NoError(t, err) + wallet2 := crypto.PubkeyToAddress(privateKey2.PublicKey).Hex() + + privateKey3, err := crypto.GenerateKey() + assert.NoError(t, err) + wallet3 := crypto.PubkeyToAddress(privateKey3.PublicKey).Hex() + + // Set up db + _, err = db.Conn.Exec("truncate chat cascade") + assert.NoError(t, err) + _, err = db.Conn.Exec("truncate users cascade") + assert.NoError(t, err) + + tx := db.Conn.MustBegin() + // Create 3 users + _, err = tx.Exec("insert into users (user_id, wallet, is_current) values ($1, lower($2), true), ($3, lower($4), true), ($5, lower($6), true)", 1, wallet1, 2, wallet2, 3, wallet3) + assert.NoError(t, err) + + // Create 2 chats + chatId1 := "chat1" + chatId2 := "chat2" + chat1CreatedAt := time.Now().UTC() + chat2CreatedAt := time.Now().UTC().Add(time.Minute * time.Duration(30)) + _, err = tx.Exec("insert into chat (chat_id, created_at, last_message_at) values ($1, $2, $2), ($3, $4, $4)", chatId1, chat1CreatedAt, chatId2, chat2CreatedAt) + assert.NoError(t, err) + + // Insert members into chats (1 and 2, 1 and 3) + _, err = tx.Exec("insert into chat_member (chat_id, invited_by_user_id, invite_code, user_id) values ($1, $2, $1, $2), ($1, $2, $1, $3), ($4, $2, $4, $2), ($4, $2, $4, $5)", chatId1, 1, 2, chatId2, 3) + assert.NoError(t, err) + tx.Commit() + + // Common expected responses + expectedHealth := schema.Health{ + IsHealthy: true, + } + encodedUser1, err := misc.EncodeHashId(1) + assert.NoError(t, err) + encodedUser2, err := misc.EncodeHashId(2) + assert.NoError(t, err) + encodedUser3, err := misc.EncodeHashId(3) + assert.NoError(t, err) + expectedMember1 := schema.ChatMember{ + UserID: encodedUser1, + } + expectedMember2 := schema.ChatMember{ + UserID: encodedUser2, + } + expectedMember3 := schema.ChatMember{ + UserID: encodedUser3, + } + expectedChat1Data := schema.UserChat{ + ChatID: chatId1, + LastMessageAt: chat1CreatedAt.Format(time.RFC3339Nano), + InviteCode: chatId1, + UnreadMessageCount: float64(0), + ChatMembers: []schema.ChatMember{ + expectedMember1, + expectedMember2, + }, + } + expectedChat2Data := schema.UserChat{ + ChatID: chatId2, + LastMessageAt: chat2CreatedAt.Format(time.RFC3339Nano), + InviteCode: chatId2, + UnreadMessageCount: float64(0), + ChatMembers: []schema.ChatMember{ + expectedMember1, + expectedMember3, + }, + } + + e := createServer() + + // Test GET /comms/chats + { + // Query /comms/chats + req, err := http.NewRequest(http.MethodGet, "/comms/chats", nil) + assert.NoError(t, err) + + // Set sig header + payload := []byte(req.URL.Path) + sigBase64 := signPayload(t, payload, privateKey1) + req.Header.Set(config.SigHeader, sigBase64) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + res := rec.Result() + defer res.Body.Close() + + // Assertions + expectedData := []schema.UserChat{ + expectedChat2Data, + expectedChat1Data, + } + expectedSummary := schema.Summary{ + TotalCount: float64(2), + RemainingCount: float64(0), + NextCursor: chat1CreatedAt.Format(time.RFC3339Nano), + } + expectedResponse, err := json.Marshal( + schema.CommsResponse{ + Health: expectedHealth, + Data: expectedData, + Summary: &expectedSummary, + }, + ) + assert.NoError(t, err) + if assert.NoError(t, getChats(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + // Remove insignificant space characters + compactResp := new(bytes.Buffer) + err = json.Compact(compactResp, rec.Body.Bytes()) + assert.Equal(t, string(expectedResponse), compactResp.String()) + } + } + + // Test GET /comms/chats/:id + { + // Query /comms/chats/chat1 + req, err := http.NewRequest(http.MethodGet, "/comms/chat/:id", nil) + assert.NoError(t, err) + + // Set sig header + payload := []byte(req.URL.Path) + sigBase64 := signPayload(t, payload, privateKey1) + req.Header.Set(config.SigHeader, sigBase64) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // Set query params + c.SetParamNames("id") + c.SetParamValues(chatId1) + + res := rec.Result() + defer res.Body.Close() + + // Assertions + expectedData := schema.UserChat{ + ChatID: chatId1, + LastMessageAt: chat1CreatedAt.Format(time.RFC3339Nano), + InviteCode: chatId1, + UnreadMessageCount: float64(0), + ChatMembers: []schema.ChatMember{ + expectedMember1, + expectedMember2, + }, + } + expectedResponse, err := json.Marshal( + schema.CommsResponse{ + Health: expectedHealth, + Data: expectedData, + }, + ) + assert.NoError(t, err) + if assert.NoError(t, getChat(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + // Remove insignificant space characters + compactResp := new(bytes.Buffer) + err = json.Compact(compactResp, rec.Body.Bytes()) + assert.Equal(t, string(expectedResponse), compactResp.String()) + } + } +} + +func TestGetMessages(t *testing.T) { + var err error + + // Generate user keys + privateKey1, err := crypto.GenerateKey() + assert.NoError(t, err) + wallet1 := crypto.PubkeyToAddress(privateKey1.PublicKey).Hex() + + privateKey2, err := crypto.GenerateKey() + assert.NoError(t, err) + wallet2 := crypto.PubkeyToAddress(privateKey2.PublicKey).Hex() + + // Set up db + _, err = db.Conn.Exec("truncate chat cascade") + assert.NoError(t, err) + _, err = db.Conn.Exec("truncate users cascade") + assert.NoError(t, err) + + tx := db.Conn.MustBegin() + // Create 2 users + _, err = tx.Exec("insert into users (user_id, wallet, is_current) values ($1, lower($2), true), ($3, lower($4), true)", 1, wallet1, 2, wallet2) + assert.NoError(t, err) + + // Create a chat + chatId := "chat1" + chatCreatedAt := time.Now().UTC().Add(-time.Hour * time.Duration(2)) + _, err = tx.Exec("insert into chat (chat_id, created_at, last_message_at) values ($1, $2, $2)", chatId, chatCreatedAt) + assert.NoError(t, err) + + // Insert members 1 and 2 into chat + _, err = tx.Exec("insert into chat_member (chat_id, invited_by_user_id, invite_code, user_id) values ($1, $2, $1, $2), ($1, $2, $1, $3)", chatId, 1, 2) + assert.NoError(t, err) + + // Insert chat messages + messageId1 := "message1" + message1CreatedAt := time.Now().UTC().Add(-time.Hour * time.Duration(2)) + message1 := "hello from user 1" + messageId2 := "message2" + message2CreatedAt := time.Now().UTC().Add(-time.Hour * time.Duration(1)) + message2 := "ack from user 2" + _, err = tx.Exec("insert into chat_message (message_id, chat_id, user_id, created_at, ciphertext) values ($1, $2, $3, $4, $5), ($6, $2, $7, $8, $9)", messageId1, chatId, 1, message1CreatedAt, message1, messageId2, 2, message2CreatedAt, message2) + assert.NoError(t, err) + + // Insert 2 message reactions to message 1 + reaction1 := "heart" + reaction1CreatedAt := time.Now().UTC().Add(-time.Minute * time.Duration(30)) + reaction2 := "fire" + reaction2CreatedAt := time.Now().UTC().Add(-time.Minute * time.Duration(15)) + _, err = tx.Exec("insert into chat_message_reactions (user_id, message_id, reaction, created_at) values ($1, $2, $3, $4), ($5, $2, $6, $7)", 1, messageId1, reaction1, reaction1CreatedAt, 2, reaction2, reaction2CreatedAt) + assert.NoError(t, err) + tx.Commit() + + // Test GET /comms/chats/:id/messages + e := createServer() + req, err := http.NewRequest(http.MethodGet, "/comms/chats/:id/messages", nil) + assert.NoError(t, err) + + // Set sig header + payload := []byte(req.URL.Path) + sigBase64 := signPayload(t, payload, privateKey1) + req.Header.Set(config.SigHeader, sigBase64) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // Set query params + c.SetParamNames("id") + c.SetParamValues(chatId) + + res := rec.Result() + defer res.Body.Close() + + // Assertions + encodedUser1, err := misc.EncodeHashId(1) + assert.NoError(t, err) + encodedUser2, err := misc.EncodeHashId(2) + assert.NoError(t, err) + expectedHealth := schema.Health{ + IsHealthy: true, + } + expectedMessage1ReactionsData := []schema.Reaction{ + { + CreatedAt: reaction1CreatedAt.Format(time.RFC3339Nano), + Reaction: reaction1, + UserID: encodedUser1, + }, + { + CreatedAt: reaction2CreatedAt.Format(time.RFC3339Nano), + Reaction: reaction2, + UserID: encodedUser2, + }, + } + expectedMessage1Data := schema.ChatMessage{ + MessageID: messageId1, + SenderUserID: encodedUser1, + Message: message1, + CreatedAt: message1CreatedAt.Format(time.RFC3339Nano), + Reactions: expectedMessage1ReactionsData, + } + expectedMessage2Data := schema.ChatMessage{ + MessageID: messageId2, + SenderUserID: encodedUser2, + Message: message2, + CreatedAt: message2CreatedAt.Format(time.RFC3339Nano), + } + expectedData := []schema.ChatMessage{ + expectedMessage2Data, + expectedMessage1Data, + } + expectedSummary := schema.Summary{ + TotalCount: float64(2), + RemainingCount: float64(0), + NextCursor: message1CreatedAt.Format(time.RFC3339Nano), + } + expectedResponse, err := json.Marshal( + schema.CommsResponse{ + Health: expectedHealth, + Data: expectedData, + Summary: &expectedSummary, + }, + ) + assert.NoError(t, err) + if assert.NoError(t, getMessages(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + // Remove insignificant space characters + compactResp := new(bytes.Buffer) + err = json.Compact(compactResp, rec.Body.Bytes()) + assert.Equal(t, string(expectedResponse), compactResp.String()) + } +} + +func signPayload(t *testing.T, payload []byte, privateKey *ecdsa.PrivateKey) string { + msgHash := crypto.Keccak256Hash(payload) + sig, err := crypto.Sign(msgHash[:], privateKey) + assert.NoError(t, err) + sigBase64 := base64.StdEncoding.EncodeToString(sig) + return sigBase64 } From 61c526fa49ef8a3604fad72b39a37a3a14e9cef4 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Wed, 21 Dec 2022 15:11:10 -0500 Subject: [PATCH 05/10] pubkey endpoint returns json --- comms/server/server.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/comms/server/server.go b/comms/server/server.go index 263811d2e38..1d8c589e290 100644 --- a/comms/server/server.go +++ b/comms/server/server.go @@ -216,7 +216,9 @@ func createServer() *echo.Echo { return err } - return c.String(200, pubkey) + return c.JSON(200, map[string]interface{}{ + "data": pubkey, + }) }) // this is a WIP endpoint that matches identity relay From e0612466c70474acdc32a6f739bb435a92b5ac63 Mon Sep 17 00:00:00 2001 From: Michelle Brier Date: Thu, 22 Dec 2022 12:30:35 -0800 Subject: [PATCH 06/10] Only process messages once per seq num (#4529) * Only process messages once per seq num * Fix small thing in unrelated test * count on conflict returning * rows affected --- .../20221222050259_add_rpc_log_pk.sql | 5 +++++ comms/internal/rpcz/apply.go | 17 +++++++++++++---- comms/server/server_test.go | 12 +----------- 3 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 comms/db/migrations/20221222050259_add_rpc_log_pk.sql diff --git a/comms/db/migrations/20221222050259_add_rpc_log_pk.sql b/comms/db/migrations/20221222050259_add_rpc_log_pk.sql new file mode 100644 index 00000000000..e71b3ba4950 --- /dev/null +++ b/comms/db/migrations/20221222050259_add_rpc_log_pk.sql @@ -0,0 +1,5 @@ +-- migrate:up +alter table rpc_log add primary key (jetstream_sequence); + +-- migrate:down +alter table rpc_log drop constraint rpc_log_pkey; diff --git a/comms/internal/rpcz/apply.go b/comms/internal/rpcz/apply.go index 194d246ae1f..7b41fc073a6 100644 --- a/comms/internal/rpcz/apply.go +++ b/comms/internal/rpcz/apply.go @@ -12,7 +12,7 @@ import ( "github.com/nats-io/nats.go" ) -// Validates + Applys a NATS message +// Validates + Applies a NATS message func Apply(msg *nats.Msg) { var err error @@ -57,7 +57,6 @@ func Apply(msg *nats.Msg) { } for attempt := 1; attempt < 5; attempt++ { - logger = logger.New("attempt", attempt) if err != nil { @@ -66,14 +65,24 @@ func Apply(msg *nats.Msg) { // write to db tx := db.Conn.MustBegin() + + query := ` + INSERT INTO rpc_log (jetstream_sequence, jetstream_timestamp, from_wallet, rpc, sig) VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING + ` + result, err := tx.Exec(query, meta.Sequence.Stream, meta.Timestamp, wallet, msg.Data, signatureHeader) if err != nil { continue } - - _, err = tx.Exec("insert into rpc_log (jetstream_sequence, jetstream_timestamp, from_wallet, rpc, sig) values($1, $2, $3, $4, $5)", meta.Sequence.Stream, meta.Timestamp, wallet, msg.Data, signatureHeader) + count, err := result.RowsAffected() if err != nil { continue } + if count == 0 { + // No rows were inserted because the jetstream seq number is already in rpc_log. + // Do not process redelivered messages that have already been processed. + logger.Info("rpc already in log, skipping duplicate seq number", meta.Sequence.Stream) + return + } switch schema.RPCMethod(rawRpc.Method) { case schema.RPCMethodChatCreate: diff --git a/comms/server/server_test.go b/comms/server/server_test.go index 85d7b3ba104..0c8c65587dc 100644 --- a/comms/server/server_test.go +++ b/comms/server/server_test.go @@ -214,20 +214,10 @@ func TestGetChats(t *testing.T) { defer res.Body.Close() // Assertions - expectedData := schema.UserChat{ - ChatID: chatId1, - LastMessageAt: chat1CreatedAt.Format(time.RFC3339Nano), - InviteCode: chatId1, - UnreadMessageCount: float64(0), - ChatMembers: []schema.ChatMember{ - expectedMember1, - expectedMember2, - }, - } expectedResponse, err := json.Marshal( schema.CommsResponse{ Health: expectedHealth, - Data: expectedData, + Data: expectedChat1Data, }, ) assert.NoError(t, err) From 41c0c14264330ff93983740e6fa6d9f55de18f14 Mon Sep 17 00:00:00 2001 From: Michelle Brier Date: Wed, 4 Jan 2023 19:17:42 -0800 Subject: [PATCH 07/10] Comms rate limits (#4542) * Rate limiting + tests. TODO limit num chats * Limit chats * Undo accidental changes * Use default limits if KV errors; move jetstream context getter and setter behind mutex in separate package * Rename chats query * Add comments * Use helper fn rather than goto, remove kv population on boot * Limits as ints in test * Combine message queries; block all new messages when any rate limit hit * Log when rate limit hit --- comms/README.md | 27 +-- comms/config/config.go | 4 +- comms/config/constants.go | 16 +- comms/db/queries/get_chat_messages.go | 26 +++ comms/db/queries/get_chats.go | 37 ++++ comms/internal/pubkeystore/recover.go | 5 +- comms/internal/rpcz/apply.go | 3 +- comms/internal/rpcz/chat_block_test.go | 12 +- comms/internal/rpcz/chat_delete_test.go | 8 +- comms/internal/rpcz/chat_permissions_test.go | 4 +- comms/internal/rpcz/chat_test.go | 31 +++- comms/internal/rpcz/rate_limit_test.go | 180 +++++++++++++++++++ comms/internal/rpcz/test_utils.go | 6 +- comms/internal/rpcz/validator.go | 149 +++++++++++++-- comms/jetstream/jetstream_client.go | 30 ++++ comms/peering/nats.go | 22 +-- comms/server/server.go | 11 +- comms/server/server_test.go | 14 +- 18 files changed, 504 insertions(+), 81 deletions(-) create mode 100644 comms/internal/rpcz/rate_limit_test.go create mode 100644 comms/jetstream/jetstream_client.go diff --git a/comms/README.md b/comms/README.md index c33dd53dc56..dcb18f4bf9d 100644 --- a/comms/README.md +++ b/comms/README.md @@ -12,24 +12,11 @@ First time: * Run `make tools` * verify `dbmate -h` works -### running single instance - -``` -make reset -make -``` - -* if you edit go code, restart `make` -* `make psql` to psql - - ### Migrations Use [dbmate](https://github.com/amacneil/dbmate): * `dbmate new create_cool_table` -* `make db.jet` - ### Typings @@ -38,19 +25,23 @@ Use [dbmate](https://github.com/amacneil/dbmate): * Run `make quicktype` * Update go code to use types -### running cluster +### running single instance ``` -make cluster.up +make reset +make ``` -start example client +* if you edit go code, restart `make` +* `make psql` to psql + +### running cluster ``` -go run example/go_client.go +make cluster.up ``` -or +start example client ``` deno run -A example/deno_client.ts diff --git a/comms/config/config.go b/comms/config/config.go index a6d4179d4ad..40326036d4d 100644 --- a/comms/config/config.go +++ b/comms/config/config.go @@ -17,8 +17,8 @@ var ( Env = os.Getenv("audius_discprov_env") - PrivateKey *ecdsa.PrivateKey - WalletAddress string + PrivateKey *ecdsa.PrivateKey + WalletAddress string NkeyPair nkeys.KeyPair NkeyPublic string diff --git a/comms/config/constants.go b/comms/config/constants.go index a8ce13158aa..1db14b5897f 100644 --- a/comms/config/constants.go +++ b/comms/config/constants.go @@ -3,5 +3,19 @@ package config var ( SigHeader = "x-sig" - PubkeystoreBucketName = "pubkeystore" + PubkeystoreBucketName = "pubkeyStore" + + // Rate limit config + RateLimitRulesBucketName = "rateLimitRules" + RateLimitTimeframeHours = "timeframeHours" + RateLimitMaxNumMessages = "maxNumMessages" + RateLimitMaxNumMessagesPerRecipient = "maxNumMessagesPerRecipient" + RateLimitMaxNumNewChats = "maxNumNewChats" + + DefaultRateLimitRules = map[string]int{ + RateLimitTimeframeHours: 24, + RateLimitMaxNumMessages: 2000, + RateLimitMaxNumMessagesPerRecipient: 1000, + RateLimitMaxNumNewChats: 100, + } ) diff --git a/comms/db/queries/get_chat_messages.go b/comms/db/queries/get_chat_messages.go index b173601142d..37592513114 100644 --- a/comms/db/queries/get_chat_messages.go +++ b/comms/db/queries/get_chat_messages.go @@ -102,3 +102,29 @@ func ChatMessagesAndReactions(q db.Queryable, ctx context.Context, arg ChatMessa ) return rows, err } + +const numChatMessagesSince = ` +WITH counts_per_chat AS ( + SELECT COUNT(*) + FROM chat_message + WHERE user_id = $1 and created_at > $2 + GROUP BY chat_id +) +SELECT COALESCE(SUM(count), 0) AS total_count, COALESCE(MAX(count), 0) as max_count_per_chat FROM counts_per_chat; +` + +type NumChatMessagesSinceParams struct { + UserID int32 `db:"user_id" json:"user_id"` + Cursor time.Time `json:"cursor"` +} + +type NumChatMessagesSinceRow struct { + TotalCount int `db:"total_count" json:"total_count"` + MaxCountPerChat int `db:"max_count_per_chat" json:"max_count_per_chat"` +} + +func NumChatMessagesSince(q db.Queryable, ctx context.Context, arg NumChatMessagesSinceParams) (NumChatMessagesSinceRow, error) { + var counts NumChatMessagesSinceRow + err := q.GetContext(ctx, &counts, numChatMessagesSince, arg.UserID, arg.Cursor) + return counts, err +} diff --git a/comms/db/queries/get_chats.go b/comms/db/queries/get_chats.go index 4aade5622ee..2c258afc892 100644 --- a/comms/db/queries/get_chats.go +++ b/comms/db/queries/get_chats.go @@ -6,6 +6,7 @@ import ( "time" "comms.audius.co/db" + "github.com/jmoiron/sqlx" ) type UserChatRow struct { @@ -60,3 +61,39 @@ func UserChats(q db.Queryable, ctx context.Context, userID int32) ([]UserChatRow err := q.SelectContext(ctx, &items, userChats, userID) return items, err } + +const maxNumNewChatsSince = ` +WITH counts AS ( + SELECT COUNT(*) AS count + FROM chat + JOIN chat_member on chat.chat_id = chat_member.chat_id + WHERE chat_member.user_id IN (:Users) AND chat.created_at > :Cursor + GROUP BY chat_member.user_id +) +SELECT COALESCE(MAX(count), 0) FROM counts; +` + +type MaxNumNewChatsSinceParams struct { + Users []int32 `json:"user_id"` + Cursor time.Time `json:"cursor"` +} + +// Return the max number of new chats since CURSOR out of the given USERS +func MaxNumNewChatsSince(q db.Queryable, ctx context.Context, arg MaxNumNewChatsSinceParams) (int, error) { + var count int + argMap := map[string]interface{}{ + "Users": arg.Users, + "Cursor": arg.Cursor, + } + query, args, err := sqlx.Named(maxNumNewChatsSince, argMap) + if err != nil { + return count, err + } + query, args, err = sqlx.In(query, args...) + if err != nil { + return count, err + } + query = q.Rebind(query) + err = q.GetContext(ctx, &count, query, args...) + return count, err +} diff --git a/comms/internal/pubkeystore/recover.go b/comms/internal/pubkeystore/recover.go index ec76ed51227..1055220e41d 100644 --- a/comms/internal/pubkeystore/recover.go +++ b/comms/internal/pubkeystore/recover.go @@ -9,7 +9,7 @@ import ( "comms.audius.co/config" "comms.audius.co/db" - "comms.audius.co/peering" + "comms.audius.co/jetstream" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/signer/core/apitypes" @@ -54,7 +54,8 @@ func RecoverUserPublicKeyBase64(ctx context.Context, userId int) (string, error) conn := db.Conn - kv, err := peering.JetstreamClient.KeyValue(config.PubkeystoreBucketName) + jsc := jetstream.GetJetstreamContext() + kv, err := jsc.KeyValue(config.PubkeystoreBucketName) if err != nil { return "", err } diff --git a/comms/internal/rpcz/apply.go b/comms/internal/rpcz/apply.go index 7b41fc073a6..3766ec3a863 100644 --- a/comms/internal/rpcz/apply.go +++ b/comms/internal/rpcz/apply.go @@ -12,8 +12,7 @@ import ( "github.com/nats-io/nats.go" ) -// Validates + Applies a NATS message - +// Validates + applies a NATS message func Apply(msg *nats.Msg) { var err error logger := config.Logger.New() diff --git a/comms/internal/rpcz/chat_block_test.go b/comms/internal/rpcz/chat_block_test.go index 590a64f9f05..7a67c2abe48 100644 --- a/comms/internal/rpcz/chat_block_test.go +++ b/comms/internal/rpcz/chat_block_test.go @@ -15,9 +15,9 @@ func TestChatBlocking(t *testing.T) { var err error // reset tables under test - _, err = db.Conn.Exec("truncate chat_blocked_users cascade") + _, err = db.Conn.Exec("truncate chat_blocked_users cascade;") assert.NoError(t, err) - _, err = db.Conn.Exec("truncate chat cascade") + _, err = db.Conn.Exec("truncate chat cascade;") assert.NoError(t, err) tx := db.Conn.MustBegin() @@ -74,14 +74,14 @@ func TestChatBlocking(t *testing.T) { chatCreate := string(schema.RPCMethodChatCreate) err = Validators[chatCreate](tx, 91, exampleRpc) - assert.ErrorContains(t, err, "Cannot create a chat with a user you have blocked or user who has blocked you") + assert.ErrorContains(t, err, "Cannot chat with a user you have blocked or user who has blocked you") } // validate 91 and 92 cannot message each other { // Assume chat was already created before blocking chatId := "chat1" - SetUpChatWithMembers(t, tx, chatId, 91, 92) + SetupChatWithMembers(t, tx, chatId, 91, 92) exampleRpc := schema.RawRPC{ Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message_id": "1", "message": "test"}`, chatId)), @@ -90,7 +90,7 @@ func TestChatBlocking(t *testing.T) { chatMessage := string(schema.RPCMethodChatMessage) err = Validators[chatMessage](tx, 91, exampleRpc) - assert.ErrorContains(t, err, "Cannot sent messages to users you have blocked or users who have blocked you") + assert.ErrorContains(t, err, "Cannot chat with a user you have blocked or user who has blocked you") } // user 91 unblocks 92 @@ -98,5 +98,5 @@ func TestChatBlocking(t *testing.T) { assert.NoError(t, err) assertBlocked(91, 92, messageTs, 0) - tx.Commit() + tx.Rollback() } diff --git a/comms/internal/rpcz/chat_delete_test.go b/comms/internal/rpcz/chat_delete_test.go index dadb8de5ba2..76d0302d21c 100644 --- a/comms/internal/rpcz/chat_delete_test.go +++ b/comms/internal/rpcz/chat_delete_test.go @@ -15,15 +15,13 @@ func TestChatDeletion(t *testing.T) { var err error // reset tables under test - _, err = db.Conn.Exec("truncate chat cascade") + _, err = db.Conn.Exec("truncate chat cascade;") assert.NoError(t, err) tx := db.Conn.MustBegin() - // TODO test queries - chatId := "chat1" - SetUpChatWithMembers(t, tx, chatId, 91, 92) + SetupChatWithMembers(t, tx, chatId, 91, 92) assertDeleted := func(chatId string, userId int, expectDeleted bool) { row := tx.QueryRow("select cleared_history_at from chat_member where chat_id = $1 and user_id = $2", chatId, userId) @@ -61,5 +59,5 @@ func TestChatDeletion(t *testing.T) { // chat is not deleted for 92 assertDeleted(chatId, 92, false) - tx.Commit() + tx.Rollback() } diff --git a/comms/internal/rpcz/chat_permissions_test.go b/comms/internal/rpcz/chat_permissions_test.go index 867ba5d4731..ff630dd1785 100644 --- a/comms/internal/rpcz/chat_permissions_test.go +++ b/comms/internal/rpcz/chat_permissions_test.go @@ -13,7 +13,7 @@ func TestChatPermissions(t *testing.T) { var err error // reset tables under test - _, err = db.Conn.Exec("truncate chat_permissions cascade") + _, err = db.Conn.Exec("truncate chat_permissions cascade;") assert.NoError(t, err) tx := db.Conn.MustBegin() @@ -50,5 +50,5 @@ func TestChatPermissions(t *testing.T) { assertPermissions(userId, schema.Followees, 0) assertPermissions(userId, schema.None, 1) - tx.Commit() + tx.Rollback() } diff --git a/comms/internal/rpcz/chat_test.go b/comms/internal/rpcz/chat_test.go index 1061d87cdea..a7797634c26 100644 --- a/comms/internal/rpcz/chat_test.go +++ b/comms/internal/rpcz/chat_test.go @@ -7,7 +7,11 @@ import ( "time" "comms.audius.co/db" + "comms.audius.co/jetstream" "comms.audius.co/schema" + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats-server/v2/test" + "github.com/nats-io/nats.go" "github.com/stretchr/testify/assert" ) @@ -17,12 +21,27 @@ func TestChat(t *testing.T) { chatId := "chat1" // reset tables under test - _, err = db.Conn.Exec("truncate chat cascade") + _, err = db.Conn.Exec("truncate chat cascade;") assert.NoError(t, err) tx := db.Conn.MustBegin() - SetUpChatWithMembers(t, tx, chatId, 91, 92) + SetupChatWithMembers(t, tx, chatId, 91, 92) + + // Connect to NATS and create JetStream Context + opts := server.Options{ + Host: "127.0.0.1", + Port: 4222, + JetStream: true, + } + natsServer := test.RunServer(&opts) + defer natsServer.Shutdown() + nc, err := nats.Connect(nats.DefaultURL) + assert.NoError(t, err) + defer nc.Close() + js, err := nc.JetStream(nats.PublishAsyncMaxPending(256)) + assert.NoError(t, err) + jetstream.SetJetstreamContext(js) // validate 91 and 92 can both send messages in this chat { @@ -30,12 +49,12 @@ func TestChat(t *testing.T) { Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "test123"}`, chatId)), } - chatSay := string(schema.RPCMethodChatMessage) + chatMessage := string(schema.RPCMethodChatMessage) - err = Validators[chatSay](tx, 91, exampleRpc) + err = Validators[chatMessage](tx, 91, exampleRpc) assert.NoError(t, err) - err = Validators[chatSay](tx, 93, exampleRpc) + err = Validators[chatMessage](tx, 93, exampleRpc) assert.ErrorIs(t, err, sql.ErrNoRows) } @@ -109,5 +128,5 @@ func TestChat(t *testing.T) { err = chatReactMessage(tx, 91, replyMessageId, newReaction, changedReactTs) assertReaction(91, replyMessageId, newReaction) - tx.Commit() + tx.Rollback() } diff --git a/comms/internal/rpcz/rate_limit_test.go b/comms/internal/rpcz/rate_limit_test.go new file mode 100644 index 00000000000..5cfecb3d09e --- /dev/null +++ b/comms/internal/rpcz/rate_limit_test.go @@ -0,0 +1,180 @@ +package rpcz + +import ( + "fmt" + "strconv" + "testing" + "time" + + "comms.audius.co/config" + "comms.audius.co/db" + "comms.audius.co/jetstream" + "comms.audius.co/misc" + "comms.audius.co/schema" + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats-server/v2/test" + "github.com/nats-io/nats.go" + "github.com/stretchr/testify/assert" +) + +func TestRateLimit(t *testing.T) { + var err error + + // reset tables under test + _, err = db.Conn.Exec("truncate chat cascade;") + assert.NoError(t, err) + + // Connect to NATS and create JetStream Context + opts := server.Options{ + Host: "127.0.0.1", + Port: 4222, + JetStream: true, + } + natsServer := test.RunServer(&opts) + defer natsServer.Shutdown() + nc, err := nats.Connect(nats.DefaultURL) + assert.NoError(t, err) + defer nc.Close() + js, err := nc.JetStream(nats.PublishAsyncMaxPending(256)) + assert.NoError(t, err) + jetstream.SetJetstreamContext(js) + + // Create rate limit KV + kv, err := js.CreateKeyValue(&nats.KeyValueConfig{ + Bucket: config.RateLimitRulesBucketName, + Replicas: 1, + }) + assert.NoError(t, err) + + // Add test rules + testRules := map[string]int{ + config.RateLimitTimeframeHours: 24, + config.RateLimitMaxNumMessages: 3, + config.RateLimitMaxNumMessagesPerRecipient: 2, + config.RateLimitMaxNumNewChats: 2, + } + for rule, limit := range testRules { + _, err := kv.PutString(rule, strconv.Itoa(limit)) + assert.NoError(t, err) + } + + tx := db.Conn.MustBegin() + + chatMessage := string(schema.RPCMethodChatMessage) + chatCreate := string(schema.RPCMethodChatCreate) + + user91Encoded, err := misc.EncodeHashId(91) + assert.NoError(t, err) + user93Encoded, err := misc.EncodeHashId(93) + assert.NoError(t, err) + user94Encoded, err := misc.EncodeHashId(94) + assert.NoError(t, err) + user95Encoded, err := misc.EncodeHashId(95) + assert.NoError(t, err) + + // 91 created a new chat with 92 48 hours ago + chatId1 := "chat1" + chatTs := time.Now().UTC().Add(-time.Hour * time.Duration(48)) + _, err = tx.Exec("insert into chat (chat_id, created_at, last_message_at) values ($1, $2, $2)", chatId1, chatTs) + assert.NoError(t, err) + _, err = tx.Exec("insert into chat_member (chat_id, invited_by_user_id, invite_code, user_id) values ($1, $2, $1, $2), ($1, $2, $1, $3)", chatId1, 91, 92) + assert.NoError(t, err) + + // 91 messaged 92 48 hours ago + err = chatSendMessage(tx, 91, chatId1, "1", chatTs, "Hello") + assert.NoError(t, err) + + // 91 messages 92 twice now + message := "Hello today 1" + messageRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "%s"}`, chatId1, message)), + } + err = Validators[chatMessage](tx, 91, messageRpc) + assert.NoError(t, err) + err = chatSendMessage(tx, 91, chatId1, "2", time.Now().UTC(), message) + assert.NoError(t, err) + message = "Hello today 2" + messageRpc = schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "%s"}`, chatId1, message)), + } + err = Validators[chatMessage](tx, 91, messageRpc) + assert.NoError(t, err) + err = chatSendMessage(tx, 91, chatId1, "3", time.Now().UTC(), message) + assert.NoError(t, err) + + // 91 messages 92 a 3rd time + // Blocked by rate limiter (hit max # messages per recipient in the past 24 hours) + message = "Hello again again." + messageRpc = schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "%s"}`, chatId1, message)), + } + err = Validators[chatMessage](tx, 91, messageRpc) + assert.ErrorContains(t, err, "User has exceeded the maximum number of new messages") + + // 91 creates a new chat with 93 (1 chat created in 24h) + chatId2 := "chat2" + createRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "invites": [{"user_id": "%s", "invite_code": "%s"}, {"user_id": "%s", "invite_code": "%s"}]}`, chatId2, user91Encoded, chatId2, user93Encoded, chatId2)), + } + err = Validators[chatCreate](tx, 91, createRpc) + assert.NoError(t, err) + SetupChatWithMembers(t, tx, chatId2, 91, 93) + + // 91 messages 93 + // Still blocked by rate limiter (hit max # messages with 92 in the past 24h) + message = "Hi 93" + messageRpc = schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "%s"}`, chatId2, message)), + } + err = Validators[chatMessage](tx, 91, messageRpc) + assert.ErrorContains(t, err, "User has exceeded the maximum number of new messages") + + // Remove message 3 from db so can test other rate limits + _, err = tx.Exec("delete from chat_message where message_id = '3'") + assert.NoError(t, err) + + // 91 should be able to message 93 now + err = Validators[chatMessage](tx, 91, messageRpc) + assert.NoError(t, err) + err = chatSendMessage(tx, 91, chatId2, "3", time.Now().UTC(), message) + assert.NoError(t, err) + + // 91 creates a new chat with 94 (2 chats created in 24h) + chatId3 := "chat3" + createRpc = schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "invites": [{"user_id": "%s", "invite_code": "%s"}, {"user_id": "%s", "invite_code": "%s"}]}`, chatId3, user91Encoded, chatId3, user94Encoded, chatId3)), + } + err = Validators[chatCreate](tx, 91, createRpc) + assert.NoError(t, err) + SetupChatWithMembers(t, tx, chatId3, 91, 94) + + // 91 messages 94 + message = "Hi 94 again" + messageRpc = schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "%s"}`, chatId3, message)), + } + err = Validators[chatMessage](tx, 91, messageRpc) + assert.NoError(t, err) + err = chatSendMessage(tx, 91, chatId3, "4", time.Now().UTC(), message) + assert.NoError(t, err) + + // 91 messages 94 again (4th message to anyone in 24h) + // Blocked by rate limiter (hit max # messages in the past 24 hours) + message = "Hi 94 again" + messageRpc = schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "%s"}`, chatId3, message)), + } + err = Validators[chatMessage](tx, 91, messageRpc) + assert.ErrorContains(t, err, "User has exceeded the maximum number of new messages") + + // 91 creates a new chat with 95 (3 chats created in 24h) + // Blocked by rate limiter (hit max # new chats) + chatId4 := "chat4" + createRpc = schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "invites": [{"user_id": "%s", "invite_code": "%s"}, {"user_id": "%s", "invite_code": "%s"}]}`, chatId4, user91Encoded, chatId2, user95Encoded, chatId4)), + } + err = Validators[chatCreate](tx, 91, createRpc) + assert.ErrorContains(t, err, "An invited user has exceeded the maximum number of new chats") + + tx.Rollback() +} diff --git a/comms/internal/rpcz/test_utils.go b/comms/internal/rpcz/test_utils.go index 3ef72faa9e3..3eff010eff2 100644 --- a/comms/internal/rpcz/test_utils.go +++ b/comms/internal/rpcz/test_utils.go @@ -8,15 +8,15 @@ import ( "github.com/stretchr/testify/assert" ) -func SetUpChatWithMembers(t *testing.T, tx *sqlx.Tx, chatId string, user1 int, user2 int) { +func SetupChatWithMembers(t *testing.T, tx *sqlx.Tx, chatId string, user1 int, user2 int) { var err error // create chat // - should create chat and initial self invite in one tx - _, err = tx.Exec("insert into chat (chat_id, created_at, last_message_at) values ($1, $2, $2)", chatId, time.Now()) + _, err = tx.Exec("insert into chat (chat_id, created_at, last_message_at) values ($1, $2, $2)", chatId, time.Now().UTC()) assert.NoError(t, err) // insert two members - _, err = tx.Exec("insert into chat_member (chat_id, invited_by_user_id, invite_code, user_id) values ($1, $2, $1, $2), ($1, $2, $1, $3)", chatId, 91, 92) + _, err = tx.Exec("insert into chat_member (chat_id, invited_by_user_id, invite_code, user_id) values ($1, $2, $1, $2), ($1, $2, $1, $3)", chatId, user1, user2) assert.NoError(t, err) } diff --git a/comms/internal/rpcz/validator.go b/comms/internal/rpcz/validator.go index 9573898dc38..303a2ed69db 100644 --- a/comms/internal/rpcz/validator.go +++ b/comms/internal/rpcz/validator.go @@ -4,13 +4,17 @@ import ( "context" "encoding/json" "errors" + "strconv" + "time" "comms.audius.co/config" "comms.audius.co/db" "comms.audius.co/db/queries" + "comms.audius.co/jetstream" "comms.audius.co/misc" "comms.audius.co/schema" "github.com/jmoiron/sqlx" + "github.com/nats-io/nats.go" ) type validatorFunc func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error @@ -67,20 +71,28 @@ var Validators = map[string]validatorFunc{ if err != nil { return err } - blockedCount, err := queries.CountChatBlocks(q, context.Background(), queries.CountChatBlocksParams{ - User1: int32(user1), - User2: int32(user2), - }) + err = validateNotBlocked(q, int32(user1), int32(user2)) if err != nil { return err } - if blockedCount > 0 { - return errors.New("Cannot create a chat with a user you have blocked or user who has blocked you") - } } // TODO check receiving invitee's permission settings + // validate does not exceed new chat rate limit for any invited users + var users []int32 + for _, invite := range params.Invites { + userId, err := misc.DecodeHashId(invite.UserID) + if err != nil { + return err + } + users = append(users, int32(userId)) + } + err = validateNewChatRateLimit(q, users) + if err != nil { + return err + } + return nil }, string(schema.RPCMethodChatDelete): func(tx *sqlx.Tx, userId int32, rpc schema.RawRPC) error { @@ -130,16 +142,16 @@ var Validators = map[string]validatorFunc{ // for 1-1 DMs: validate chat members are not a pair chatMembers, err := queries.ChatMembers(q, context.Background(), params.ChatID) if len(chatMembers) == 2 { - blockedCount, err := queries.CountChatBlocks(q, context.Background(), queries.CountChatBlocksParams{ - User1: chatMembers[0].UserID, - User2: chatMembers[1].UserID, - }) + err = validateNotBlocked(q, chatMembers[0].UserID, chatMembers[1].UserID) if err != nil { return err } - if blockedCount > 0 { - return errors.New("Cannot sent messages to users you have blocked or users who have blocked you") - } + } + + // validate does not exceed new message rate limit + err = validateNewMessageRateLimit(q, userId, params.ChatID) + if err != nil { + return err } return nil @@ -249,6 +261,7 @@ var Validators = map[string]validatorFunc{ } // Helpers + func validateChatMembership(q db.Queryable, userId int32, chatId string) (db.ChatMember, error) { member, err := queries.ChatMembership(q, context.Background(), queries.ChatMembershipParams{ UserID: userId, @@ -256,3 +269,111 @@ func validateChatMembership(q db.Queryable, userId int32, chatId string) (db.Cha }) return member, err } + +func validateNotBlocked(q db.Queryable, user1 int32, user2 int32) error { + blockedCount, err := queries.CountChatBlocks(q, context.Background(), queries.CountChatBlocksParams{ + User1: user1, + User2: user2, + }) + if err != nil { + return err + } + if blockedCount > 0 { + return errors.New("Cannot chat with a user you have blocked or user who has blocked you") + } + + return nil +} + +func getRateLimit(kv nats.KeyValue, rule string, fallback int) int { + got, err := kv.Get(rule) + if err != nil { + config.Logger.Warn("unable to retrive rate limit KV rule, using default value", "error", err, "rule", rule) + return fallback + } + limit, err := strconv.Atoi(string(got.Value())) + if err != nil { + config.Logger.Warn("unable to convert rate limit from KV to int, using default value", "error", err, "rule", rule) + return fallback + } + return limit +} + +// Calculate cursor from rate limit timeframe +func calculateRateLimitCursor(timeframe int) time.Time { + return time.Now().UTC().Add(-time.Hour * time.Duration(timeframe)) +} + +func validateNewChatRateLimit(q db.Queryable, users []int32) error { + var err error + + timeframe := config.DefaultRateLimitRules[config.RateLimitTimeframeHours] + // Max num of new chats permitted per timeframe + maxNumChats := config.DefaultRateLimitRules[config.RateLimitMaxNumNewChats] + + // Retrieve rate limit rules KV + jsc := jetstream.GetJetstreamContext() + if kv, err := jsc.KeyValue(config.RateLimitRulesBucketName); err == nil { + timeframe = getRateLimit(kv, config.RateLimitTimeframeHours, timeframe) + maxNumChats = getRateLimit(kv, config.RateLimitMaxNumNewChats, maxNumChats) + } else { + config.Logger.Warn("unable to retrive rate limit KV, using default values", "error", err) + } + + cursor := calculateRateLimitCursor(timeframe) + numChats, err := queries.MaxNumNewChatsSince(q, context.Background(), queries.MaxNumNewChatsSinceParams{ + Users: users, + Cursor: cursor, + }) + if err != nil { + return err + } + if numChats >= maxNumChats { + config.Logger.Info("hit rate limit (new chats)", "users", users) + return errors.New("An invited user has exceeded the maximum number of new chats") + } + + return nil +} + +func validateNewMessageRateLimit(q db.Queryable, userId int32, chatId string) error { + var err error + + timeframe := config.DefaultRateLimitRules[config.RateLimitTimeframeHours] + // Max number of new messages permitted per timeframe + maxNumMessages := config.DefaultRateLimitRules[config.RateLimitMaxNumMessages] + // Max number of new messages permitted per recipient (chat) per timeframe + maxNumMessagesPerRecipient := config.DefaultRateLimitRules[config.RateLimitMaxNumMessagesPerRecipient] + + // Retrieve rate limit rules KV + jsc := jetstream.GetJetstreamContext() + if kv, err := jsc.KeyValue(config.RateLimitRulesBucketName); err == nil { + timeframe = getRateLimit(kv, config.RateLimitTimeframeHours, timeframe) + maxNumMessages = getRateLimit(kv, config.RateLimitMaxNumMessages, maxNumMessages) + maxNumMessagesPerRecipient = getRateLimit(kv, config.RateLimitMaxNumMessagesPerRecipient, maxNumMessagesPerRecipient) + } else { + config.Logger.Warn("unable to retrive rate limit KV, using default values", "error", err) + } + + // Cursor for rate limit timeframe + cursor := calculateRateLimitCursor(timeframe) + + counts, err := queries.NumChatMessagesSince(q, context.Background(), queries.NumChatMessagesSinceParams{ + UserID: userId, + Cursor: cursor, + }) + if err != nil { + return err + } + if counts.TotalCount >= maxNumMessages || counts.MaxCountPerChat >= maxNumMessagesPerRecipient { + if counts.TotalCount >= maxNumMessages { + config.Logger.Info("hit rate limit (total count new messages)", "user", userId, "chat", chatId) + } + if counts.MaxCountPerChat >= maxNumMessagesPerRecipient { + config.Logger.Info("hit rate limit (new messages per recipient)", "user", userId, "chat", chatId) + } + return errors.New("User has exceeded the maximum number of new messages") + } + + return nil +} diff --git a/comms/jetstream/jetstream_client.go b/comms/jetstream/jetstream_client.go new file mode 100644 index 00000000000..14e58d428ad --- /dev/null +++ b/comms/jetstream/jetstream_client.go @@ -0,0 +1,30 @@ +package jetstream + +import ( + "sync" + + "github.com/nats-io/nats.go" +) + +type JetstreamClient struct { + sync.Mutex + context nats.JetStreamContext +} + +var ( + jetstreamClient JetstreamClient +) + +func SetJetstreamContext(jsc nats.JetStreamContext) { + client := &jetstreamClient + client.Lock() + client.context = jsc + client.Unlock() +} + +func GetJetstreamContext() nats.JetStreamContext { + client := &jetstreamClient + client.Lock() + defer client.Unlock() + return client.context +} diff --git a/comms/peering/nats.go b/comms/peering/nats.go index f16f28ab2b1..fbc608b3bd9 100644 --- a/comms/peering/nats.go +++ b/comms/peering/nats.go @@ -10,15 +10,12 @@ import ( "comms.audius.co/config" "comms.audius.co/internal/rpcz" + "comms.audius.co/jetstream" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go" ) -// NATS CLIENTS -var ( - NatsClient *nats.Conn - JetstreamClient nats.JetStreamContext -) +var NatsClient *nats.Conn type NatsManager struct { natsServer *server.Server @@ -105,7 +102,6 @@ func (manager *NatsManager) StartNats(peerMap map[string]*Info) { manager.setupNatsClient() manager.setupJetstream() - } func (manager *NatsManager) setupNatsClient() { @@ -229,11 +225,17 @@ func (manager *NatsManager) setupJetstream() { Replicas: config.NatsReplicaCount, }) if err != nil { - log.Fatal("CreateKeyValue failed", err) + log.Fatal("CreateKeyValue failed", err, "bucket", config.PubkeystoreBucketName) + } + _, err = jsc.CreateKeyValue(&nats.KeyValueConfig{ + Bucket: config.RateLimitRulesBucketName, + Replicas: config.NatsReplicaCount, + }) + if err != nil { + log.Fatal("CreateKeyValue failed", err, "bucket", config.RateLimitRulesBucketName) } - // finally "expose" this as public var + // finally "expose" this via the jetstream package // the server checks if this is non-nil to know if it's ready - // todo: THIS IS NOT SAFE... should be something like peering.GetJetstream with a mutex - JetstreamClient = jsc + jetstream.SetJetstreamContext(jsc) } diff --git a/comms/server/server.go b/comms/server/server.go index 1d8c589e290..0f7d6bb7c95 100644 --- a/comms/server/server.go +++ b/comms/server/server.go @@ -14,6 +14,7 @@ import ( "comms.audius.co/db/queries" "comms.audius.co/internal/pubkeystore" "comms.audius.co/internal/rpcz" + "comms.audius.co/jetstream" "comms.audius.co/misc" "comms.audius.co/peering" "comms.audius.co/schema" @@ -234,7 +235,8 @@ func createServer() *echo.Echo { // subject := "audius.comms.demo" subject := "audius.staging.relay" - if peering.JetstreamClient == nil { + jsc := jetstream.GetJetstreamContext() + if jsc == nil { return c.String(500, "jetstream not ready") } @@ -242,7 +244,7 @@ func createServer() *echo.Echo { msg := nats.NewMsg(subject) msg.Header.Add(config.SigHeader, c.Request().Header.Get(config.SigHeader)) msg.Data = payload - ok, err := peering.JetstreamClient.PublishMsg(msg) + ok, err := jsc.PublishMsg(msg) if err != nil { logger.Warn(string(payload), "err", err) return c.String(500, err.Error()) @@ -282,7 +284,8 @@ func createServer() *echo.Echo { rpcz.Validate(userId, rawRpc) subject := "audius.dms.demo" - if peering.JetstreamClient == nil { + jsc := jetstream.GetJetstreamContext() + if jsc == nil { return c.JSON(500, "jetstream not ready") } @@ -290,7 +293,7 @@ func createServer() *echo.Echo { msg := nats.NewMsg(subject) msg.Header.Add(config.SigHeader, c.Request().Header.Get(config.SigHeader)) msg.Data = payload - ok, err := peering.JetstreamClient.PublishMsg(msg) + ok, err := jsc.PublishMsg(msg) if err != nil { logger.Warn(string(payload), "wallet", wallet, "err", err) return c.JSON(500, err.Error()) diff --git a/comms/server/server_test.go b/comms/server/server_test.go index 0c8c65587dc..f7fd3e200b1 100644 --- a/comms/server/server_test.go +++ b/comms/server/server_test.go @@ -83,9 +83,9 @@ func TestGetChats(t *testing.T) { wallet3 := crypto.PubkeyToAddress(privateKey3.PublicKey).Hex() // Set up db - _, err = db.Conn.Exec("truncate chat cascade") + _, err = db.Conn.Exec("truncate chat cascade;") assert.NoError(t, err) - _, err = db.Conn.Exec("truncate users cascade") + _, err = db.Conn.Exec("truncate users cascade;") assert.NoError(t, err) tx := db.Conn.MustBegin() @@ -104,7 +104,8 @@ func TestGetChats(t *testing.T) { // Insert members into chats (1 and 2, 1 and 3) _, err = tx.Exec("insert into chat_member (chat_id, invited_by_user_id, invite_code, user_id) values ($1, $2, $1, $2), ($1, $2, $1, $3), ($4, $2, $4, $2), ($4, $2, $4, $5)", chatId1, 1, 2, chatId2, 3) assert.NoError(t, err) - tx.Commit() + err = tx.Commit() + assert.NoError(t, err) // Common expected responses expectedHealth := schema.Health{ @@ -244,9 +245,9 @@ func TestGetMessages(t *testing.T) { wallet2 := crypto.PubkeyToAddress(privateKey2.PublicKey).Hex() // Set up db - _, err = db.Conn.Exec("truncate chat cascade") + _, err = db.Conn.Exec("truncate chat cascade;") assert.NoError(t, err) - _, err = db.Conn.Exec("truncate users cascade") + _, err = db.Conn.Exec("truncate users cascade;") assert.NoError(t, err) tx := db.Conn.MustBegin() @@ -282,7 +283,8 @@ func TestGetMessages(t *testing.T) { _, err = tx.Exec("insert into chat_message_reactions (user_id, message_id, reaction, created_at) values ($1, $2, $3, $4), ($5, $2, $6, $7)", 1, messageId1, reaction1, reaction1CreatedAt, 2, reaction2, reaction2CreatedAt) assert.NoError(t, err) - tx.Commit() + err = tx.Commit() + assert.NoError(t, err) // Test GET /comms/chats/:id/messages e := createServer() From 785051772a7e369878412dd02a86870d4d2c04e3 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Thu, 5 Jan 2023 11:04:52 -0500 Subject: [PATCH 08/10] Don't run package tests in parallel. Package tests will truncate and write to tables and step on each other when run in parallel, so run in serial for now. Would be nice to have tests run in parallel for faster tests. Some options: - each package TestMain creates a dedicated test database from template for that package - tests generate unique IDs to avoid data conflicts - tests don't trucate and by convention rollback at the end so no data is written --- comms/Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/comms/Makefile b/comms/Makefile index b8089dd3439..3729c93823a 100644 --- a/comms/Makefile +++ b/comms/Makefile @@ -43,8 +43,7 @@ fmt:: test:: docker compose up -d comdb - # go test ./database -v -count=1 - go test ./... + go test ./... -count=1 -p=1 # this is a "fast build and push" From fc3f4f1007e6a42d978c5c76084dd30e0d2550d5 Mon Sep 17 00:00:00 2001 From: Michelle Brier Date: Thu, 5 Jan 2023 09:59:23 -0800 Subject: [PATCH 09/10] Fix typos when truncating tables in tests; close db connection after running tests --- comms/internal/rpcz/chat_block_test.go | 4 ++-- comms/internal/rpcz/chat_delete_test.go | 2 +- comms/internal/rpcz/chat_permissions_test.go | 2 +- comms/internal/rpcz/chat_test.go | 2 +- comms/internal/rpcz/main_test.go | 4 +++- comms/internal/rpcz/rate_limit_test.go | 2 +- comms/server/main_test.go | 4 +++- comms/server/server_test.go | 8 ++++---- 8 files changed, 16 insertions(+), 12 deletions(-) diff --git a/comms/internal/rpcz/chat_block_test.go b/comms/internal/rpcz/chat_block_test.go index 7a67c2abe48..331fd9ada21 100644 --- a/comms/internal/rpcz/chat_block_test.go +++ b/comms/internal/rpcz/chat_block_test.go @@ -15,9 +15,9 @@ func TestChatBlocking(t *testing.T) { var err error // reset tables under test - _, err = db.Conn.Exec("truncate chat_blocked_users cascade;") + _, err = db.Conn.Exec("truncate table chat_blocked_users cascade") assert.NoError(t, err) - _, err = db.Conn.Exec("truncate chat cascade;") + _, err = db.Conn.Exec("truncate table chat cascade") assert.NoError(t, err) tx := db.Conn.MustBegin() diff --git a/comms/internal/rpcz/chat_delete_test.go b/comms/internal/rpcz/chat_delete_test.go index 76d0302d21c..bb8d70bed68 100644 --- a/comms/internal/rpcz/chat_delete_test.go +++ b/comms/internal/rpcz/chat_delete_test.go @@ -15,7 +15,7 @@ func TestChatDeletion(t *testing.T) { var err error // reset tables under test - _, err = db.Conn.Exec("truncate chat cascade;") + _, err = db.Conn.Exec("truncate table chat cascade") assert.NoError(t, err) tx := db.Conn.MustBegin() diff --git a/comms/internal/rpcz/chat_permissions_test.go b/comms/internal/rpcz/chat_permissions_test.go index ff630dd1785..e6763c6ab91 100644 --- a/comms/internal/rpcz/chat_permissions_test.go +++ b/comms/internal/rpcz/chat_permissions_test.go @@ -13,7 +13,7 @@ func TestChatPermissions(t *testing.T) { var err error // reset tables under test - _, err = db.Conn.Exec("truncate chat_permissions cascade;") + _, err = db.Conn.Exec("truncate table chat_permissions cascade") assert.NoError(t, err) tx := db.Conn.MustBegin() diff --git a/comms/internal/rpcz/chat_test.go b/comms/internal/rpcz/chat_test.go index a7797634c26..9005fcebff8 100644 --- a/comms/internal/rpcz/chat_test.go +++ b/comms/internal/rpcz/chat_test.go @@ -21,7 +21,7 @@ func TestChat(t *testing.T) { chatId := "chat1" // reset tables under test - _, err = db.Conn.Exec("truncate chat cascade;") + _, err = db.Conn.Exec("truncate table chat cascade") assert.NoError(t, err) tx := db.Conn.MustBegin() diff --git a/comms/internal/rpcz/main_test.go b/comms/internal/rpcz/main_test.go index 3bb2b711ca7..7500eec20fe 100644 --- a/comms/internal/rpcz/main_test.go +++ b/comms/internal/rpcz/main_test.go @@ -20,6 +20,8 @@ func TestMain(m *testing.M) { // run tests code := m.Run() - // teardown code here... + // teardown + defer db.Conn.Close() + os.Exit(code) } diff --git a/comms/internal/rpcz/rate_limit_test.go b/comms/internal/rpcz/rate_limit_test.go index 5cfecb3d09e..1b09742799a 100644 --- a/comms/internal/rpcz/rate_limit_test.go +++ b/comms/internal/rpcz/rate_limit_test.go @@ -21,7 +21,7 @@ func TestRateLimit(t *testing.T) { var err error // reset tables under test - _, err = db.Conn.Exec("truncate chat cascade;") + _, err = db.Conn.Exec("truncate table chat cascade") assert.NoError(t, err) // Connect to NATS and create JetStream Context diff --git a/comms/server/main_test.go b/comms/server/main_test.go index 9d750d93e79..da9a8bbb918 100644 --- a/comms/server/main_test.go +++ b/comms/server/main_test.go @@ -20,6 +20,8 @@ func TestMain(m *testing.M) { // run tests code := m.Run() - // teardown code here... + // teardown + defer db.Conn.Close() + os.Exit(code) } diff --git a/comms/server/server_test.go b/comms/server/server_test.go index f7fd3e200b1..d3470a45046 100644 --- a/comms/server/server_test.go +++ b/comms/server/server_test.go @@ -83,9 +83,9 @@ func TestGetChats(t *testing.T) { wallet3 := crypto.PubkeyToAddress(privateKey3.PublicKey).Hex() // Set up db - _, err = db.Conn.Exec("truncate chat cascade;") + _, err = db.Conn.Exec("truncate table chat cascade") assert.NoError(t, err) - _, err = db.Conn.Exec("truncate users cascade;") + _, err = db.Conn.Exec("truncate table users cascade") assert.NoError(t, err) tx := db.Conn.MustBegin() @@ -245,9 +245,9 @@ func TestGetMessages(t *testing.T) { wallet2 := crypto.PubkeyToAddress(privateKey2.PublicKey).Hex() // Set up db - _, err = db.Conn.Exec("truncate chat cascade;") + _, err = db.Conn.Exec("truncate table chat cascade") assert.NoError(t, err) - _, err = db.Conn.Exec("truncate users cascade;") + _, err = db.Conn.Exec("truncate table users cascade") assert.NoError(t, err) tx := db.Conn.MustBegin() From b48b6c059632c9a92b470ef28c7ad53aadb33cd5 Mon Sep 17 00:00:00 2001 From: Steve Perkins Date: Thu, 5 Jan 2023 13:48:16 -0500 Subject: [PATCH 10/10] test cleanup --- comms/internal/rpcz/main_test.go | 2 +- comms/peering/sps_test.go | 2 +- comms/server/main_test.go | 2 +- comms/server/server_test.go | 16 +++------------- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/comms/internal/rpcz/main_test.go b/comms/internal/rpcz/main_test.go index 7500eec20fe..71e396c6159 100644 --- a/comms/internal/rpcz/main_test.go +++ b/comms/internal/rpcz/main_test.go @@ -21,7 +21,7 @@ func TestMain(m *testing.M) { code := m.Run() // teardown - defer db.Conn.Close() + db.Conn.Close() os.Exit(code) } diff --git a/comms/peering/sps_test.go b/comms/peering/sps_test.go index eb4e1dd2467..ac240304f6d 100644 --- a/comms/peering/sps_test.go +++ b/comms/peering/sps_test.go @@ -6,7 +6,7 @@ import ( ) func TestPeers(t *testing.T) { - // t.Skip() + t.Skip() input := map[string]interface{}{ "query": gql, diff --git a/comms/server/main_test.go b/comms/server/main_test.go index da9a8bbb918..7e180bf53aa 100644 --- a/comms/server/main_test.go +++ b/comms/server/main_test.go @@ -21,7 +21,7 @@ func TestMain(m *testing.M) { code := m.Run() // teardown - defer db.Conn.Close() + db.Conn.Close() os.Exit(code) } diff --git a/comms/server/server_test.go b/comms/server/server_test.go index d3470a45046..6d253c74f94 100644 --- a/comms/server/server_test.go +++ b/comms/server/server_test.go @@ -1,7 +1,6 @@ package server import ( - "bytes" "crypto/ecdsa" "encoding/base64" "encoding/json" @@ -186,10 +185,7 @@ func TestGetChats(t *testing.T) { assert.NoError(t, err) if assert.NoError(t, getChats(c)) { assert.Equal(t, http.StatusOK, rec.Code) - // Remove insignificant space characters - compactResp := new(bytes.Buffer) - err = json.Compact(compactResp, rec.Body.Bytes()) - assert.Equal(t, string(expectedResponse), compactResp.String()) + assert.JSONEq(t, string(expectedResponse), rec.Body.String()) } } @@ -224,10 +220,7 @@ func TestGetChats(t *testing.T) { assert.NoError(t, err) if assert.NoError(t, getChat(c)) { assert.Equal(t, http.StatusOK, rec.Code) - // Remove insignificant space characters - compactResp := new(bytes.Buffer) - err = json.Compact(compactResp, rec.Body.Bytes()) - assert.Equal(t, string(expectedResponse), compactResp.String()) + assert.JSONEq(t, string(expectedResponse), rec.Body.String()) } } } @@ -358,10 +351,7 @@ func TestGetMessages(t *testing.T) { assert.NoError(t, err) if assert.NoError(t, getMessages(c)) { assert.Equal(t, http.StatusOK, rec.Code) - // Remove insignificant space characters - compactResp := new(bytes.Buffer) - err = json.Compact(compactResp, rec.Body.Bytes()) - assert.Equal(t, string(expectedResponse), compactResp.String()) + assert.JSONEq(t, string(expectedResponse), rec.Body.String()) } }