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/.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..a56cb266cb1 --- /dev/null +++ b/comms/Dockerfile @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1 + +FROM golang:latest + +# install in-container tools: dbmate, nats cli +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 && \ + 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 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..3729c93823a --- /dev/null +++ b/comms/Makefile @@ -0,0 +1,75 @@ + +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 ./... -count=1 -p=1 + + +# 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..dcb18f4bf9d --- /dev/null +++ b/comms/README.md @@ -0,0 +1,48 @@ +# 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 + +### Migrations + +Use [dbmate](https://github.com/amacneil/dbmate): + +* `dbmate new create_cool_table` + +### 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 single instance + +``` +make reset +make +``` + +* if you edit go code, restart `make` +* `make psql` to psql + +### running cluster + +``` +make cluster.up +``` + +start example client + +``` +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..40326036d4d --- /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..1db14b5897f --- /dev/null +++ b/comms/config/constants.go @@ -0,0 +1,21 @@ +package config + +var ( + SigHeader = "x-sig" + + 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/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..2001c7af8d6 --- /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 cascade; 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..118c2f05853 --- /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 cascade; 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..17a64dff641 --- /dev/null +++ b/comms/db/migrations/20221202144236_create_chat.sql @@ -0,0 +1,44 @@ +-- migrate:up + +create table if not exists chat ( + chat_id text primary key, + created_at timestamp not null, + last_message_at timestamp not null +); + +create index if not exists chat_chat_id_idx on chat(chat_id); + +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, + + 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 if not exists chat_member_user_idx on chat_member(user_id); + +create table if not exists 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 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 new file mode 100644 index 00000000000..ce5c7fe4b4a --- /dev/null +++ b/comms/db/migrations/20221210021301_create_chat_permissions.sql @@ -0,0 +1,9 @@ +-- migrate:up + +create table if not exists chat_permissions ( + user_id int primary key, + permits text default 'all' +); + +-- migrate:down +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 new file mode 100644 index 00000000000..43b3898cfdc --- /dev/null +++ b/comms/db/migrations/20221212074228_create_chat_blocked_users.sql @@ -0,0 +1,11 @@ +-- migrate:up +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, + + primary key (blocker_user_id, blockee_user_id) +); + +-- migrate:down +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 new file mode 100644 index 00000000000..43ac0d6965d --- /dev/null +++ b/comms/db/migrations/20221212081249_create_chat_message_reactions.sql @@ -0,0 +1,12 @@ +-- migrate:up +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, + 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 if exists chat_message_reactions cascade; 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/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..37592513114 --- /dev/null +++ b/comms/db/queries/get_chat_messages.go @@ -0,0 +1,130 @@ +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, chat_message.message_id +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 Reactions `json:"reactions"` +} + +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 *Reactions) Scan(value interface{}) error { + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + + 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, + arg.UserID, + arg.ChatID, + arg.Limit, + arg.Cursor, + ) + 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 new file mode 100644 index 00000000000..2c258afc892 --- /dev/null +++ b/comms/db/queries/get_chats.go @@ -0,0 +1,99 @@ +package queries + +import ( + "context" + "database/sql" + "time" + + "comms.audius.co/db" + "github.com/jmoiron/sqlx" +) + +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) +ORDER BY chat.last_message_at DESC, chat.chat_id +` + +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 +} + +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/db/queries/get_summaries.go b/comms/db/queries/get_summaries.go new file mode 100644 index 00000000000..12fa7ef4dcb --- /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 `db:"total_count" json:"total_count"` + RemainingCount int64 `db:"remaining_count" 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..1055220e41d --- /dev/null +++ b/comms/internal/pubkeystore/recover.go @@ -0,0 +1,179 @@ +package pubkeystore + +import ( + "context" + "errors" + "fmt" + "math/big" + "strings" + + "comms.audius.co/config" + "comms.audius.co/db" + "comms.audius.co/jetstream" + "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 + + jsc := jetstream.GetJetstreamContext() + kv, err := jsc.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..3766ec3a863 --- /dev/null +++ b/comms/internal/rpcz/apply.go @@ -0,0 +1,173 @@ +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 + applies 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() + + 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 + } + 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: + 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..331fd9ada21 --- /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 table chat_blocked_users cascade") + assert.NoError(t, err) + _, err = db.Conn.Exec("truncate table 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 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 chat with a user you have blocked or user who has blocked you") + } + + // user 91 unblocks 92 + err = chatUnblock(tx, 91, 92) + assert.NoError(t, err) + assertBlocked(91, 92, messageTs, 0) + + tx.Rollback() +} diff --git a/comms/internal/rpcz/chat_delete_test.go b/comms/internal/rpcz/chat_delete_test.go new file mode 100644 index 00000000000..bb8d70bed68 --- /dev/null +++ b/comms/internal/rpcz/chat_delete_test.go @@ -0,0 +1,63 @@ +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 table chat cascade") + assert.NoError(t, err) + + tx := db.Conn.MustBegin() + + 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.Rollback() +} diff --git a/comms/internal/rpcz/chat_permissions_test.go b/comms/internal/rpcz/chat_permissions_test.go new file mode 100644 index 00000000000..e6763c6ab91 --- /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 table 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.Rollback() +} diff --git a/comms/internal/rpcz/chat_test.go b/comms/internal/rpcz/chat_test.go new file mode 100644 index 00000000000..9005fcebff8 --- /dev/null +++ b/comms/internal/rpcz/chat_test.go @@ -0,0 +1,132 @@ +package rpcz + +import ( + "database/sql" + "fmt" + "testing" + "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" +) + +func TestChat(t *testing.T) { + var err error + + chatId := "chat1" + + // reset tables under test + _, err = db.Conn.Exec("truncate table chat cascade") + assert.NoError(t, err) + + tx := db.Conn.MustBegin() + + 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 + { + exampleRpc := schema.RawRPC{ + Params: []byte(fmt.Sprintf(`{"chat_id": "%s", "message": "test123"}`, chatId)), + } + + chatMessage := string(schema.RPCMethodChatMessage) + + err = Validators[chatMessage](tx, 91, exampleRpc) + assert.NoError(t, err) + + err = Validators[chatMessage](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.Rollback() +} diff --git a/comms/internal/rpcz/main_test.go b/comms/internal/rpcz/main_test.go new file mode 100644 index 00000000000..71e396c6159 --- /dev/null +++ b/comms/internal/rpcz/main_test.go @@ -0,0 +1,27 @@ +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 + db.Conn.Close() + + os.Exit(code) +} diff --git a/comms/internal/rpcz/rate_limit_test.go b/comms/internal/rpcz/rate_limit_test.go new file mode 100644 index 00000000000..1b09742799a --- /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 table 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 new file mode 100644 index 00000000000..3eff010eff2 --- /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().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, user1, user2) + assert.NoError(t, err) +} diff --git a/comms/internal/rpcz/validator.go b/comms/internal/rpcz/validator.go new file mode 100644 index 00000000000..303a2ed69db --- /dev/null +++ b/comms/internal/rpcz/validator.go @@ -0,0 +1,379 @@ +package rpcz + +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 + +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 + } + err = validateNotBlocked(q, int32(user1), int32(user2)) + if err != nil { + return err + } + } + + // 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 { + 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 { + err = validateNotBlocked(q, chatMembers[0].UserID, chatMembers[1].UserID) + if err != nil { + return err + } + } + + // validate does not exceed new message rate limit + err = validateNewMessageRateLimit(q, userId, params.ChatID) + if err != nil { + return err + } + + 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 +} + +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/main.go b/comms/main.go new file mode 100644 index 00000000000..80993fd6181 --- /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", "--no-dump-schema", "--url", os.Getenv("audius_db_url"), "up").CombinedOutput() + if err != nil { + log.Fatalf("dbmate: %s %s \n", err, out) + } + 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..fbc608b3bd9 --- /dev/null +++ b/comms/peering/nats.go @@ -0,0 +1,241 @@ +package peering + +import ( + "errors" + "fmt" + "log" + "net/url" + "sync" + "time" + + "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" +) + +var NatsClient *nats.Conn + +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, "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 via the jetstream package + // the server checks if this is non-nil to know if it's ready + jetstream.SetJetstreamContext(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..ac240304f6d --- /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/main_test.go b/comms/server/main_test.go new file mode 100644 index 00000000000..7e180bf53aa --- /dev/null +++ b/comms/server/main_test.go @@ -0,0 +1,27 @@ +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 + db.Conn.Close() + + os.Exit(code) +} diff --git a/comms/server/response_mapper.go b/comms/server/response_mapper.go new file mode 100644 index 00000000000..7966e0a9da0 --- /dev/null +++ b/comms/server/response_mapper.go @@ -0,0 +1,77 @@ +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, + 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) + } + 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.Reactions) []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..0f7d6bb7c95 --- /dev/null +++ b/comms/server/server.go @@ -0,0 +1,374 @@ +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/jetstream" + "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 Start() { + e := createServer() + e.Logger.Fatal(e.Start(":8925")) +} + +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 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 + 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.JSON(200, map[string]interface{}{ + "data": 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" + + jsc := jetstream.GetJetstreamContext() + if jsc == 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 := jsc.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", getChats) + + g.GET("/chats/:id", getChat) + + g.GET("/chats/:id/messages", getMessages) + + // 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" + jsc := jetstream.GetJetstreamContext() + if jsc == 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 := jsc.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 +} diff --git a/comms/server/server_test.go b/comms/server/server_test.go new file mode 100644 index 00000000000..6d253c74f94 --- /dev/null +++ b/comms/server/server_test.go @@ -0,0 +1,364 @@ +package server + +import ( + "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" +) + +// 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) + 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 table chat cascade") + assert.NoError(t, err) + _, err = db.Conn.Exec("truncate table 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) + err = tx.Commit() + assert.NoError(t, err) + + // 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) + assert.JSONEq(t, string(expectedResponse), rec.Body.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 + expectedResponse, err := json.Marshal( + schema.CommsResponse{ + Health: expectedHealth, + Data: expectedChat1Data, + }, + ) + assert.NoError(t, err) + if assert.NoError(t, getChat(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.JSONEq(t, string(expectedResponse), rec.Body.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 table chat cascade") + assert.NoError(t, err) + _, err = db.Conn.Exec("truncate table 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) + + err = tx.Commit() + assert.NoError(t, err) + + // 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) + assert.JSONEq(t, string(expectedResponse), rec.Body.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 +} 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