diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..863258e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: Start pinspire CI + +on: + workflow_dispatch: {} + push: {} + pull_request: + types: [opened, edited, reopened] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Get repository code + uses: actions/checkout@v4 + - name: Test application + continue-on-error: true + run: go test ./... + lint: + runs-on: ubuntu-latest + steps: + - name: Get repository code + uses: actions/checkout@v4 + - name: Lint application + run: make lint + build: + runs-on: ubuntu-latest + steps: + - name: Get repository code + uses: actions/checkout@v4 + - name: Build application + run: make build_all diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml new file mode 100644 index 0000000..b2fda69 --- /dev/null +++ b/.github/workflows/deployment.yml @@ -0,0 +1,58 @@ +name: Start Pinspire deployment + +on: + workflow_dispatch: {} + push: + branches: + - main + - dev4 + +jobs: + build_images: + runs-on: ubuntu-latest + steps: + - name: get repository code + uses: actions/checkout@v4 + - name: Login to DockerHub Registry + run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + - name: Build docker images of services + run: | + docker build -t pinspireapp/main:latest -f deployments/Dockerfile.main . & + docker build -t pinspireapp/auth:latest -f deployments/Dockerfile.auth . & + docker build -t pinspireapp/realtime:latest -f deployments/Dockerfile.realtime . & + docker build -t pinspireapp/messenger:latest -f deployments/Dockerfile.messenger . & + for p in $(jobs -p); do wait "$p" || { echo "job $p failed" >&2; exit; }; done + - name: Push docker images + run: | + docker push pinspireapp/main:latest & + docker push pinspireapp/auth:latest & + docker push pinspireapp/realtime:latest & + docker push pinspireapp/messenger:latest & + for p in $(jobs -p); do wait "$p" || { echo "job $p failed" >&2; exit; }; done + + deploy: + runs-on: ubuntu-latest + needs: build_images + steps: + - name: fetch changes + uses: appleboy/ssh-action@master + with: + host: pinspire.site + username: ${{ secrets.REMOTE_USERNAME }} + key: ${{ secrets.PRIVATE_KEY }} + script: | + cd ${{ secrets.PINSPIRE_BACKEND_PATH }} + sudo git switch dev4 + sudo git pull + - name: deploy application + uses: appleboy/ssh-action@master + with: + host: pinspire.site + username: ${{ secrets.REMOTE_USERNAME }} + key: ${{ secrets.PRIVATE_KEY }} + script: | + cd ${{ secrets.PINSPIRE_BACKEND_PATH }}/deployments + sudo docker compose down main_service auth_service realtime_service messenger_service + sudo docker rmi pinspireapp/main:latest pinspireapp/auth:latest pinspireapp/realtime:latest pinspireapp/messenger:latest + sudo docker compose -f docker-compose.yml -f compose.prod.yml up -d + diff --git a/.gitignore b/.gitignore index 1d94894..ad9c3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ testdata/ cert/ .env redis.conf +inventory +keyVision.json +script* \ No newline at end of file diff --git a/Makefile b/Makefile index 75dc439..f054d18 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,31 @@ .PHONY: build run test test_with_coverage cleantest retest doc generate cover_all currcover +.PHONY: build_auth build_realtime build_messenger build_all +.PHONY: .install-linter lint lint-fast + ENTRYPOINT=cmd/app/main.go DOC_DIR=./docs COV_OUT=coverage.out COV_HTML=coverage.html CURRCOVER=github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1 +PROJECT_BIN = $(CURDIR)/bin +$(shell [ -f bin ] || mkdir -p $(PROJECT_BIN)) +GOLANGCI_LINT = $(PROJECT_BIN)/golangci-lint build: go build -o bin/app cmd/app/*.go +build_auth: + go build -o bin/auth cmd/auth/*.go + +build_realtime: + go build -o bin/realtime cmd/realtime/*.go + +build_messenger: + go build -o bin/messenger cmd/messenger/*.go + +build_all: build build_auth build_realtime build_messenger + run: build ./bin/app @@ -40,3 +57,12 @@ cover_all: currcover: go test -cover -v -coverprofile=cover.out ${CURRCOVER} go tool cover -html=cover.out -o cover.html + +.install-linter: + [ -f $(PROJECT_BIN)/golangci-lint ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(PROJECT_BIN) v1.55.2 + +lint: .install-linter + $(GOLANGCI_LINT) run ./... --config=configs/.golangci.yml + +lint-fast: .install-linter + $(GOLANGCI_LINT) run ./... --fast --config=configs/.golangci.yml diff --git a/api/proto/auth.proto b/api/proto/auth.proto new file mode 100644 index 0000000..b819a5c --- /dev/null +++ b/api/proto/auth.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/auth"; + +package auth; + +service Auth { + rpc Register(RegisterData) returns (google.protobuf.Empty) {} + rpc Login(Credentials) returns (Session) {} + rpc Logout(Session) returns (google.protobuf.Empty) {} + rpc GetUserID(Session) returns (UserID) {} +} + +message Credentials { + string password = 1; + string username = 2; +} + +message RegisterData { + Credentials cred = 1; + string email = 2; +} + +message User { + int64 id = 1; + string username = 2; + string avatar = 3; +} + +message Session { + string key = 1; + int64 userID = 2; + google.protobuf.Timestamp expire = 3; +} + +message UserID { + int64 id = 1; +} \ No newline at end of file diff --git a/api/proto/messenger.proto b/api/proto/messenger.proto new file mode 100644 index 0000000..07a0dec --- /dev/null +++ b/api/proto/messenger.proto @@ -0,0 +1,64 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; + +option go_package = "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger"; + +package messenger; + +service Messenger { + rpc UserChatsWithOtherUsers(FeedChatRequest) returns (FeedChat) {} + rpc SendMessage(Message) returns (MsgID) {} + rpc MessageFromChat(FeedMessageRequest) returns (FeedMessage) {} + rpc UpdateMessage(Message) returns (google.protobuf.Empty) {} + rpc DeleteMessage(MsgID) returns (google.protobuf.Empty) {} + rpc GetMessage(MsgID) returns (Message) {} +} + +message MsgID { + int64 id = 1; +} + +message Message { + MsgID id = 1; + int64 user_from = 2; + int64 user_to = 3; + string content = 4; +} + +message Chat { + int64 userID1 = 1; + int64 userID2 = 2; +} + +message FeedChatRequest { + int64 count = 1; + int64 lastID = 2; +} + +message FeedMessageRequest { + Chat chat = 1; + int64 count = 2; + int64 lastID = 3; +} + +message WichWhomChat { + int64 userID = 1; + string username = 2; + string avatar = 3; +} + +message ChatWithUser { + int64 lastMessageID = 1; + WichWhomChat chat = 2; +} + +message FeedMessage { + repeated Message messages = 1; + int64 lastID = 2; +} + +message FeedChat { + repeated ChatWithUser chats = 1; + int64 lastID = 2; +} diff --git a/api/proto/realtime.proto b/api/proto/realtime.proto new file mode 100644 index 0000000..3fca899 --- /dev/null +++ b/api/proto/realtime.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; + +option go_package = "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime"; + +package realtime; + +service RealTime { + rpc Publish(PublishMessage) returns (google.protobuf.Empty) {} + rpc Subscribe(Channels) returns (stream Message) {} +} + +message Channels { + repeated Channel chans = 1; +} + +message Channel { + string topic = 1; + string name = 2; +} + +enum EventType { + EV_CREATE = 0; + EV_DELETE = 1; + EV_UPDATE = 2; +} + +message EventObject { + int64 id = 1; + EventType type = 2; +} + +message EventMap { + int64 type = 1; + map m = 2; +} + +message Message { + oneof body { + EventObject object = 1; + EventMap content = 2; + } +} + +message PublishMessage { + Channel channel = 1; + Message message = 2; +} diff --git a/cmd/app/config.go b/cmd/app/config.go deleted file mode 100644 index a59b192..0000000 --- a/cmd/app/config.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -import "github.com/go-park-mail-ru/2023_2_OND_team/internal/app" - -var configFiles = app.ConfigFiles{ - ServerConfigFile: "configs/config.yml", - RedisConfigFile: "redis.conf", -} diff --git a/cmd/app/main.go b/cmd/app/main.go index 3d4dbb6..c571572 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -2,10 +2,18 @@ package main import ( "context" + "flag" "fmt" + "os" "github.com/go-park-mail-ru/2023_2_OND_team/internal/app" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" + "github.com/joho/godotenv" +) + +var ( + logOutput = flag.String("log", "stdout", "file paths to write logging output to") + logErrorOutput = flag.String("logerror", "stderr", "path to write internal logger errors to.") ) // @title Pinspire API @@ -21,15 +29,25 @@ import ( // @license.url http://www.apache.org/licenses/LICENSE-2.0.html func main() { + godotenv.Load() + flag.Parse() ctxBase, cancel := context.WithCancel(context.Background()) defer cancel() - log, err := logger.New(logger.RFC3339FormatTime()) + log, err := logger.New( + logger.RFC3339FormatTime(), + logger.SetOutputPaths(*logOutput), + logger.SetErrorOutputPaths(*logErrorOutput), + ) if err != nil { fmt.Println(err) return } defer log.Sync() + configFiles := app.ConfigFiles{ + ServerConfigFile: "configs/config.yml", + AddrAuthServer: os.Getenv("AUTH_SERVICE_HOST") + ":" + os.Getenv("AUTH_SERVICE_PORT"), // "localhost:8085", + } app.Run(ctxBase, log, configFiles) } diff --git a/cmd/auth/config.go b/cmd/auth/config.go new file mode 100644 index 0000000..2ffd53a --- /dev/null +++ b/cmd/auth/config.go @@ -0,0 +1,8 @@ +package main + +import "github.com/go-park-mail-ru/2023_2_OND_team/internal/app/auth" + +var configAuth = auth.Config{ + Addr: "0.0.0.0:8085", + RedisFileConfig: "redis.conf", +} diff --git a/cmd/auth/main.go b/cmd/auth/main.go new file mode 100644 index 0000000..91b055b --- /dev/null +++ b/cmd/auth/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + "fmt" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/app/auth" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +func main() { + log, err := logger.New(logger.RFC3339FormatTime()) + if err != nil { + fmt.Println(err) + return + } + defer log.Sync() + + auth.Run(context.Background(), log, configAuth) +} diff --git a/cmd/messenger/main.go b/cmd/messenger/main.go new file mode 100644 index 0000000..a9dd91a --- /dev/null +++ b/cmd/messenger/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + "fmt" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/app/messenger" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +func main() { + log, err := logger.New() + if err != nil { + fmt.Println(err) + return + } + defer log.Sync() + + messenger.Run(context.Background(), log) +} diff --git a/cmd/realtime/main.go b/cmd/realtime/main.go new file mode 100644 index 0000000..a01f79f --- /dev/null +++ b/cmd/realtime/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "net" + + "google.golang.org/grpc" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/microservices/realtime" + grpcMetrics "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/metrics/grpc" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/grpc/interceptor" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" + "github.com/joho/godotenv" +) + +const _address = "0.0.0.0:8090" + +func main() { + godotenv.Load() + log, err := logger.New() + if err != nil { + fmt.Println(err) + return + } + if err := RealTimeRun(log, _address); err != nil { + log.Error(err.Error()) + } +} + +func RealTimeRun(log *logger.Logger, addr string) error { + l, err := net.Listen("tcp", addr) + if err != nil { + return fmt.Errorf("listen tcp %s: %w", addr, err) + } + + metrics := grpcMetrics.New("realtime") + if err := metrics.Registry(); err != nil { + log.Error(err.Error()) + return err + } + + node, err := realtime.NewNode() + if err != nil { + return fmt.Errorf("new server node: %w", err) + } + + serv := grpc.NewServer(grpc.ChainUnaryInterceptor( + interceptor.Monitoring(metrics, "0.0.0.0:8091"), + interceptor.Logger(log), + )) + rt.RegisterRealTimeServer(serv, realtime.NewServer(node)) + + log.Info("start realtime server", logger.F{"network", "tcp"}, logger.F{"addr", addr}) + return serv.Serve(l) +} diff --git a/configs/.golangci.yml b/configs/.golangci.yml new file mode 100644 index 0000000..52e544b --- /dev/null +++ b/configs/.golangci.yml @@ -0,0 +1,206 @@ +# This code is licensed under the terms of the MIT license https://opensource.org/license/mit +# Copyright (c) 2021 Marat Reymers + +## Golden config for golangci-lint v1.55.2 +# +# This is the best config for golangci-lint based on my experience and opinion. +# It is very strict, but not extremely strict. +# Feel free to adapt and change it for your needs. + +run: + # Timeout for analysis, e.g. 30s, 5m. + # Default: 1m + timeout: 3m + skip-dirs: + .. + + + +# This file contains only configs which differ from defaults. +# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml +linters-settings: + cyclop: + # The maximal code complexity to report. + # Default: 10 + max-complexity: 30 + # The maximal average package complexity. + # If it's higher than 0.0 (float) the check is enabled + # Default: 0.0 + package-average: 10.0 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Such cases aren't reported by default. + # Default: false + check-type-assertions: true + + exhaustive: + # Program elements to check for exhaustiveness. + # Default: [ switch ] + check: + - switch + - map + + exhaustruct: + # List of regular expressions to exclude struct packages and names from check. + # Default: [] + exclude: + # std libs + - "^net/http.Client$" + - "^net/http.Cookie$" + - "^net/http.Request$" + - "^net/http.Response$" + - "^net/http.Server$" + - "^net/http.Transport$" + - "^net/url.URL$" + - "^os/exec.Cmd$" + - "^reflect.StructField$" + # public libs + - "^github.com/Shopify/sarama.Config$" + - "^github.com/Shopify/sarama.ProducerMessage$" + - "^github.com/mitchellh/mapstructure.DecoderConfig$" + - "^github.com/prometheus/client_golang/.+Opts$" + - "^github.com/spf13/cobra.Command$" + - "^github.com/spf13/cobra.CompletionOptions$" + - "^github.com/stretchr/testify/mock.Mock$" + - "^github.com/testcontainers/testcontainers-go.+Request$" + - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" + - "^golang.org/x/tools/go/analysis.Analyzer$" + - "^google.golang.org/protobuf/.+Options$" + - "^gopkg.in/yaml.v3.Node$" + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 100 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 + # Ignore comments when counting lines. + # Default false + ignore-comments: true + + gocognit: + # Minimal code complexity to report. + # Default: 30 (but we recommend 10-20) + min-complexity: 20 + + gocritic: + # Settings passed to gocritic. + # The settings key is the name of a supported gocritic checker. + # The list of supported checkers can be find in https://go-critic.github.io/overview. + settings: + captLocal: + # Whether to restrict checker to params only. + # Default: true + paramsOnly: false + underef: + # Whether to skip (*x).method() calls where x is a pointer receiver. + # Default: true + skipRecvDeref: false + + gomnd: + # List of function patterns to exclude from analysis. + # Values always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: + - flag.Arg + - flag.Duration.* + - flag.Float.* + - flag.Int.* + - flag.Uint.* + - os.Chmod + - os.Mkdir.* + - os.OpenFile + - os.WriteFile + - prometheus.ExponentialBuckets.* + - prometheus.LinearBuckets + + gomodguard: + blocked: + # List of blocked modules. + # Default: [] + modules: + - github.com/golang/protobuf: + recommendations: + - google.golang.org/protobuf + reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" + - github.com/satori/go.uuid: + recommendations: + - github.com/google/uuid + reason: "satori's package is not maintained" + - github.com/gofrs/uuid: + recommendations: + - github.com/google/uuid + reason: "gofrs' package is not go module" + + govet: + # Enable all analyzers. + # Default: false + enable-all: true + # Disable analyzers by name. + # Run `go tool vet help` to see all analyzers. + # Default: [] + disable: + - fieldalignment # too strict + # Settings per analyzer. + settings: + shadow: + # Whether to be strict about shadowing; can be noisy. + # Default: false + strict: true + + nakedret: + # Make an issue if func has more lines of code than this setting, and it has naked returns. + # Default: 30 + max-func-lines: 0 + + nolintlint: + # Exclude following linters from requiring an explanation. + # Default: [] + allow-no-explanation: [ funlen, gocognit, lll ] + # Enable to require an explanation of nonzero length after each nolint directive. + # Default: false + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. + # Default: false + require-specific: true + + rowserrcheck: + # database/sql is always checked + # Default: [] + packages: + - github.com/jmoiron/sqlx + + tenv: + # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. + # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. + # Default: false + all: true + + +issues: + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + exclude-use-default: true + max-same-issues: 50 + + exclude-rules: + - source: "(noinspection|TODO)" + linters: [ godot ] + - source: "//noinspection" + linters: [ gocritic ] + - path: "_test\\.go" + linters: + - bodyclose + - dupl + - funlen + - goconst + - gosec + - noctx + - wrapcheck \ No newline at end of file diff --git a/configs/config.yml b/configs/config.yml index 4d1fda6..87b2acd 100644 --- a/configs/config.yml +++ b/configs/config.yml @@ -2,6 +2,6 @@ app: server: host: 0.0.0.0 port: 8080 - https: false + https: true certFile: /home/ond_team/cert/fullchain.pem keyFile: /home/ond_team/cert/privkey.pem diff --git a/configs/playbook.yml b/configs/playbook.yml new file mode 100644 index 0000000..2e134dc --- /dev/null +++ b/configs/playbook.yml @@ -0,0 +1,18 @@ +- name: "Provide configuration files" + become: yes + hosts: pinspire + tasks: + - name: "Provide .env file" + copy: + src: ../.env + dest: /home/ond_team/go/src/github.com/go-park-mail-ru/{{ item }}/.env + with_items: + - ci-cd + - 2023_2_OND_team + - name: "Provide redis config" + copy: + src: ../redis.conf + dest: /home/ond_team/go/src/github.com/go-park-mail-ru/{{ item }}/redis.conf + with_items: + - ci-cd + - 2023_2_OND_team diff --git a/configs/prometheus.yml b/configs/prometheus.yml new file mode 100644 index 0000000..a65d7b8 --- /dev/null +++ b/configs/prometheus.yml @@ -0,0 +1,29 @@ +global: + scrape_interval: 10s + evaluation_interval: 10s + +scrape_configs: + - job_name: 'api' + static_configs: + - targets: ['main_service:8079'] + + - job_name: 'auth' + static_configs: + - targets: ['auth_service:8086'] + + - job_name: 'messenger' + static_configs: + - targets: ['messenger_service:8096'] + + - job_name: 'realtime' + static_configs: + - targets: ['realtime_service:8091'] + + - job_name: 'node_exporter' + static_configs: + - targets: ['node_exporter:9100'] + + - job_name: 'pinspire' + scheme: https + static_configs: + - targets: ['pinspire.online:8080'] diff --git a/deployments/Dockerfile.auth b/deployments/Dockerfile.auth new file mode 100644 index 0000000..74b9443 --- /dev/null +++ b/deployments/Dockerfile.auth @@ -0,0 +1,20 @@ +FROM golang:1.19.13-alpine AS build + +RUN apk --no-cache add make + +WORKDIR /pinspire + +COPY go.mod go.sum /pinspire/ +RUN go mod download + +COPY . . + +RUN make build_auth + +FROM alpine:latest + +WORKDIR / + +COPY --from=build /pinspire/bin/auth . + +ENTRYPOINT [ "./auth" ] diff --git a/deployments/Dockerfile.main b/deployments/Dockerfile.main new file mode 100644 index 0000000..4d500c8 --- /dev/null +++ b/deployments/Dockerfile.main @@ -0,0 +1,21 @@ +FROM golang:1.19.13-alpine AS build + +RUN apk --no-cache add make + +WORKDIR /pinspire + +COPY go.mod go.sum /pinspire/ +RUN go mod download + +COPY . . + +RUN make build + +FROM alpine:latest + +WORKDIR / + +COPY --from=build /pinspire/bin/app . +COPY --from=build /pinspire/configs configs + +ENTRYPOINT [ "./app" ] diff --git a/deployments/Dockerfile.messenger b/deployments/Dockerfile.messenger new file mode 100644 index 0000000..cdaec41 --- /dev/null +++ b/deployments/Dockerfile.messenger @@ -0,0 +1,20 @@ +FROM golang:1.19.13-alpine AS build + +RUN apk --no-cache add make + +WORKDIR /pinspire + +COPY go.mod go.sum /pinspire/ +RUN go mod download + +COPY . . + +RUN make build_messenger + +FROM alpine:latest + +WORKDIR / + +COPY --from=build /pinspire/bin/messenger . + +ENTRYPOINT [ "./messenger" ] diff --git a/deployments/Dockerfile.realtime b/deployments/Dockerfile.realtime new file mode 100644 index 0000000..da5610f --- /dev/null +++ b/deployments/Dockerfile.realtime @@ -0,0 +1,20 @@ +FROM golang:1.19.13-alpine AS build + +RUN apk --no-cache add make + +WORKDIR /pinspire + +COPY go.mod go.sum /pinspire/ +RUN go mod download + +COPY . . + +RUN make build_realtime + +FROM alpine:latest + +WORKDIR / + +COPY --from=build /pinspire/bin/realtime . + +ENTRYPOINT [ "./realtime" ] diff --git a/deployments/compose.prod.yml b/deployments/compose.prod.yml new file mode 100644 index 0000000..5156d5d --- /dev/null +++ b/deployments/compose.prod.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + main_service: + image: pinspireapp/main:latest + + auth_service: + image: pinspireapp/auth:latest + + messenger_service: + image: pinspireapp/messenger:latest + + realtime_service: + image: pinspireapp/realtime:latest diff --git a/deployments/docker-compose.yml b/deployments/docker-compose.yml index ba817e0..a06c4cc 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -8,14 +8,194 @@ services: - ../.env volumes: - ../db/migrations:/docker-entrypoint-initdb.d + - 'postgres_storage:/var/lib/postgresql/data' ports: - 5432:5432 - + healthcheck: + test: ["CMD", "pg_isready"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 15s + redis: image: redis:latest container_name: pinspireRedis volumes: - ../redis.conf:/usr/local/etc/redis/redis.conf + - 'redis_storage:/data' command: redis-server /usr/local/etc/redis/redis.conf ports: - 6379:6379 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 15s + + main_service: + build: + context: ./.. + dockerfile: deployments/Dockerfile.main + container_name: pinspireMainService + env_file: + - ../.env + environment: + - POSTGRES_HOST=postgres + - AUTH_SERVICE_HOST=auth_service + - MESSENGER_SERVICE_HOST=messenger_service + - REALTIME_SERVICE_HOST=realtime_service + volumes: + - '/home/ond_team/cert/fullchain.pem:/home/ond_team/cert/fullchain.pem:ro' + - '/home/ond_team/cert/privkey.pem:/home/ond_team/cert/privkey.pem:ro' + - '/home/ond_team/go/src/github.com/go-park-mail-ru/ci-cd/upload:/upload' + depends_on: + postgres: + condition: 'service_healthy' + auth_service: + condition: 'service_started' + messenger_service: + condition: 'service_started' + realtime_service: + condition: 'service_started' + ports: + - 8079:8080 + + auth_service: + build: + context: ./.. + dockerfile: deployments/Dockerfile.auth + container_name: pinspireAuthService + env_file: + - ../.env + environment: + - POSTGRES_HOST=postgres + - REDIS_HOST=redis + depends_on: + postgres: + condition: 'service_healthy' + redis: + condition: 'service_healthy' + ports: + - 8186:8086 + # - 8101:8085 + + messenger_service: + build: + context: ./.. + dockerfile: deployments/Dockerfile.messenger + container_name: pinspireMessengerService + env_file: + - ../.env + environment: + - POSTGRES_HOST=postgres + depends_on: + postgres: + condition: 'service_healthy' + ports: + - 8196:8096 + # - 8102:8095 + + realtime_service: + build: + context: ./.. + dockerfile: deployments/Dockerfile.realtime + container_name: pinspireRealtimeService + env_file: + - ../.env + environment: + - KAFKA_BROKER_ADDRESS=kafka + - KAFKA_BROKER_PORT=29092 + depends_on: + kafka: + condition: 'service_healthy' + ports: + - 8191:8091 + # - 8103:8090 + + zookeeper: + image: bitnami/zookeeper:latest + container_name: pinspireZookeeper + ports: + - "2181:2181" + volumes: + - "zookeeper_data:/bitnami" + environment: + - ALLOW_ANONYMOUS_LOGIN=yes + healthcheck: + test: | + curl localhost:2181 + [ $(echo $?) = '52' ] && exit 0 || exit -1 + interval: 5s + timeout: 5s + retries: 10 + start_period: 15s + + kafka: + image: bitnami/kafka:latest + container_name: pinspireKafka + ports: + - "9092:9092" + volumes: + - "kafka_data:/bitnami" + environment: + - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 + - ALLOW_PLAINTEXT_LISTENER=yes + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT + - KAFKA_CFG_LISTENERS=CLIENT://:29092,EXTERNAL://:9092 + - KAFKA_CFG_ADVERTISED_LISTENERS=CLIENT://kafka:29092,EXTERNAL://localhost:9092 + - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=CLIENT + healthcheck: + test: | + curl localhost:9092 + [ $(echo $?) = '52' ] && exit 0 || exit -1 + interval: 5s + timeout: 5s + retries: 10 + start_period: 15s + depends_on: + zookeeper: + condition: 'service_healthy' + + prometheus: + image: prom/prometheus:latest + container_name: pinspirePrometheus + ports: + - "9090:9090" + volumes: + - "../configs/prometheus.yml:/etc/prometheus/prometheus.yml" + + grafana: + image: grafana/grafana:latest + container_name: pinspireGrafana + env_file: + - ../.env + ports: + - 3000:3000 + volumes: + - 'grafana_storage:/var/lib/grafana' + + node_exporter: + image: quay.io/prometheus/node-exporter:latest + container_name: pinspireNodeExporter + user: root + privileged: true + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + ports: + - "9100:9100" + +volumes: + postgres_storage: {} + redis_storage: {} + zookeeper_data: + driver: local + kafka_data: + driver: local + grafana_storage: {} diff --git a/go.mod b/go.mod index 1919958..64ddf05 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,21 @@ module github.com/go-park-mail-ru/2023_2_OND_team go 1.19 require ( + cloud.google.com/go/vision v1.2.0 + cloud.google.com/go/vision/v2 v2.7.5 + github.com/IBM/sarama v1.42.1 github.com/Masterminds/squirrel v1.5.4 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/go-chi/chi/v5 v5.0.10 github.com/golang/mock v1.6.0 - github.com/google/uuid v1.3.1 + github.com/golang/protobuf v1.5.3 + github.com/google/uuid v1.4.0 github.com/jackc/pgx/v5 v5.4.3 github.com/joho/godotenv v1.5.1 + github.com/mailru/easyjson v0.7.7 github.com/microcosm-cc/bluemonday v1.0.26 github.com/pashagolub/pgxmock/v2 v2.12.0 + github.com/prometheus/client_golang v1.17.0 github.com/proullon/ramsql v0.0.1 github.com/redis/go-redis/v9 v9.2.1 github.com/rs/cors v1.10.1 @@ -23,18 +29,30 @@ require ( go.uber.org/zap v1.26.0 golang.org/x/crypto v0.14.0 golang.org/x/image v0.13.0 + google.golang.org/grpc v1.60.0 + google.golang.org/protobuf v1.31.0 + nhooyr.io/websocket v1.8.10 ) require ( + cloud.google.com/go v0.111.0 // indirect + cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/longrunning v0.5.4 // indirect github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/TwiN/go-away v1.6.12 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/benoitkugler/textlayout v0.3.0 // indirect github.com/benoitkugler/textprocessing v0.0.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dsnet/compress v0.0.1 // indirect + github.com/eapache/go-resiliency v1.4.0 // indirect + github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect + github.com/eapache/queue v1.1.0 // indirect github.com/go-fonts/latin-modern v0.3.1 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -42,26 +60,52 @@ require ( github.com/go-openapi/swag v0.22.4 // indirect github.com/go-text/typesetting v0.0.0-20231013144250-6cc35dbfae7d // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.16.7 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/tdewolff/minify/v2 v2.20.5 // indirect github.com/tdewolff/parse/v2 v2.7.3 // indirect + go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect + golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.14.0 // indirect + google.golang.org/api v0.149.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect star-tex.org/x/tex v0.4.0 // indirect diff --git a/go.sum b/go.sum index 9f5314b..2c71421 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,82 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= +cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/vision v1.2.0 h1:/CsSTkbmO9HC8iQpxbK8ATms3OQaX3YQUeTMGCxlaK4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.7.5 h1:T/ujUghvEaTb+YnFY/jiYwVAkMbIC8EieK0CJo6B4vg= +cloud.google.com/go/vision/v2 v2.7.5/go.mod h1:GcviprJLFfK9OLf0z8Gm6lQb6ZFUulvpZws+mm6yPLM= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v/9acCWA64Yz/HDLqjcRTvc0noQACi4MsJw= github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic= +github.com/IBM/sarama v1.42.1 h1:wugyWa15TDEHh2kvq2gAy1IHLjEjuYOYgXz/ruC/OSQ= +github.com/IBM/sarama v1.42.1/go.mod h1:Xxho9HkHd4K/MDUo/T/sOqwtX/17D33++E9Wib6hUdQ= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/TwiN/go-away v1.6.12 h1:80AjDyeTjfQaSFYbALzRcDKMAmxKW0a5PoxwXKZlW2A= +github.com/TwiN/go-away v1.6.12/go.mod h1:MpvIC9Li3minq+CGgbgUDvQ9tDaeW35k5IXZrF9MVas= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -17,11 +87,28 @@ github.com/benoitkugler/textlayout v0.3.0/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7 github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo= github.com/benoitkugler/textprocessing v0.0.3 h1:Q2X+Z6vxuW5Bxn1R9RaNt0qcprBfpc2hEUDeTlz90Ng= github.com/benoitkugler/textprocessing v0.0.3/go.mod h1:/4bLyCf1QYywunMK3Gf89Nhb50YI/9POewqrLxWhxd4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -32,14 +119,37 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/eapache/go-resiliency v1.4.0 h1:3OK9bWpPk5q6pbFAaYSEwD9CLUSHG8bnZuqX2yMt3B0= +github.com/eapache/go-resiliency v1.4.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= +github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-fonts/latin-modern v0.3.1 h1:/cT8A7uavYKvglYXvrdDw4oS5ZLkcOU22fa2HJ1/JVM= github.com/go-fonts/latin-modern v0.3.1/go.mod h1:ysEQXnuT/sCDOAONxC7ImeEDVINbltClhasMAqEtRK0= github.com/go-fonts/liberation v0.3.1 h1:9RPT2NhUpxQ7ukUvz3jeUckmN42T9D9TpjtQcqK/ceM= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gorp/gorp v2.0.0+incompatible h1:dIQPsBtl6/H1MjVseWuWPXa7ET4p6Dve4j3Hg+UjqYw= github.com/go-gorp/gorp v2.0.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= @@ -61,13 +171,108 @@ github.com/go-text/typesetting v0.0.0-20231013144250-6cc35dbfae7d/go.mod h1:evDB github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -76,12 +281,31 @@ github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -100,29 +324,50 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pashagolub/pgxmock/v2 v2.12.0 h1:IVRmQtVFNCoq7NOZ+PdfvB6fwnLJmEuWDhnc3yrDxBs= github.com/pashagolub/pgxmock/v2 v2.12.0/go.mod h1:D3YslkN/nJ4+umVqWmbwfSXugJIjPMChkGBG47OJpNw= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/proullon/ramsql v0.0.1 h1:tI7qN48Oj1LTmgdo4aWlvI9z45a4QlWaXlmdJ+IIfbU= github.com/proullon/ramsql v0.0.1/go.mod h1:jG8oAQG0ZPHPyxg5QlMERS31airDC+ZuqiAe8DUvFVo= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -145,7 +390,26 @@ github.com/tdewolff/parse/v2 v2.7.3/go.mod h1:9p2qMIHpjRSTr1qnFxQr+igogyTUTlwvf9 github.com/tdewolff/test v1.0.10/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52 h1:gAQliwn+zJrkjAHVcBEYW/RFvd2St4yYimisvozAYlA= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/config v1.4.0 h1:upnMPpMm6WlbZtXoasNkK4f0FhxwS+W4Iqz5oNznehQ= go.uber.org/config v1.4.0/go.mod h1:aCyrMHmUAc/s2h9sv1koP84M9ZF/4K+g2oleyESO/Ig= @@ -158,62 +422,427 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-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-20191104232314-dc038396d1f0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= +google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3 h1:kzJAXnzZoFbe5bhZd4zjUuHos/I31yH4thfMb/13oVY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k= +google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -221,6 +850,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= @@ -228,6 +858,17 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= +nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= star-tex.org/x/tex v0.4.0 h1:AXUwgpnHLCxZUWW3qrmjv6ezNhH3PjUVBuLLejz2cgU= star-tex.org/x/tex v0.4.0/go.mod h1:w91ycsU/DkkCr7GWr60GPWqp3gn2U+6VX71T0o8k8qE= diff --git a/internal/api/auth/auth.pb.go b/internal/api/auth/auth.pb.go new file mode 100644 index 0000000..24c7be4 --- /dev/null +++ b/internal/api/auth/auth.pb.go @@ -0,0 +1,483 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: api/proto/auth.proto + +package auth + +import ( + empty "github.com/golang/protobuf/ptypes/empty" + timestamp "github.com/golang/protobuf/ptypes/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Credentials struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` +} + +func (x *Credentials) Reset() { + *x = Credentials{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Credentials) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Credentials) ProtoMessage() {} + +func (x *Credentials) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_auth_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Credentials.ProtoReflect.Descriptor instead. +func (*Credentials) Descriptor() ([]byte, []int) { + return file_api_proto_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *Credentials) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *Credentials) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +type RegisterData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Cred *Credentials `protobuf:"bytes,1,opt,name=cred,proto3" json:"cred,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` +} + +func (x *RegisterData) Reset() { + *x = RegisterData{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_auth_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RegisterData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterData) ProtoMessage() {} + +func (x *RegisterData) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_auth_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterData.ProtoReflect.Descriptor instead. +func (*RegisterData) Descriptor() ([]byte, []int) { + return file_api_proto_auth_proto_rawDescGZIP(), []int{1} +} + +func (x *RegisterData) GetCred() *Credentials { + if x != nil { + return x.Cred + } + return nil +} + +func (x *RegisterData) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +type User struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Avatar string `protobuf:"bytes,3,opt,name=avatar,proto3" json:"avatar,omitempty"` +} + +func (x *User) Reset() { + *x = User{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_auth_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_auth_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_api_proto_auth_proto_rawDescGZIP(), []int{2} +} + +func (x *User) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetAvatar() string { + if x != nil { + return x.Avatar + } + return "" +} + +type Session struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + UserID int64 `protobuf:"varint,2,opt,name=userID,proto3" json:"userID,omitempty"` + Expire *timestamp.Timestamp `protobuf:"bytes,3,opt,name=expire,proto3" json:"expire,omitempty"` +} + +func (x *Session) Reset() { + *x = Session{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_auth_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Session) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Session) ProtoMessage() {} + +func (x *Session) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_auth_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Session.ProtoReflect.Descriptor instead. +func (*Session) Descriptor() ([]byte, []int) { + return file_api_proto_auth_proto_rawDescGZIP(), []int{3} +} + +func (x *Session) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Session) GetUserID() int64 { + if x != nil { + return x.UserID + } + return 0 +} + +func (x *Session) GetExpire() *timestamp.Timestamp { + if x != nil { + return x.Expire + } + return nil +} + +type UserID struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *UserID) Reset() { + *x = UserID{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_auth_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserID) ProtoMessage() {} + +func (x *UserID) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_auth_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserID.ProtoReflect.Descriptor instead. +func (*UserID) Descriptor() ([]byte, []int) { + return file_api_proto_auth_proto_rawDescGZIP(), []int{4} +} + +func (x *UserID) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +var File_api_proto_auth_proto protoreflect.FileDescriptor + +var file_api_proto_auth_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x61, 0x75, 0x74, 0x68, 0x1a, 0x1b, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, + 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x45, 0x0a, 0x0b, 0x43, 0x72, + 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x4b, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x44, 0x61, 0x74, + 0x61, 0x12, 0x25, 0x0a, 0x04, 0x63, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x73, 0x52, 0x04, 0x63, 0x72, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x4a, + 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x22, 0x67, 0x0a, 0x07, 0x53, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, + 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, + 0x32, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x22, 0x18, 0x0a, 0x06, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x32, 0xcc, 0x01, + 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x12, 0x38, 0x0a, 0x08, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x12, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x2b, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x1a, 0x0d, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x12, 0x31, 0x0a, + 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x0d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x53, + 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x2a, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, 0x0d, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x2e, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x0c, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x22, 0x00, 0x42, 0x35, 0x5a, 0x33, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x70, 0x61, + 0x72, 0x6b, 0x2d, 0x6d, 0x61, 0x69, 0x6c, 0x2d, 0x72, 0x75, 0x2f, 0x32, 0x30, 0x32, 0x33, 0x5f, + 0x32, 0x5f, 0x4f, 0x4e, 0x44, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, + 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_api_proto_auth_proto_rawDescOnce sync.Once + file_api_proto_auth_proto_rawDescData = file_api_proto_auth_proto_rawDesc +) + +func file_api_proto_auth_proto_rawDescGZIP() []byte { + file_api_proto_auth_proto_rawDescOnce.Do(func() { + file_api_proto_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_auth_proto_rawDescData) + }) + return file_api_proto_auth_proto_rawDescData +} + +var file_api_proto_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_api_proto_auth_proto_goTypes = []interface{}{ + (*Credentials)(nil), // 0: auth.Credentials + (*RegisterData)(nil), // 1: auth.RegisterData + (*User)(nil), // 2: auth.User + (*Session)(nil), // 3: auth.Session + (*UserID)(nil), // 4: auth.UserID + (*timestamp.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*empty.Empty)(nil), // 6: google.protobuf.Empty +} +var file_api_proto_auth_proto_depIdxs = []int32{ + 0, // 0: auth.RegisterData.cred:type_name -> auth.Credentials + 5, // 1: auth.Session.expire:type_name -> google.protobuf.Timestamp + 1, // 2: auth.Auth.Register:input_type -> auth.RegisterData + 0, // 3: auth.Auth.Login:input_type -> auth.Credentials + 3, // 4: auth.Auth.Logout:input_type -> auth.Session + 3, // 5: auth.Auth.GetUserID:input_type -> auth.Session + 6, // 6: auth.Auth.Register:output_type -> google.protobuf.Empty + 3, // 7: auth.Auth.Login:output_type -> auth.Session + 6, // 8: auth.Auth.Logout:output_type -> google.protobuf.Empty + 4, // 9: auth.Auth.GetUserID:output_type -> auth.UserID + 6, // [6:10] is the sub-list for method output_type + 2, // [2:6] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_api_proto_auth_proto_init() } +func file_api_proto_auth_proto_init() { + if File_api_proto_auth_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_api_proto_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Credentials); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RegisterData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*User); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Session); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_auth_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserID); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_api_proto_auth_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_proto_auth_proto_goTypes, + DependencyIndexes: file_api_proto_auth_proto_depIdxs, + MessageInfos: file_api_proto_auth_proto_msgTypes, + }.Build() + File_api_proto_auth_proto = out.File + file_api_proto_auth_proto_rawDesc = nil + file_api_proto_auth_proto_goTypes = nil + file_api_proto_auth_proto_depIdxs = nil +} diff --git a/internal/api/auth/auth_grpc.pb.go b/internal/api/auth/auth_grpc.pb.go new file mode 100644 index 0000000..bc4a5e7 --- /dev/null +++ b/internal/api/auth/auth_grpc.pb.go @@ -0,0 +1,214 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.6.1 +// source: api/proto/auth.proto + +package auth + +import ( + context "context" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// AuthClient is the client API for Auth service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AuthClient interface { + Register(ctx context.Context, in *RegisterData, opts ...grpc.CallOption) (*empty.Empty, error) + Login(ctx context.Context, in *Credentials, opts ...grpc.CallOption) (*Session, error) + Logout(ctx context.Context, in *Session, opts ...grpc.CallOption) (*empty.Empty, error) + GetUserID(ctx context.Context, in *Session, opts ...grpc.CallOption) (*UserID, error) +} + +type authClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthClient(cc grpc.ClientConnInterface) AuthClient { + return &authClient{cc} +} + +func (c *authClient) Register(ctx context.Context, in *RegisterData, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/auth.Auth/Register", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authClient) Login(ctx context.Context, in *Credentials, opts ...grpc.CallOption) (*Session, error) { + out := new(Session) + err := c.cc.Invoke(ctx, "/auth.Auth/Login", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authClient) Logout(ctx context.Context, in *Session, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/auth.Auth/Logout", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authClient) GetUserID(ctx context.Context, in *Session, opts ...grpc.CallOption) (*UserID, error) { + out := new(UserID) + err := c.cc.Invoke(ctx, "/auth.Auth/GetUserID", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServer is the server API for Auth service. +// All implementations must embed UnimplementedAuthServer +// for forward compatibility +type AuthServer interface { + Register(context.Context, *RegisterData) (*empty.Empty, error) + Login(context.Context, *Credentials) (*Session, error) + Logout(context.Context, *Session) (*empty.Empty, error) + GetUserID(context.Context, *Session) (*UserID, error) + mustEmbedUnimplementedAuthServer() +} + +// UnimplementedAuthServer must be embedded to have forward compatible implementations. +type UnimplementedAuthServer struct { +} + +func (UnimplementedAuthServer) Register(context.Context, *RegisterData) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Register not implemented") +} +func (UnimplementedAuthServer) Login(context.Context, *Credentials) (*Session, error) { + return nil, status.Errorf(codes.Unimplemented, "method Login not implemented") +} +func (UnimplementedAuthServer) Logout(context.Context, *Session) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") +} +func (UnimplementedAuthServer) GetUserID(context.Context, *Session) (*UserID, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUserID not implemented") +} +func (UnimplementedAuthServer) mustEmbedUnimplementedAuthServer() {} + +// UnsafeAuthServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthServer will +// result in compilation errors. +type UnsafeAuthServer interface { + mustEmbedUnimplementedAuthServer() +} + +func RegisterAuthServer(s grpc.ServiceRegistrar, srv AuthServer) { + s.RegisterService(&Auth_ServiceDesc, srv) +} + +func _Auth_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterData) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).Register(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/auth.Auth/Register", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).Register(ctx, req.(*RegisterData)) + } + return interceptor(ctx, in, info, handler) +} + +func _Auth_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Credentials) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).Login(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/auth.Auth/Login", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).Login(ctx, req.(*Credentials)) + } + return interceptor(ctx, in, info, handler) +} + +func _Auth_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Session) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).Logout(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/auth.Auth/Logout", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).Logout(ctx, req.(*Session)) + } + return interceptor(ctx, in, info, handler) +} + +func _Auth_GetUserID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Session) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServer).GetUserID(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/auth.Auth/GetUserID", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServer).GetUserID(ctx, req.(*Session)) + } + return interceptor(ctx, in, info, handler) +} + +// Auth_ServiceDesc is the grpc.ServiceDesc for Auth service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Auth_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "auth.Auth", + HandlerType: (*AuthServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Register", + Handler: _Auth_Register_Handler, + }, + { + MethodName: "Login", + Handler: _Auth_Login_Handler, + }, + { + MethodName: "Logout", + Handler: _Auth_Logout_Handler, + }, + { + MethodName: "GetUserID", + Handler: _Auth_GetUserID_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/proto/auth.proto", +} diff --git a/internal/api/messenger/messenger.pb.go b/internal/api/messenger/messenger.pb.go new file mode 100644 index 0000000..a9b7c6a --- /dev/null +++ b/internal/api/messenger/messenger.pb.go @@ -0,0 +1,812 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: api/proto/messenger.proto + +package messenger + +import ( + empty "github.com/golang/protobuf/ptypes/empty" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type MsgID struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *MsgID) Reset() { + *x = MsgID{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MsgID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MsgID) ProtoMessage() {} + +func (x *MsgID) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MsgID.ProtoReflect.Descriptor instead. +func (*MsgID) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{0} +} + +func (x *MsgID) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id *MsgID `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + UserFrom int64 `protobuf:"varint,2,opt,name=user_from,json=userFrom,proto3" json:"user_from,omitempty"` + UserTo int64 `protobuf:"varint,3,opt,name=user_to,json=userTo,proto3" json:"user_to,omitempty"` + Content string `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{1} +} + +func (x *Message) GetId() *MsgID { + if x != nil { + return x.Id + } + return nil +} + +func (x *Message) GetUserFrom() int64 { + if x != nil { + return x.UserFrom + } + return 0 +} + +func (x *Message) GetUserTo() int64 { + if x != nil { + return x.UserTo + } + return 0 +} + +func (x *Message) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type Chat struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserID1 int64 `protobuf:"varint,1,opt,name=userID1,proto3" json:"userID1,omitempty"` + UserID2 int64 `protobuf:"varint,2,opt,name=userID2,proto3" json:"userID2,omitempty"` +} + +func (x *Chat) Reset() { + *x = Chat{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Chat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Chat) ProtoMessage() {} + +func (x *Chat) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Chat.ProtoReflect.Descriptor instead. +func (*Chat) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{2} +} + +func (x *Chat) GetUserID1() int64 { + if x != nil { + return x.UserID1 + } + return 0 +} + +func (x *Chat) GetUserID2() int64 { + if x != nil { + return x.UserID2 + } + return 0 +} + +type FeedChatRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Count int64 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + LastID int64 `protobuf:"varint,2,opt,name=lastID,proto3" json:"lastID,omitempty"` +} + +func (x *FeedChatRequest) Reset() { + *x = FeedChatRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FeedChatRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FeedChatRequest) ProtoMessage() {} + +func (x *FeedChatRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FeedChatRequest.ProtoReflect.Descriptor instead. +func (*FeedChatRequest) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{3} +} + +func (x *FeedChatRequest) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *FeedChatRequest) GetLastID() int64 { + if x != nil { + return x.LastID + } + return 0 +} + +type FeedMessageRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Chat *Chat `protobuf:"bytes,1,opt,name=chat,proto3" json:"chat,omitempty"` + Count int64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` + LastID int64 `protobuf:"varint,3,opt,name=lastID,proto3" json:"lastID,omitempty"` +} + +func (x *FeedMessageRequest) Reset() { + *x = FeedMessageRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FeedMessageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FeedMessageRequest) ProtoMessage() {} + +func (x *FeedMessageRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FeedMessageRequest.ProtoReflect.Descriptor instead. +func (*FeedMessageRequest) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{4} +} + +func (x *FeedMessageRequest) GetChat() *Chat { + if x != nil { + return x.Chat + } + return nil +} + +func (x *FeedMessageRequest) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *FeedMessageRequest) GetLastID() int64 { + if x != nil { + return x.LastID + } + return 0 +} + +type WichWhomChat struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserID int64 `protobuf:"varint,1,opt,name=userID,proto3" json:"userID,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Avatar string `protobuf:"bytes,3,opt,name=avatar,proto3" json:"avatar,omitempty"` +} + +func (x *WichWhomChat) Reset() { + *x = WichWhomChat{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WichWhomChat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WichWhomChat) ProtoMessage() {} + +func (x *WichWhomChat) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WichWhomChat.ProtoReflect.Descriptor instead. +func (*WichWhomChat) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{5} +} + +func (x *WichWhomChat) GetUserID() int64 { + if x != nil { + return x.UserID + } + return 0 +} + +func (x *WichWhomChat) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *WichWhomChat) GetAvatar() string { + if x != nil { + return x.Avatar + } + return "" +} + +type ChatWithUser struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LastMessageID int64 `protobuf:"varint,1,opt,name=lastMessageID,proto3" json:"lastMessageID,omitempty"` + Chat *WichWhomChat `protobuf:"bytes,2,opt,name=chat,proto3" json:"chat,omitempty"` +} + +func (x *ChatWithUser) Reset() { + *x = ChatWithUser{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChatWithUser) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChatWithUser) ProtoMessage() {} + +func (x *ChatWithUser) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChatWithUser.ProtoReflect.Descriptor instead. +func (*ChatWithUser) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{6} +} + +func (x *ChatWithUser) GetLastMessageID() int64 { + if x != nil { + return x.LastMessageID + } + return 0 +} + +func (x *ChatWithUser) GetChat() *WichWhomChat { + if x != nil { + return x.Chat + } + return nil +} + +type FeedMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Messages []*Message `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` + LastID int64 `protobuf:"varint,2,opt,name=lastID,proto3" json:"lastID,omitempty"` +} + +func (x *FeedMessage) Reset() { + *x = FeedMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FeedMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FeedMessage) ProtoMessage() {} + +func (x *FeedMessage) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FeedMessage.ProtoReflect.Descriptor instead. +func (*FeedMessage) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{7} +} + +func (x *FeedMessage) GetMessages() []*Message { + if x != nil { + return x.Messages + } + return nil +} + +func (x *FeedMessage) GetLastID() int64 { + if x != nil { + return x.LastID + } + return 0 +} + +type FeedChat struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Chats []*ChatWithUser `protobuf:"bytes,1,rep,name=chats,proto3" json:"chats,omitempty"` + LastID int64 `protobuf:"varint,2,opt,name=lastID,proto3" json:"lastID,omitempty"` +} + +func (x *FeedChat) Reset() { + *x = FeedChat{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_messenger_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FeedChat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FeedChat) ProtoMessage() {} + +func (x *FeedChat) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_messenger_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FeedChat.ProtoReflect.Descriptor instead. +func (*FeedChat) Descriptor() ([]byte, []int) { + return file_api_proto_messenger_proto_rawDescGZIP(), []int{8} +} + +func (x *FeedChat) GetChats() []*ChatWithUser { + if x != nil { + return x.Chats + } + return nil +} + +func (x *FeedChat) GetLastID() int64 { + if x != nil { + return x.LastID + } + return 0 +} + +var File_api_proto_messenger_proto protoreflect.FileDescriptor + +var file_api_proto_messenger_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6d, 0x65, 0x73, 0x73, + 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x6d, 0x65, 0x73, + 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0x17, 0x0a, 0x05, 0x4d, 0x73, 0x67, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x7b, 0x0a, 0x07, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, + 0x4d, 0x73, 0x67, 0x49, 0x44, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x74, + 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x54, 0x6f, 0x12, + 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x3a, 0x0a, 0x04, 0x43, 0x68, 0x61, + 0x74, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x31, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x07, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x31, 0x12, 0x18, 0x0a, 0x07, 0x75, + 0x73, 0x65, 0x72, 0x49, 0x44, 0x32, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x75, 0x73, + 0x65, 0x72, 0x49, 0x44, 0x32, 0x22, 0x3f, 0x0a, 0x0f, 0x46, 0x65, 0x65, 0x64, 0x43, 0x68, 0x61, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, + 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x6c, 0x61, 0x73, 0x74, 0x49, 0x44, 0x22, 0x67, 0x0a, 0x12, 0x46, 0x65, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x04, + 0x63, 0x68, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6d, 0x65, 0x73, + 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x43, 0x68, 0x61, 0x74, 0x52, 0x04, 0x63, 0x68, 0x61, + 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x49, + 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x44, 0x22, + 0x5a, 0x0a, 0x0c, 0x57, 0x69, 0x63, 0x68, 0x57, 0x68, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x44, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x22, 0x61, 0x0a, 0x0c, 0x43, + 0x68, 0x61, 0x74, 0x57, 0x69, 0x74, 0x68, 0x55, 0x73, 0x65, 0x72, 0x12, 0x24, 0x0a, 0x0d, 0x6c, + 0x61, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0d, 0x6c, 0x61, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, + 0x44, 0x12, 0x2b, 0x0a, 0x04, 0x63, 0x68, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x57, 0x69, 0x63, 0x68, + 0x57, 0x68, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x74, 0x52, 0x04, 0x63, 0x68, 0x61, 0x74, 0x22, 0x55, + 0x0a, 0x0b, 0x46, 0x65, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, + 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x16, 0x0a, + 0x06, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6c, + 0x61, 0x73, 0x74, 0x49, 0x44, 0x22, 0x51, 0x0a, 0x08, 0x46, 0x65, 0x65, 0x64, 0x43, 0x68, 0x61, + 0x74, 0x12, 0x2d, 0x0a, 0x05, 0x63, 0x68, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x43, 0x68, 0x61, + 0x74, 0x57, 0x69, 0x74, 0x68, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x63, 0x68, 0x61, 0x74, 0x73, + 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x44, 0x32, 0x8e, 0x03, 0x0a, 0x09, 0x4d, 0x65, 0x73, + 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x12, 0x4c, 0x0a, 0x17, 0x55, 0x73, 0x65, 0x72, 0x43, 0x68, + 0x61, 0x74, 0x73, 0x57, 0x69, 0x74, 0x68, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x55, 0x73, 0x65, 0x72, + 0x73, 0x12, 0x1a, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x46, 0x65, + 0x65, 0x64, 0x43, 0x68, 0x61, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, + 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x43, 0x68, + 0x61, 0x74, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x12, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x10, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, + 0x67, 0x65, 0x72, 0x2e, 0x4d, 0x73, 0x67, 0x49, 0x44, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0f, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x74, 0x12, 0x1d, + 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x46, 0x65, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0d, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, + 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x10, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, + 0x67, 0x65, 0x72, 0x2e, 0x4d, 0x73, 0x67, 0x49, 0x44, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x00, 0x12, 0x34, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x12, 0x10, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x4d, 0x73, + 0x67, 0x49, 0x44, 0x1a, 0x12, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x2e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x43, 0x5a, 0x41, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x70, 0x61, 0x72, 0x6b, 0x2d, + 0x6d, 0x61, 0x69, 0x6c, 0x2d, 0x72, 0x75, 0x2f, 0x32, 0x30, 0x32, 0x33, 0x5f, 0x32, 0x5f, 0x4f, + 0x4e, 0x44, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x65, 0x6e, 0x67, 0x65, 0x72, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_api_proto_messenger_proto_rawDescOnce sync.Once + file_api_proto_messenger_proto_rawDescData = file_api_proto_messenger_proto_rawDesc +) + +func file_api_proto_messenger_proto_rawDescGZIP() []byte { + file_api_proto_messenger_proto_rawDescOnce.Do(func() { + file_api_proto_messenger_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_messenger_proto_rawDescData) + }) + return file_api_proto_messenger_proto_rawDescData +} + +var file_api_proto_messenger_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_api_proto_messenger_proto_goTypes = []interface{}{ + (*MsgID)(nil), // 0: messenger.MsgID + (*Message)(nil), // 1: messenger.Message + (*Chat)(nil), // 2: messenger.Chat + (*FeedChatRequest)(nil), // 3: messenger.FeedChatRequest + (*FeedMessageRequest)(nil), // 4: messenger.FeedMessageRequest + (*WichWhomChat)(nil), // 5: messenger.WichWhomChat + (*ChatWithUser)(nil), // 6: messenger.ChatWithUser + (*FeedMessage)(nil), // 7: messenger.FeedMessage + (*FeedChat)(nil), // 8: messenger.FeedChat + (*empty.Empty)(nil), // 9: google.protobuf.Empty +} +var file_api_proto_messenger_proto_depIdxs = []int32{ + 0, // 0: messenger.Message.id:type_name -> messenger.MsgID + 2, // 1: messenger.FeedMessageRequest.chat:type_name -> messenger.Chat + 5, // 2: messenger.ChatWithUser.chat:type_name -> messenger.WichWhomChat + 1, // 3: messenger.FeedMessage.messages:type_name -> messenger.Message + 6, // 4: messenger.FeedChat.chats:type_name -> messenger.ChatWithUser + 3, // 5: messenger.Messenger.UserChatsWithOtherUsers:input_type -> messenger.FeedChatRequest + 1, // 6: messenger.Messenger.SendMessage:input_type -> messenger.Message + 4, // 7: messenger.Messenger.MessageFromChat:input_type -> messenger.FeedMessageRequest + 1, // 8: messenger.Messenger.UpdateMessage:input_type -> messenger.Message + 0, // 9: messenger.Messenger.DeleteMessage:input_type -> messenger.MsgID + 0, // 10: messenger.Messenger.GetMessage:input_type -> messenger.MsgID + 8, // 11: messenger.Messenger.UserChatsWithOtherUsers:output_type -> messenger.FeedChat + 0, // 12: messenger.Messenger.SendMessage:output_type -> messenger.MsgID + 7, // 13: messenger.Messenger.MessageFromChat:output_type -> messenger.FeedMessage + 9, // 14: messenger.Messenger.UpdateMessage:output_type -> google.protobuf.Empty + 9, // 15: messenger.Messenger.DeleteMessage:output_type -> google.protobuf.Empty + 1, // 16: messenger.Messenger.GetMessage:output_type -> messenger.Message + 11, // [11:17] is the sub-list for method output_type + 5, // [5:11] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_api_proto_messenger_proto_init() } +func file_api_proto_messenger_proto_init() { + if File_api_proto_messenger_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_api_proto_messenger_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MsgID); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_messenger_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_messenger_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Chat); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_messenger_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FeedChatRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_messenger_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FeedMessageRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_messenger_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WichWhomChat); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_messenger_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChatWithUser); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_messenger_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FeedMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_messenger_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FeedChat); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_api_proto_messenger_proto_rawDesc, + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_proto_messenger_proto_goTypes, + DependencyIndexes: file_api_proto_messenger_proto_depIdxs, + MessageInfos: file_api_proto_messenger_proto_msgTypes, + }.Build() + File_api_proto_messenger_proto = out.File + file_api_proto_messenger_proto_rawDesc = nil + file_api_proto_messenger_proto_goTypes = nil + file_api_proto_messenger_proto_depIdxs = nil +} diff --git a/internal/api/messenger/messenger_grpc.pb.go b/internal/api/messenger/messenger_grpc.pb.go new file mode 100644 index 0000000..b2fb9eb --- /dev/null +++ b/internal/api/messenger/messenger_grpc.pb.go @@ -0,0 +1,286 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.6.1 +// source: api/proto/messenger.proto + +package messenger + +import ( + context "context" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// MessengerClient is the client API for Messenger service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type MessengerClient interface { + UserChatsWithOtherUsers(ctx context.Context, in *FeedChatRequest, opts ...grpc.CallOption) (*FeedChat, error) + SendMessage(ctx context.Context, in *Message, opts ...grpc.CallOption) (*MsgID, error) + MessageFromChat(ctx context.Context, in *FeedMessageRequest, opts ...grpc.CallOption) (*FeedMessage, error) + UpdateMessage(ctx context.Context, in *Message, opts ...grpc.CallOption) (*empty.Empty, error) + DeleteMessage(ctx context.Context, in *MsgID, opts ...grpc.CallOption) (*empty.Empty, error) + GetMessage(ctx context.Context, in *MsgID, opts ...grpc.CallOption) (*Message, error) +} + +type messengerClient struct { + cc grpc.ClientConnInterface +} + +func NewMessengerClient(cc grpc.ClientConnInterface) MessengerClient { + return &messengerClient{cc} +} + +func (c *messengerClient) UserChatsWithOtherUsers(ctx context.Context, in *FeedChatRequest, opts ...grpc.CallOption) (*FeedChat, error) { + out := new(FeedChat) + err := c.cc.Invoke(ctx, "/messenger.Messenger/UserChatsWithOtherUsers", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *messengerClient) SendMessage(ctx context.Context, in *Message, opts ...grpc.CallOption) (*MsgID, error) { + out := new(MsgID) + err := c.cc.Invoke(ctx, "/messenger.Messenger/SendMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *messengerClient) MessageFromChat(ctx context.Context, in *FeedMessageRequest, opts ...grpc.CallOption) (*FeedMessage, error) { + out := new(FeedMessage) + err := c.cc.Invoke(ctx, "/messenger.Messenger/MessageFromChat", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *messengerClient) UpdateMessage(ctx context.Context, in *Message, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/messenger.Messenger/UpdateMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *messengerClient) DeleteMessage(ctx context.Context, in *MsgID, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/messenger.Messenger/DeleteMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *messengerClient) GetMessage(ctx context.Context, in *MsgID, opts ...grpc.CallOption) (*Message, error) { + out := new(Message) + err := c.cc.Invoke(ctx, "/messenger.Messenger/GetMessage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MessengerServer is the server API for Messenger service. +// All implementations must embed UnimplementedMessengerServer +// for forward compatibility +type MessengerServer interface { + UserChatsWithOtherUsers(context.Context, *FeedChatRequest) (*FeedChat, error) + SendMessage(context.Context, *Message) (*MsgID, error) + MessageFromChat(context.Context, *FeedMessageRequest) (*FeedMessage, error) + UpdateMessage(context.Context, *Message) (*empty.Empty, error) + DeleteMessage(context.Context, *MsgID) (*empty.Empty, error) + GetMessage(context.Context, *MsgID) (*Message, error) + mustEmbedUnimplementedMessengerServer() +} + +// UnimplementedMessengerServer must be embedded to have forward compatible implementations. +type UnimplementedMessengerServer struct { +} + +func (UnimplementedMessengerServer) UserChatsWithOtherUsers(context.Context, *FeedChatRequest) (*FeedChat, error) { + return nil, status.Errorf(codes.Unimplemented, "method UserChatsWithOtherUsers not implemented") +} +func (UnimplementedMessengerServer) SendMessage(context.Context, *Message) (*MsgID, error) { + return nil, status.Errorf(codes.Unimplemented, "method SendMessage not implemented") +} +func (UnimplementedMessengerServer) MessageFromChat(context.Context, *FeedMessageRequest) (*FeedMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method MessageFromChat not implemented") +} +func (UnimplementedMessengerServer) UpdateMessage(context.Context, *Message) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateMessage not implemented") +} +func (UnimplementedMessengerServer) DeleteMessage(context.Context, *MsgID) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteMessage not implemented") +} +func (UnimplementedMessengerServer) GetMessage(context.Context, *MsgID) (*Message, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetMessage not implemented") +} +func (UnimplementedMessengerServer) mustEmbedUnimplementedMessengerServer() {} + +// UnsafeMessengerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to MessengerServer will +// result in compilation errors. +type UnsafeMessengerServer interface { + mustEmbedUnimplementedMessengerServer() +} + +func RegisterMessengerServer(s grpc.ServiceRegistrar, srv MessengerServer) { + s.RegisterService(&Messenger_ServiceDesc, srv) +} + +func _Messenger_UserChatsWithOtherUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FeedChatRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MessengerServer).UserChatsWithOtherUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/messenger.Messenger/UserChatsWithOtherUsers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MessengerServer).UserChatsWithOtherUsers(ctx, req.(*FeedChatRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Messenger_SendMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Message) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MessengerServer).SendMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/messenger.Messenger/SendMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MessengerServer).SendMessage(ctx, req.(*Message)) + } + return interceptor(ctx, in, info, handler) +} + +func _Messenger_MessageFromChat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FeedMessageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MessengerServer).MessageFromChat(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/messenger.Messenger/MessageFromChat", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MessengerServer).MessageFromChat(ctx, req.(*FeedMessageRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Messenger_UpdateMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Message) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MessengerServer).UpdateMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/messenger.Messenger/UpdateMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MessengerServer).UpdateMessage(ctx, req.(*Message)) + } + return interceptor(ctx, in, info, handler) +} + +func _Messenger_DeleteMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgID) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MessengerServer).DeleteMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/messenger.Messenger/DeleteMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MessengerServer).DeleteMessage(ctx, req.(*MsgID)) + } + return interceptor(ctx, in, info, handler) +} + +func _Messenger_GetMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgID) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MessengerServer).GetMessage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/messenger.Messenger/GetMessage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MessengerServer).GetMessage(ctx, req.(*MsgID)) + } + return interceptor(ctx, in, info, handler) +} + +// Messenger_ServiceDesc is the grpc.ServiceDesc for Messenger service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Messenger_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "messenger.Messenger", + HandlerType: (*MessengerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UserChatsWithOtherUsers", + Handler: _Messenger_UserChatsWithOtherUsers_Handler, + }, + { + MethodName: "SendMessage", + Handler: _Messenger_SendMessage_Handler, + }, + { + MethodName: "MessageFromChat", + Handler: _Messenger_MessageFromChat_Handler, + }, + { + MethodName: "UpdateMessage", + Handler: _Messenger_UpdateMessage_Handler, + }, + { + MethodName: "DeleteMessage", + Handler: _Messenger_DeleteMessage_Handler, + }, + { + MethodName: "GetMessage", + Handler: _Messenger_GetMessage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/proto/messenger.proto", +} diff --git a/internal/api/realtime/realtime.pb.go b/internal/api/realtime/realtime.pb.go new file mode 100644 index 0000000..3b3ed8f --- /dev/null +++ b/internal/api/realtime/realtime.pb.go @@ -0,0 +1,626 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.6.1 +// source: api/proto/realtime.proto + +package realtime + +import ( + empty "github.com/golang/protobuf/ptypes/empty" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EventType int32 + +const ( + EventType_EV_CREATE EventType = 0 + EventType_EV_DELETE EventType = 1 + EventType_EV_UPDATE EventType = 2 +) + +// Enum value maps for EventType. +var ( + EventType_name = map[int32]string{ + 0: "EV_CREATE", + 1: "EV_DELETE", + 2: "EV_UPDATE", + } + EventType_value = map[string]int32{ + "EV_CREATE": 0, + "EV_DELETE": 1, + "EV_UPDATE": 2, + } +) + +func (x EventType) Enum() *EventType { + p := new(EventType) + *p = x + return p +} + +func (x EventType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (EventType) Descriptor() protoreflect.EnumDescriptor { + return file_api_proto_realtime_proto_enumTypes[0].Descriptor() +} + +func (EventType) Type() protoreflect.EnumType { + return &file_api_proto_realtime_proto_enumTypes[0] +} + +func (x EventType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use EventType.Descriptor instead. +func (EventType) EnumDescriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{0} +} + +type Channels struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Chans []*Channel `protobuf:"bytes,1,rep,name=chans,proto3" json:"chans,omitempty"` +} + +func (x *Channels) Reset() { + *x = Channels{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_realtime_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Channels) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Channels) ProtoMessage() {} + +func (x *Channels) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_realtime_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Channels.ProtoReflect.Descriptor instead. +func (*Channels) Descriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{0} +} + +func (x *Channels) GetChans() []*Channel { + if x != nil { + return x.Chans + } + return nil +} + +type Channel struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *Channel) Reset() { + *x = Channel{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_realtime_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Channel) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Channel) ProtoMessage() {} + +func (x *Channel) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_realtime_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Channel.ProtoReflect.Descriptor instead. +func (*Channel) Descriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{1} +} + +func (x *Channel) GetTopic() string { + if x != nil { + return x.Topic + } + return "" +} + +func (x *Channel) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type EventObject struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Type EventType `protobuf:"varint,2,opt,name=type,proto3,enum=realtime.EventType" json:"type,omitempty"` +} + +func (x *EventObject) Reset() { + *x = EventObject{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_realtime_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EventObject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventObject) ProtoMessage() {} + +func (x *EventObject) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_realtime_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EventObject.ProtoReflect.Descriptor instead. +func (*EventObject) Descriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{2} +} + +func (x *EventObject) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *EventObject) GetType() EventType { + if x != nil { + return x.Type + } + return EventType_EV_CREATE +} + +type EventMap struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type int64 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"` + M map[string]string `protobuf:"bytes,2,rep,name=m,proto3" json:"m,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *EventMap) Reset() { + *x = EventMap{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_realtime_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EventMap) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventMap) ProtoMessage() {} + +func (x *EventMap) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_realtime_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EventMap.ProtoReflect.Descriptor instead. +func (*EventMap) Descriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{3} +} + +func (x *EventMap) GetType() int64 { + if x != nil { + return x.Type + } + return 0 +} + +func (x *EventMap) GetM() map[string]string { + if x != nil { + return x.M + } + return nil +} + +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Body: + // + // *Message_Object + // *Message_Content + Body isMessage_Body `protobuf_oneof:"body"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_realtime_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_realtime_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{4} +} + +func (m *Message) GetBody() isMessage_Body { + if m != nil { + return m.Body + } + return nil +} + +func (x *Message) GetObject() *EventObject { + if x, ok := x.GetBody().(*Message_Object); ok { + return x.Object + } + return nil +} + +func (x *Message) GetContent() *EventMap { + if x, ok := x.GetBody().(*Message_Content); ok { + return x.Content + } + return nil +} + +type isMessage_Body interface { + isMessage_Body() +} + +type Message_Object struct { + Object *EventObject `protobuf:"bytes,1,opt,name=object,proto3,oneof"` +} + +type Message_Content struct { + Content *EventMap `protobuf:"bytes,2,opt,name=content,proto3,oneof"` +} + +func (*Message_Object) isMessage_Body() {} + +func (*Message_Content) isMessage_Body() {} + +type PublishMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Channel *Channel `protobuf:"bytes,1,opt,name=channel,proto3" json:"channel,omitempty"` + Message *Message `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *PublishMessage) Reset() { + *x = PublishMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_api_proto_realtime_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PublishMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PublishMessage) ProtoMessage() {} + +func (x *PublishMessage) ProtoReflect() protoreflect.Message { + mi := &file_api_proto_realtime_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PublishMessage.ProtoReflect.Descriptor instead. +func (*PublishMessage) Descriptor() ([]byte, []int) { + return file_api_proto_realtime_proto_rawDescGZIP(), []int{5} +} + +func (x *PublishMessage) GetChannel() *Channel { + if x != nil { + return x.Channel + } + return nil +} + +func (x *PublishMessage) GetMessage() *Message { + if x != nil { + return x.Message + } + return nil +} + +var File_api_proto_realtime_proto protoreflect.FileDescriptor + +var file_api_proto_realtime_proto_rawDesc = []byte{ + 0x0a, 0x18, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x72, 0x65, 0x61, 0x6c, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x72, 0x65, 0x61, 0x6c, + 0x74, 0x69, 0x6d, 0x65, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x33, 0x0a, 0x08, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x12, 0x27, 0x0a, + 0x05, 0x63, 0x68, 0x61, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, + 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, + 0x05, 0x63, 0x68, 0x61, 0x6e, 0x73, 0x22, 0x33, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, + 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x46, 0x0a, 0x0b, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x27, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, + 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x22, 0x7d, 0x0a, 0x08, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x70, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x01, 0x6d, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, + 0x61, 0x70, 0x2e, 0x4d, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x01, 0x6d, 0x1a, 0x34, 0x0a, 0x06, + 0x4d, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0x72, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2f, 0x0a, + 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x48, 0x00, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x2e, + 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x12, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x4d, 0x61, 0x70, 0x48, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x42, 0x06, + 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x6a, 0x0a, 0x0e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, + 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, + 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x07, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x2b, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, + 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x2a, 0x38, 0x0a, 0x09, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x0d, 0x0a, 0x09, 0x45, 0x56, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x00, 0x12, 0x0d, + 0x0a, 0x09, 0x45, 0x56, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, + 0x09, 0x45, 0x56, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x32, 0x81, 0x01, 0x0a, + 0x08, 0x52, 0x65, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3d, 0x0a, 0x07, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x73, 0x68, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, + 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x36, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x12, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, + 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x1a, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, + 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, + 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x6f, 0x2d, 0x70, 0x61, 0x72, 0x6b, 0x2d, 0x6d, 0x61, 0x69, 0x6c, 0x2d, 0x72, 0x75, 0x2f, 0x32, + 0x30, 0x32, 0x33, 0x5f, 0x32, 0x5f, 0x4f, 0x4e, 0x44, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x61, 0x6c, + 0x74, 0x69, 0x6d, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_api_proto_realtime_proto_rawDescOnce sync.Once + file_api_proto_realtime_proto_rawDescData = file_api_proto_realtime_proto_rawDesc +) + +func file_api_proto_realtime_proto_rawDescGZIP() []byte { + file_api_proto_realtime_proto_rawDescOnce.Do(func() { + file_api_proto_realtime_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_realtime_proto_rawDescData) + }) + return file_api_proto_realtime_proto_rawDescData +} + +var file_api_proto_realtime_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_api_proto_realtime_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_api_proto_realtime_proto_goTypes = []interface{}{ + (EventType)(0), // 0: realtime.EventType + (*Channels)(nil), // 1: realtime.Channels + (*Channel)(nil), // 2: realtime.Channel + (*EventObject)(nil), // 3: realtime.EventObject + (*EventMap)(nil), // 4: realtime.EventMap + (*Message)(nil), // 5: realtime.Message + (*PublishMessage)(nil), // 6: realtime.PublishMessage + nil, // 7: realtime.EventMap.MEntry + (*empty.Empty)(nil), // 8: google.protobuf.Empty +} +var file_api_proto_realtime_proto_depIdxs = []int32{ + 2, // 0: realtime.Channels.chans:type_name -> realtime.Channel + 0, // 1: realtime.EventObject.type:type_name -> realtime.EventType + 7, // 2: realtime.EventMap.m:type_name -> realtime.EventMap.MEntry + 3, // 3: realtime.Message.object:type_name -> realtime.EventObject + 4, // 4: realtime.Message.content:type_name -> realtime.EventMap + 2, // 5: realtime.PublishMessage.channel:type_name -> realtime.Channel + 5, // 6: realtime.PublishMessage.message:type_name -> realtime.Message + 6, // 7: realtime.RealTime.Publish:input_type -> realtime.PublishMessage + 1, // 8: realtime.RealTime.Subscribe:input_type -> realtime.Channels + 8, // 9: realtime.RealTime.Publish:output_type -> google.protobuf.Empty + 5, // 10: realtime.RealTime.Subscribe:output_type -> realtime.Message + 9, // [9:11] is the sub-list for method output_type + 7, // [7:9] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_api_proto_realtime_proto_init() } +func file_api_proto_realtime_proto_init() { + if File_api_proto_realtime_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_api_proto_realtime_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Channels); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_realtime_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Channel); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_realtime_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EventObject); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_realtime_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EventMap); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_realtime_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_api_proto_realtime_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PublishMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_api_proto_realtime_proto_msgTypes[4].OneofWrappers = []interface{}{ + (*Message_Object)(nil), + (*Message_Content)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_api_proto_realtime_proto_rawDesc, + NumEnums: 1, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_proto_realtime_proto_goTypes, + DependencyIndexes: file_api_proto_realtime_proto_depIdxs, + EnumInfos: file_api_proto_realtime_proto_enumTypes, + MessageInfos: file_api_proto_realtime_proto_msgTypes, + }.Build() + File_api_proto_realtime_proto = out.File + file_api_proto_realtime_proto_rawDesc = nil + file_api_proto_realtime_proto_goTypes = nil + file_api_proto_realtime_proto_depIdxs = nil +} diff --git a/internal/api/realtime/realtime_grpc.pb.go b/internal/api/realtime/realtime_grpc.pb.go new file mode 100644 index 0000000..420b61e --- /dev/null +++ b/internal/api/realtime/realtime_grpc.pb.go @@ -0,0 +1,170 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.2.0 +// - protoc v3.6.1 +// source: api/proto/realtime.proto + +package realtime + +import ( + context "context" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// RealTimeClient is the client API for RealTime service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type RealTimeClient interface { + Publish(ctx context.Context, in *PublishMessage, opts ...grpc.CallOption) (*empty.Empty, error) + Subscribe(ctx context.Context, in *Channels, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) +} + +type realTimeClient struct { + cc grpc.ClientConnInterface +} + +func NewRealTimeClient(cc grpc.ClientConnInterface) RealTimeClient { + return &realTimeClient{cc} +} + +func (c *realTimeClient) Publish(ctx context.Context, in *PublishMessage, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/realtime.RealTime/Publish", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *realTimeClient) Subscribe(ctx context.Context, in *Channels, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) { + stream, err := c.cc.NewStream(ctx, &RealTime_ServiceDesc.Streams[0], "/realtime.RealTime/Subscribe", opts...) + if err != nil { + return nil, err + } + x := &realTimeSubscribeClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type RealTime_SubscribeClient interface { + Recv() (*Message, error) + grpc.ClientStream +} + +type realTimeSubscribeClient struct { + grpc.ClientStream +} + +func (x *realTimeSubscribeClient) Recv() (*Message, error) { + m := new(Message) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// RealTimeServer is the server API for RealTime service. +// All implementations must embed UnimplementedRealTimeServer +// for forward compatibility +type RealTimeServer interface { + Publish(context.Context, *PublishMessage) (*empty.Empty, error) + Subscribe(*Channels, RealTime_SubscribeServer) error + mustEmbedUnimplementedRealTimeServer() +} + +// UnimplementedRealTimeServer must be embedded to have forward compatible implementations. +type UnimplementedRealTimeServer struct { +} + +func (UnimplementedRealTimeServer) Publish(context.Context, *PublishMessage) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Publish not implemented") +} +func (UnimplementedRealTimeServer) Subscribe(*Channels, RealTime_SubscribeServer) error { + return status.Errorf(codes.Unimplemented, "method Subscribe not implemented") +} +func (UnimplementedRealTimeServer) mustEmbedUnimplementedRealTimeServer() {} + +// UnsafeRealTimeServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RealTimeServer will +// result in compilation errors. +type UnsafeRealTimeServer interface { + mustEmbedUnimplementedRealTimeServer() +} + +func RegisterRealTimeServer(s grpc.ServiceRegistrar, srv RealTimeServer) { + s.RegisterService(&RealTime_ServiceDesc, srv) +} + +func _RealTime_Publish_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PublishMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RealTimeServer).Publish(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/realtime.RealTime/Publish", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RealTimeServer).Publish(ctx, req.(*PublishMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _RealTime_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(Channels) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(RealTimeServer).Subscribe(m, &realTimeSubscribeServer{stream}) +} + +type RealTime_SubscribeServer interface { + Send(*Message) error + grpc.ServerStream +} + +type realTimeSubscribeServer struct { + grpc.ServerStream +} + +func (x *realTimeSubscribeServer) Send(m *Message) error { + return x.ServerStream.SendMsg(m) +} + +// RealTime_ServiceDesc is the grpc.ServiceDesc for RealTime service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RealTime_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "realtime.RealTime", + HandlerType: (*RealTimeServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Publish", + Handler: _RealTime_Publish_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Subscribe", + Handler: _RealTime_Subscribe_Handler, + ServerStreams: true, + }, + }, + Metadata: "api/proto/realtime.proto", +} diff --git a/internal/api/server/router/router.go b/internal/api/server/router/router.go index 368fb08..cb1bfa4 100644 --- a/internal/api/server/router/router.go +++ b/internal/api/server/router/router.go @@ -2,6 +2,7 @@ package router import ( "net/http" + "time" "github.com/go-chi/chi/v5" "github.com/rs/cors" @@ -9,13 +10,17 @@ import ( _ "github.com/go-park-mail-ru/2023_2_OND_team/docs" deliveryHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1" + deliveryWS "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/websocket" mw "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/monitoring" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/security" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/session" + authCase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/auth" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) +const requestTimeout = 10 * time.Second + type Router struct { Mux *chi.Mux } @@ -24,24 +29,26 @@ func New() Router { return Router{chi.NewMux()} } -func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, sm session.SessionManager, log *logger.Logger) { +func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, wsHandler *deliveryWS.HandlerWebSocket, ac authCase.Usecase, metrics monitoring.Metrics, log *logger.Logger) { cfgCSRF := security.DefaultCSRFConfig() cfgCSRF.PathToGet = "/api/v1/csrf" c := cors.New(cors.Options{ - AllowedOrigins: []string{"https://pinspire.online", "https://pinspire.online:1443", - "https://pinspire.online:1444", "https://pinspire.online:1445", "https://pinspire.online:1446"}, + AllowedOrigins: []string{"https://pinspire.site", "https://pinspire.site:1443", + "https://pinspire.site:1444", "https://pinspire.site:1445", "https://pinspire.site:1446", "https://pinspire.site:8081"}, AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPut}, AllowCredentials: true, AllowedHeaders: []string{"content-type", cfgCSRF.Header}, ExposedHeaders: []string{cfgCSRF.HeaderSet}, }) - r.Mux.Use(mw.RequestID(log), mw.Logger(log), c.Handler, - security.CSRF(cfgCSRF), mw.SetResponseHeaders(map[string]string{ + r.Mux.Use(mw.SetRequestTimeout(requestTimeout), mw.RequestID(log), mw.Logger(log), + monitoring.Monitoring("/metrics", metrics), c.Handler, + security.CSRF(cfgCSRF), + mw.SetResponseHeaders(map[string]string{ "Content-Type": "application/json", }), - auth.NewAuthMiddleware(sm).ContextWithUserID) + auth.NewAuthMiddleware(ac).ContextWithUserID) r.Mux.Route("/api/v1", func(r chi.Router) { r.Get("/docs/*", httpSwagger.WrapHandler) @@ -60,6 +67,27 @@ func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, sm session.Sess r.Get("/info", handler.GetProfileInfo) r.Put("/edit", handler.ProfileEditInfo) r.Put("/avatar", handler.ProfileEditAvatar) + r.Get("/header", handler.GetProfileHeaderInfo) + }) + + r.Route("/user", func(r chi.Router) { + r.Get("/info/{userID:\\d+}", handler.GetUserInfo) + }) + + r.Route("/subscription", func(r chi.Router) { + r.Route("/user", func(r chi.Router) { + r.With(auth.RequireAuth).Group(func(r chi.Router) { + r.Post("/create", handler.Subscribe) + r.Delete("/delete", handler.Unsubscribe) + }) + r.Get("/get", handler.GetSubscriptionInfoForUser) + }) + }) + + r.Route("/search", func(r chi.Router) { + r.Get("/users", handler.SearchUsers) + r.Get("/boards", handler.SearchBoards) + r.Get("/pins", handler.SearchPins) }) r.Route("/pin", func(r chi.Router) { @@ -73,6 +101,15 @@ func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, sm session.Sess r.Delete("/like/{pinID:\\d+}", handler.DeleteLikePin) r.Delete("/delete/{pinID:\\d+}", handler.DeletePin) }) + + r.Route("/comment", func(r chi.Router) { + r.Get("/feed/{pinID:\\d+}", handler.ViewFeedComment) + + r.With(auth.RequireAuth).Group(func(r chi.Router) { + r.Post("/{pinID:\\d+}", handler.WriteComment) + r.Delete("/{commentID:\\d+}", handler.DeleteComment) + }) + }) }) r.Route("/board", func(r chi.Router) { @@ -83,6 +120,7 @@ func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, sm session.Sess }) r.With(auth.RequireAuth).Group(func(r chi.Router) { r.Post("/add/pins/{boardID:\\d+}", handler.AddPinsToBoard) + r.Delete("/delete/pin/{boardID:\\d+}", handler.DeletePinFromBoard) r.Post("/create", handler.CreateNewBoard) r.Put("/update/{boardID:\\d+}", handler.UpdateBoardInfo) r.Delete("/delete/{boardID:\\d+}", handler.DeleteBoard) @@ -92,5 +130,18 @@ func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, sm session.Sess r.Route("/feed", func(r chi.Router) { r.Get("/pin", handler.FeedPins) }) + + r.With(auth.RequireAuth).Route("/chat", func(r chi.Router) { + r.Get("/personal", handler.FeedChats) + r.Get("/get/{userID:\\d+}", handler.GetMessagesFromChat) + r.Post("/send/{userID:\\d+}", handler.SendMessageToUser) + r.Put("/update/{messageID:\\d+}", handler.UpdateMessage) + r.Delete("/delete/{messageID:\\d+}", handler.DeleteMessage) + }) + }) + + r.Mux.With(auth.RequireAuth).Route("/websocket/connect", func(r chi.Router) { + r.Get("/chat", wsHandler.Chat) + r.Get("/notification", wsHandler.Notification) }) } diff --git a/internal/app/app.go b/internal/app/app.go index e6031b5..f2706ef 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,38 +2,66 @@ package app import ( "context" + "encoding/base64" + "os" "time" - "github.com/joho/godotenv" + goaway "github.com/TwiN/go-away" "github.com/microcosm-cc/bluemonday" + "google.golang.org/api/option" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + vision "cloud.google.com/go/vision/v2/apiv1" + authProto "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/auth" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger" + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/server" "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/server/router" deliveryHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1" + deliveryWS "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/websocket" + notify "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/metrics" + commentNotify "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/notification/comment" boardRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/board/postgres" + commentRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/comment" imgRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/image" pinRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/pin" - sessionRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/session" + searchRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/search/postgres" + subRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/subscription/postgres" userRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/auth" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/board" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/comment" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/image" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/message" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/pin" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/session" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime/chat" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime/notification" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/search" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/subscription" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" + validate "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) var ( - timeoutForConnPG = 5 * time.Second - timeoutForConnRedis = 5 * time.Second + _timeoutForConnPG = 5 * time.Second + timeoutCloudVisionAPI = 10 * time.Second ) const uploadFiles = "upload/" func Run(ctx context.Context, log *log.Logger, cfg ConfigFiles) { - godotenv.Load() + metrics := metrics.New("pinspire") + err := metrics.Registry() + if err != nil { + log.Error(err.Error()) + return + } - ctx, cancelCtxPG := context.WithTimeout(ctx, timeoutForConnPG) + ctx, cancelCtxPG := context.WithTimeout(ctx, _timeoutForConnPG) defer cancelCtxPG() pool, err := NewPoolPG(ctx) @@ -43,29 +71,79 @@ func Run(ctx context.Context, log *log.Logger, cfg ConfigFiles) { } defer pool.Close() - ctx, cancelCtxRedis := context.WithTimeout(ctx, timeoutForConnRedis) - defer cancelCtxRedis() + connMessMS, err := grpc.Dial(os.Getenv("MESSENGER_SERVICE_HOST")+":"+os.Getenv("MESSENGER_SERVICE_PORT"), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error(err.Error()) + return + } + defer connMessMS.Close() - redisCfg, err := NewConfig(cfg.RedisConfigFile) + connRealtime, err := grpc.Dial(os.Getenv("REALTIME_SERVICE_HOST")+":"+os.Getenv("REALTIME_SERVICE_PORT"), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Error(err.Error()) return } + defer connRealtime.Close() - redisCl, err := NewRedisClient(ctx, redisCfg) + rtClient := rt.NewRealTimeClient(connRealtime) + + commentRepository := commentRepo.NewCommentRepoPG(pool) + + visionCtx, cancel := context.WithTimeout(ctx, timeoutCloudVisionAPI) + defer cancel() + + token, err := base64.StdEncoding.DecodeString(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + if err != nil { + log.Error(err.Error()) + return + } + visionClient, err := vision.NewImageAnnotatorClient(visionCtx, option.WithCredentialsJSON(token)) if err != nil { log.Error(err.Error()) return } - defer redisCl.Close() - sm := session.New(log, sessionRepo.NewSessionRepo(redisCl)) - imgCase := image.New(log, imgRepo.NewImageRepoFS(uploadFiles)) - userCase := user.New(log, imgCase, userRepo.NewUserRepoPG(pool)) + profanityCensor := goaway.NewProfanityDetector().WithCustomDictionary( + append(goaway.DefaultProfanities, validate.GetLabels()...), + goaway.DefaultFalsePositives, + goaway.DefaultFalseNegatives, + ) + + imgCase := image.New(log, imgRepo.NewImageRepoFS(uploadFiles), image.NewFilter(visionClient, validate.NewCensor(profanityCensor))) + messageCase := message.New(log, messenger.NewMessengerClient(connMessMS), chat.New(realtime.NewRealTimeChatClient(rtClient), log)) pinCase := pin.New(log, imgCase, pinRepo.NewPinRepoPG(pool)) - boardCase := board.New(log, boardRepo.NewBoardRepoPG(pool), userRepo.NewUserRepoPG(pool), bluemonday.UGCPolicy()) - handler := deliveryHTTP.New(log, sm, userCase, pinCase, boardCase) + notifyBuilder, err := notify.NewWithType(notify.NotifyComment) + if err != nil { + log.Error(err.Error()) + return + } + + notifyCase := notification.New(realtime.NewRealTimeNotificationClient(rtClient), log, + notification.Register(commentNotify.NewCommentNotify(notifyBuilder, comment.New(commentRepository, pinCase, nil), pinCase))) + + conn, err := grpc.Dial(cfg.AddrAuthServer, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Error(err.Error()) + return + } + defer conn.Close() + ac := auth.New(authProto.NewAuthClient(conn)) + + handler := deliveryHTTP.New(log, deliveryHTTP.NewConverterHTTP(validate.NewSanitizerXSS(bluemonday.UGCPolicy()), validate.NewCensor(profanityCensor)), deliveryHTTP.UsecaseHub{ + AuhtCase: ac, + UserCase: user.New(log, imgCase, userRepo.NewUserRepoPG(pool)), + PinCase: pinCase, + BoardCase: board.New(log, boardRepo.NewBoardRepoPG(pool), userRepo.NewUserRepoPG(pool)), + SubscriptionCase: subscription.New(log, subRepo.NewSubscriptionRepoPG(pool), userRepo.NewUserRepoPG(pool)), + SearchCase: search.New(log, searchRepo.NewSearchRepoPG(pool)), + MessageCase: messageCase, + CommentCase: comment.New(commentRepo.NewCommentRepoPG(pool), pinCase, notifyCase), + }) + + wsHandler := deliveryWS.New(log, messageCase, notifyCase, + deliveryWS.SetOriginPatterns([]string{"pinspire.site", "pinspire.site:*"})) + cfgServ, err := server.NewConfig(cfg.ServerConfigFile) if err != nil { log.Error(err.Error()) @@ -73,7 +151,7 @@ func Run(ctx context.Context, log *log.Logger, cfg ConfigFiles) { } server := server.New(log, cfgServ) router := router.New() - router.RegisterRoute(handler, sm, log) + router.RegisterRoute(handler, wsHandler, ac, metrics, log) if err := server.Run(router.Mux); err != nil { log.Error(err.Error()) diff --git a/internal/app/auth/auth.go b/internal/app/auth/auth.go new file mode 100644 index 0000000..ddfc24e --- /dev/null +++ b/internal/app/auth/auth.go @@ -0,0 +1,89 @@ +package auth + +import ( + "context" + "net" + "os" + "time" + + "github.com/joho/godotenv" + "google.golang.org/grpc" + + authProto "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/auth" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/app" + authMS "github.com/go-park-mail-ru/2023_2_OND_team/internal/microservices/auth/delivery/grpc" + grpcMetrics "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/metrics/grpc" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/grpc/interceptor" + sessRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/session" + userRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/session" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +var ( + _timeoutForConnPG = 5 * time.Second + _timeoutForConnRedis = 5 * time.Second +) + +func Run(ctx context.Context, log *logger.Logger, cfg Config) { + godotenv.Load() + + metrics := grpcMetrics.New("auth") + if err := metrics.Registry(); err != nil { + log.Error(err.Error()) + return + } + + l, err := net.Listen("tcp", cfg.Addr) + if err != nil { + log.Error(err.Error()) + return + } + defer l.Close() + + ctxPG, cancelCtxPG := context.WithTimeout(ctx, _timeoutForConnPG) + defer cancelCtxPG() + + pool, err := app.NewPoolPG(ctxPG) + if err != nil { + log.Error(err.Error()) + return + } + defer pool.Close() + + ctxRedis, cancelCtxRedis := context.WithTimeout(ctx, _timeoutForConnRedis) + defer cancelCtxRedis() + + // redisCfg, err := app.NewConfig(cfg.RedisFileConfig) + redisCfg := app.RedisConfig{ + Addr: os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"), + Password: os.Getenv("REDIS_PASSWORD"), + } + // if err != nil { + // log.Error(err.Error()) + // return + // } + + redisCl, err := app.NewRedisClient(ctxRedis, redisCfg) + if err != nil { + log.Error(err.Error()) + return + } + defer redisCl.Close() + + sm := session.New(log, sessRepo.NewSessionRepo(redisCl)) + u := user.New(log, nil, userRepo.NewUserRepoPG(pool)) + + s := grpc.NewServer(grpc.ChainUnaryInterceptor( + interceptor.Monitoring(metrics, "0.0.0.0:8086"), + interceptor.Logger(log), + )) + authProto.RegisterAuthServer(s, authMS.New(log, sm, u)) + + log.Info("service auht start", logger.F{"addr", cfg.Addr}) + if err = s.Serve(l); err != nil { + log.Error(err.Error()) + return + } +} diff --git a/internal/app/auth/config.go b/internal/app/auth/config.go new file mode 100644 index 0000000..e31181b --- /dev/null +++ b/internal/app/auth/config.go @@ -0,0 +1,6 @@ +package auth + +type Config struct { + Addr string + RedisFileConfig string +} diff --git a/internal/app/config.go b/internal/app/config.go index e667344..4fb3bb2 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -8,21 +8,21 @@ import ( type ConfigFiles struct { ServerConfigFile string - RedisConfigFile string + AddrAuthServer string } -type redisConfig struct { +type RedisConfig struct { Password string Addr string } -func NewConfig(filename string) (redisConfig, error) { +func NewConfig(filename string) (RedisConfig, error) { cfg, err := config.ParseConfig(filename) if err != nil { - return redisConfig{}, fmt.Errorf("new redis config: %w", err) + return RedisConfig{}, fmt.Errorf("new redis config: %w", err) } - return redisConfig{ + return RedisConfig{ Password: cfg.Get("requirepass"), Addr: cfg.Get("host") + ":" + cfg.Get("port"), }, nil diff --git a/internal/app/messenger/messenger.go b/internal/app/messenger/messenger.go new file mode 100644 index 0000000..27ca153 --- /dev/null +++ b/internal/app/messenger/messenger.go @@ -0,0 +1,62 @@ +package messenger + +import ( + "context" + "net" + "time" + + "github.com/joho/godotenv" + "google.golang.org/grpc" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/app" + messMS "github.com/go-park-mail-ru/2023_2_OND_team/internal/microservices/messenger/delivery/grpc" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/microservices/messenger/usecase/message" + grpcMetrics "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/metrics/grpc" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/grpc/interceptor" + mesRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/message" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +const _timeoutForConnPG = 5 * time.Second + +func Run(ctx context.Context, log *logger.Logger) { + godotenv.Load() + + metrics := grpcMetrics.New("messenger") + if err := metrics.Registry(); err != nil { + log.Error(err.Error()) + return + } + + ctx, cancelCtxPG := context.WithTimeout(ctx, _timeoutForConnPG) + defer cancelCtxPG() + + pool, err := app.NewPoolPG(ctx) + if err != nil { + log.Error(err.Error()) + return + } + defer pool.Close() + + messageCase := message.New(mesRepo.NewMessageRepo(pool)) + + server := grpc.NewServer(grpc.ChainUnaryInterceptor( + interceptor.Monitoring(metrics, "0.0.0.0:8096"), + interceptor.Logger(log), + interceptor.Auth(), + )) + messenger.RegisterMessengerServer(server, messMS.New(log, messageCase)) + + l, err := net.Listen("tcp", "0.0.0.0:8095") + if err != nil { + log.Error(err.Error()) + return + } + + log.Info("server messenger start", logger.F{"addr", "localhost:8095"}) + if err := server.Serve(l); err != nil { + log.Error(err.Error()) + return + } +} diff --git a/internal/app/redis_conn.go b/internal/app/redis_conn.go index 8a7527c..8ee910e 100644 --- a/internal/app/redis_conn.go +++ b/internal/app/redis_conn.go @@ -7,7 +7,7 @@ import ( redis "github.com/redis/go-redis/v9" ) -func NewRedisClient(ctx context.Context, cfg redisConfig) (*redis.Client, error) { +func NewRedisClient(ctx context.Context, cfg RedisConfig) (*redis.Client, error) { redisCl := redis.NewClient(&redis.Options{ Addr: cfg.Addr, Password: cfg.Password, diff --git a/internal/microservices/auth/delivery/grpc/server.go b/internal/microservices/auth/delivery/grpc/server.go new file mode 100644 index 0000000..1118b07 --- /dev/null +++ b/internal/microservices/auth/delivery/grpc/server.go @@ -0,0 +1,94 @@ +package auth + +import ( + "context" + + "github.com/golang/protobuf/ptypes/empty" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + authProto "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/auth" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/session" + userUsecase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +type Usecase interface { + Register(ctx context.Context, user *user.User) error + Authentication(ctx context.Context, credentials userUsecase.UserCredentials) (*user.User, error) +} + +type AuthServer struct { + authProto.UnimplementedAuthServer + + log *logger.Logger + sm session.SessionManager + userCase Usecase +} + +func New(log *logger.Logger, sm session.SessionManager, userCase Usecase) AuthServer { + return AuthServer{ + UnimplementedAuthServer: authProto.UnimplementedAuthServer{}, + log: log, + sm: sm, + userCase: userCase, + } +} + +func (as AuthServer) Register(ctx context.Context, rd *authProto.RegisterData) (*empty.Empty, error) { + user := &user.User{ + Email: rd.Email, + Username: rd.Cred.Username, + Password: rd.Cred.Password, + } + + err := as.userCase.Register(ctx, user) + if err != nil { + as.log.Error(err.Error()) + return nil, status.Error(codes.Internal, "") + } + return &empty.Empty{}, nil +} + +func (as AuthServer) Login(ctx context.Context, cred *authProto.Credentials) (*authProto.Session, error) { + user, err := as.userCase.Authentication(ctx, userUsecase.UserCredentials{ + Username: cred.Username, + Password: cred.Password, + }) + if err != nil { + as.log.Error(err.Error()) + return nil, status.Error(codes.Unauthenticated, "failed authentication") + } + + session, err := as.sm.CreateNewSessionForUser(ctx, user.ID) + if err != nil { + as.log.Error(err.Error()) + return nil, status.Error(codes.Internal, "failed to create a session for the user") + } + + return &authProto.Session{ + Key: session.Key, + UserID: int64(session.UserID), + Expire: timestamppb.New(session.Expire), + }, nil +} + +func (as AuthServer) Logout(ctx context.Context, sess *authProto.Session) (*empty.Empty, error) { + err := as.sm.DeleteUserSession(ctx, sess.Key) + if err != nil { + as.log.Error(err.Error()) + return nil, status.Error(codes.Internal, "delete user session") + } + return &empty.Empty{}, nil +} + +func (as AuthServer) GetUserID(ctx context.Context, sess *authProto.Session) (*authProto.UserID, error) { + userID, err := as.sm.GetUserIDBySessionKey(ctx, sess.Key) + if err != nil { + as.log.Error(err.Error()) + return nil, status.Error(codes.NotFound, "session not found") + } + return &authProto.UserID{Id: int64(userID)}, nil +} diff --git a/internal/microservices/messenger/delivery/grpc/convert.go b/internal/microservices/messenger/delivery/grpc/convert.go new file mode 100644 index 0000000..fed3fd8 --- /dev/null +++ b/internal/microservices/messenger/delivery/grpc/convert.go @@ -0,0 +1,36 @@ +package grpc + +import ( + mess "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" +) + +func convertFeedChat(feed message.FeedUserChats) []*mess.ChatWithUser { + res := make([]*mess.ChatWithUser, len(feed)) + + for ind := range feed { + res[ind] = &mess.ChatWithUser{ + LastMessageID: int64(feed[ind].MessageLastID), + Chat: &mess.WichWhomChat{ + UserID: int64(feed[ind].WichWhomChat.ID), + Username: feed[ind].WichWhomChat.Username, + Avatar: feed[ind].WichWhomChat.Avatar, + }, + } + } + return res +} + +func convertFeedMessage(feed []message.Message) []*mess.Message { + res := make([]*mess.Message, len(feed)) + + for ind := range res { + res[ind] = &mess.Message{ + Id: &mess.MsgID{Id: int64(feed[ind].ID)}, + UserFrom: int64(feed[ind].From), + UserTo: int64(feed[ind].To), + Content: feed[ind].Content.String, + } + } + return res +} diff --git a/internal/microservices/messenger/delivery/grpc/server.go b/internal/microservices/messenger/delivery/grpc/server.go new file mode 100644 index 0000000..eeb23ce --- /dev/null +++ b/internal/microservices/messenger/delivery/grpc/server.go @@ -0,0 +1,113 @@ +package grpc + +import ( + "context" + + mess "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/microservices/messenger/usecase/message" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" + "github.com/golang/protobuf/ptypes/empty" + "github.com/jackc/pgx/v5/pgtype" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const AuthenticatedMetadataKey = "user_id" + +type MessengerServer struct { + mess.UnimplementedMessengerServer + + log *logger.Logger + messageCase message.Usecase +} + +func New(log *logger.Logger, msgCase message.Usecase) MessengerServer { + return MessengerServer{ + log: log, + messageCase: msgCase, + } +} + +func (m MessengerServer) UserChatsWithOtherUsers(ctx context.Context, r *mess.FeedChatRequest) (*mess.FeedChat, error) { + userID := ctx.Value(auth.KeyCurrentUserID).(int) + + feed, lastID, err := m.messageCase.GetUserChatsWithOtherUsers(ctx, userID, int(r.GetCount()), int(r.GetLastID())) + if err != nil { + m.log.Error(err.Error()) + } + + return &mess.FeedChat{Chats: convertFeedChat(feed), LastID: int64(lastID)}, nil +} + +func (m MessengerServer) SendMessage(ctx context.Context, msg *mess.Message) (*mess.MsgID, error) { + userID := ctx.Value(auth.KeyCurrentUserID).(int) + + msgID, err := m.messageCase.SendMessage(ctx, &entity.Message{ + From: userID, + To: int(msg.UserTo), + Content: pgtype.Text{String: msg.GetContent(), Valid: true}, + }) + + if err != nil { + m.log.Error(err.Error()) + return nil, status.Error(codes.Internal, "send message error") + } + + return &mess.MsgID{Id: int64(msgID)}, nil +} + +func (m MessengerServer) MessageFromChat(ctx context.Context, r *mess.FeedMessageRequest) (*mess.FeedMessage, error) { + feed, lastID, err := m.messageCase.GetMessagesFromChat(ctx, entity.Chat{int(r.Chat.GetUserID1()), int(r.Chat.GetUserID2())}, + int(r.GetCount()), int(r.GetLastID())) + if err != nil { + m.log.Error(err.Error()) + } + + return &mess.FeedMessage{ + Messages: convertFeedMessage(feed), + LastID: int64(lastID), + }, nil +} + +func (m MessengerServer) UpdateMessage(ctx context.Context, msg *mess.Message) (*empty.Empty, error) { + userID := ctx.Value(auth.KeyCurrentUserID).(int) + + err := m.messageCase.UpdateContentMessage(ctx, userID, &entity.Message{ + ID: int(msg.Id.GetId()), + Content: pgtype.Text{String: msg.Content, Valid: true}, + }) + if err != nil { + return nil, status.Error(codes.Internal, "update message error") + } + + return &empty.Empty{}, nil +} + +func (m MessengerServer) DeleteMessage(ctx context.Context, msgID *mess.MsgID) (*empty.Empty, error) { + userID := ctx.Value(auth.KeyCurrentUserID).(int) + + err := m.messageCase.DeleteMessage(ctx, userID, int(msgID.GetId())) + if err != nil { + m.log.Error(err.Error()) + return nil, status.Error(codes.Internal, "delete message") + } + + return &empty.Empty{}, nil +} + +func (m MessengerServer) GetMessage(ctx context.Context, msgID *mess.MsgID) (*mess.Message, error) { + msg, err := m.messageCase.GetMessage(ctx, int(msgID.GetId())) + if err != nil { + m.log.Error(err.Error()) + return nil, status.Error(codes.Internal, "get message") + } + + return &mess.Message{ + Id: msgID, + UserFrom: int64(msg.From), + UserTo: int64(msg.To), + Content: msg.Content.String, + }, nil +} diff --git a/internal/microservices/messenger/usecase/message/check.go b/internal/microservices/messenger/usecase/message/check.go new file mode 100644 index 0000000..136082f --- /dev/null +++ b/internal/microservices/messenger/usecase/message/check.go @@ -0,0 +1,17 @@ +package message + +import ( + "context" + "fmt" +) + +func (m *messageCase) isAvailableForChanges(ctx context.Context, userID, mesID int) (bool, error) { + mes, err := m.repo.GetMessageByID(ctx, mesID) + if err != nil { + return false, fmt.Errorf("get message for check available: %w", err) + } + if mes.From == userID { + return true, nil + } + return false, nil +} diff --git a/internal/microservices/messenger/usecase/message/mock/message_mock.go b/internal/microservices/messenger/usecase/message/mock/message_mock.go new file mode 100644 index 0000000..65e877a --- /dev/null +++ b/internal/microservices/messenger/usecase/message/mock/message_mock.go @@ -0,0 +1,126 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + message "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + gomock "github.com/golang/mock/gomock" +) + +// MockUsecase is a mock of Usecase interface. +type MockUsecase struct { + ctrl *gomock.Controller + recorder *MockUsecaseMockRecorder +} + +// MockUsecaseMockRecorder is the mock recorder for MockUsecase. +type MockUsecaseMockRecorder struct { + mock *MockUsecase +} + +// NewMockUsecase creates a new mock instance. +func NewMockUsecase(ctrl *gomock.Controller) *MockUsecase { + mock := &MockUsecase{ctrl: ctrl} + mock.recorder = &MockUsecaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { + return m.recorder +} + +// DeleteMessage mocks base method. +func (m *MockUsecase) DeleteMessage(ctx context.Context, userID, mesID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteMessage", ctx, userID, mesID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteMessage indicates an expected call of DeleteMessage. +func (mr *MockUsecaseMockRecorder) DeleteMessage(ctx, userID, mesID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockUsecase)(nil).DeleteMessage), ctx, userID, mesID) +} + +// GetMessage mocks base method. +func (m *MockUsecase) GetMessage(ctx context.Context, messageID int) (*message.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMessage", ctx, messageID) + ret0, _ := ret[0].(*message.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMessage indicates an expected call of GetMessage. +func (mr *MockUsecaseMockRecorder) GetMessage(ctx, messageID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockUsecase)(nil).GetMessage), ctx, messageID) +} + +// GetMessagesFromChat mocks base method. +func (m *MockUsecase) GetMessagesFromChat(ctx context.Context, chat message.Chat, count, lastID int) ([]message.Message, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMessagesFromChat", ctx, chat, count, lastID) + ret0, _ := ret[0].([]message.Message) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetMessagesFromChat indicates an expected call of GetMessagesFromChat. +func (mr *MockUsecaseMockRecorder) GetMessagesFromChat(ctx, chat, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessagesFromChat", reflect.TypeOf((*MockUsecase)(nil).GetMessagesFromChat), ctx, chat, count, lastID) +} + +// GetUserChatsWithOtherUsers mocks base method. +func (m *MockUsecase) GetUserChatsWithOtherUsers(ctx context.Context, userID, count, lastID int) (message.FeedUserChats, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserChatsWithOtherUsers", ctx, userID, count, lastID) + ret0, _ := ret[0].(message.FeedUserChats) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetUserChatsWithOtherUsers indicates an expected call of GetUserChatsWithOtherUsers. +func (mr *MockUsecaseMockRecorder) GetUserChatsWithOtherUsers(ctx, userID, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserChatsWithOtherUsers", reflect.TypeOf((*MockUsecase)(nil).GetUserChatsWithOtherUsers), ctx, userID, count, lastID) +} + +// SendMessage mocks base method. +func (m *MockUsecase) SendMessage(ctx context.Context, mes *message.Message) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendMessage", ctx, mes) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SendMessage indicates an expected call of SendMessage. +func (mr *MockUsecaseMockRecorder) SendMessage(ctx, mes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockUsecase)(nil).SendMessage), ctx, mes) +} + +// UpdateContentMessage mocks base method. +func (m *MockUsecase) UpdateContentMessage(ctx context.Context, userID int, mes *message.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateContentMessage", ctx, userID, mes) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateContentMessage indicates an expected call of UpdateContentMessage. +func (mr *MockUsecaseMockRecorder) UpdateContentMessage(ctx, userID, mes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContentMessage", reflect.TypeOf((*MockUsecase)(nil).UpdateContentMessage), ctx, userID, mes) +} diff --git a/internal/microservices/messenger/usecase/message/usecase.go b/internal/microservices/messenger/usecase/message/usecase.go new file mode 100644 index 0000000..d68c3b1 --- /dev/null +++ b/internal/microservices/messenger/usecase/message/usecase.go @@ -0,0 +1,80 @@ +package message + +import ( + "context" + "errors" + "fmt" + + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + mesRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/message" +) + +var ErrNoAccess = errors.New("there is no access to perform this action") + +//go:generate mockgen -destination=./mock/message_mock.go -package=mock -source=usecase.go Usecase +type Usecase interface { + GetUserChatsWithOtherUsers(ctx context.Context, userID, count, lastID int) (entity.FeedUserChats, int, error) + SendMessage(ctx context.Context, mes *entity.Message) (int, error) + GetMessagesFromChat(ctx context.Context, chat entity.Chat, count, lastID int) (feed []entity.Message, newLastID int, err error) + UpdateContentMessage(ctx context.Context, userID int, mes *entity.Message) error + DeleteMessage(ctx context.Context, userID, mesID int) error + GetMessage(ctx context.Context, messageID int) (*entity.Message, error) +} + +type messageCase struct { + repo mesRepo.Repository +} + +func New(repo mesRepo.Repository) *messageCase { + return &messageCase{repo} +} + +func (m *messageCase) SendMessage(ctx context.Context, mes *entity.Message) (int, error) { + return m.repo.AddNewMessage(ctx, mes) +} + +func (m *messageCase) GetMessagesFromChat(ctx context.Context, chat entity.Chat, count, lastID int) (feed []entity.Message, newLastID int, err error) { + feed, err = m.repo.GetMessages(ctx, chat, count, lastID) + if err != nil { + err = fmt.Errorf("get message: %w", err) + } + if len(feed) != 0 { + newLastID = feed[len(feed)-1].ID + } + return +} + +func (m *messageCase) UpdateContentMessage(ctx context.Context, userID int, mes *entity.Message) error { + if ok, err := m.isAvailableForChanges(ctx, userID, mes.ID); err != nil { + return fmt.Errorf("update message: %w", err) + } else if !ok { + return ErrNoAccess + } + return m.repo.UpdateContentMessage(ctx, mes.ID, mes.Content.String) +} + +func (m *messageCase) DeleteMessage(ctx context.Context, userID, mesID int) error { + if ok, err := m.isAvailableForChanges(ctx, userID, mesID); err != nil { + return fmt.Errorf("delete message: %w", err) + } else if !ok { + return ErrNoAccess + } + return m.repo.DelMessage(ctx, mesID) +} + +func (m *messageCase) GetMessage(ctx context.Context, messageID int) (*entity.Message, error) { + return m.repo.GetMessageByID(ctx, messageID) +} + +func (m *messageCase) GetUserChatsWithOtherUsers(ctx context.Context, userID, count, lastID int) (entity.FeedUserChats, int, error) { + var newLastID int + feed, err := m.repo.GetUserChats(ctx, userID, count, lastID) + if len(feed) != 0 { + newLastID = feed[len(feed)-1].MessageLastID + } + + if err != nil { + return feed, newLastID, fmt.Errorf("get user chats with other users: %w", err) + } + return feed, newLastID, nil +} diff --git a/internal/microservices/realtime/kafka_broker.go b/internal/microservices/realtime/kafka_broker.go new file mode 100644 index 0000000..d9922b0 --- /dev/null +++ b/internal/microservices/realtime/kafka_broker.go @@ -0,0 +1,163 @@ +package realtime + +import ( + "errors" + "fmt" + "hash" + "hash/fnv" + "sync" + "time" + + "github.com/IBM/sarama" +) + +var ErrAlreadyMaxNumberTopic = errors.New("the maximum number has already been created topics") + +const _timeoutCreateTopic = time.Second + +type KafkaConfig struct { + Addres []string + PartitionsOnTopic int + MaxNumTopic int +} + +type kafkaBroker struct { + node *Node + mainClient sarama.Client + existingTopics map[string]struct{} + cfg KafkaConfig + producer sarama.SyncProducer + + m sync.RWMutex +} + +var _ Broker = (*kafkaBroker)(nil) + +func NewKafkaBroker(node *Node, cfg KafkaConfig) (*kafkaBroker, error) { + clientCfg := sarama.NewConfig() + clientCfg.Producer.Return.Successes = true + clientCfg.Producer.Partitioner = sarama.NewCustomHashPartitioner(func() hash.Hash32 { return fnv.New32() }) + + client, err := sarama.NewClient(cfg.Addres, clientCfg) + if err != nil { + return nil, fmt.Errorf("new client for kafka broker: %w", err) + } + producer, err := sarama.NewSyncProducerFromClient(client) + if err != nil { + return nil, fmt.Errorf("new sync producer for kafka broker: %w", err) + } + + k := &kafkaBroker{ + node: node, + cfg: cfg, + mainClient: client, + producer: producer, + m: sync.RWMutex{}, + existingTopics: make(map[string]struct{}), + } + return k, nil +} + +func (k *kafkaBroker) Publish(topic string, channel string, message []byte) error { + created, err := k.checkOrCreateTopic(topic) + if err != nil { + return fmt.Errorf("publish to topic %s: %w", topic, err) + } + + if created { + err = k.serveTopic(topic) + if err != nil { + return fmt.Errorf("serve new topic %s: %w", topic, err) + } + } + + _, _, err = k.producer.SendMessage(&sarama.ProducerMessage{ + Topic: topic, + Key: sarama.ByteEncoder(channel), + Value: sarama.ByteEncoder(message), + }) + if err != nil { + return fmt.Errorf("send message with topic %s and channel %s to kafka server: %w", topic, channel, err) + } + return nil +} + +func (k *kafkaBroker) Close() { + k.producer.Close() + k.mainClient.Close() +} + +func (k *kafkaBroker) checkOrCreateTopic(topic string) (bool, error) { + isExists := false + var countTopics int + k.m.RLock() + if _, ok := k.existingTopics[topic]; ok { + isExists = true + } else { + countTopics = len(k.existingTopics) + } + k.m.RUnlock() + + if isExists { + return false, nil + } + if countTopics == k.cfg.MaxNumTopic { + return false, ErrAlreadyMaxNumberTopic + } + + detail := &sarama.TopicDetail{ + NumPartitions: int32(k.cfg.PartitionsOnTopic), + ReplicationFactor: -1, + } + _, err := k.mainClient.LeastLoadedBroker().CreateTopics(&sarama.CreateTopicsRequest{ + ValidateOnly: true, + Timeout: _timeoutCreateTopic, + TopicDetails: map[string]*sarama.TopicDetail{topic: detail}, + }) + if err != nil { + return false, fmt.Errorf("create topic: %w", err) + } + + k.m.Lock() + if _, ok := k.existingTopics[topic]; !ok && len(k.existingTopics) == k.cfg.MaxNumTopic { + k.m.Unlock() + go delTopic(topic, k.mainClient) + return false, ErrAlreadyMaxNumberTopic + } + k.existingTopics[topic] = struct{}{} + k.m.Unlock() + + k.mainClient.RefreshController() + return true, nil +} + +func delTopic(topic string, client sarama.Client) { + client.LeastLoadedBroker().DeleteTopics(&sarama.DeleteTopicsRequest{ + Topics: []string{topic}, + Timeout: _timeoutCreateTopic, + }) +} + +func (k *kafkaBroker) serveTopic(topic string) error { + cons, err := sarama.NewConsumer(k.cfg.Addres, sarama.NewConfig()) + if err != nil { + return fmt.Errorf("serve topic %s: %w", topic, err) + } + + for i := int32(0); int(i) < k.node.numWorkers; i++ { + go func(partition int32) { + offset, err := k.mainClient.GetOffset(topic, partition, -1) + if err != nil { + return + } + partConsumer, err := cons.ConsumePartition(topic, int32(partition), offset) + if err != nil { + return + } + for message := range partConsumer.Messages() { + k.node.SendOut(Channel{Name: string(message.Key), Topic: topic}, message.Value) + } + }(i) + } + return nil +} diff --git a/internal/microservices/realtime/node.go b/internal/microservices/realtime/node.go new file mode 100644 index 0000000..53d64cc --- /dev/null +++ b/internal/microservices/realtime/node.go @@ -0,0 +1,81 @@ +package realtime + +import ( + "fmt" + "os" + "sync" + + "google.golang.org/protobuf/proto" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" +) + +type Node struct { + subscriptions [_numWorkers]SubscriberHub + broker Broker + numWorkers int + mu sync.RWMutex +} + +func NewNode() (*Node, error) { + node := &Node{} + broker, err := NewKafkaBroker(node, KafkaConfig{ + // Addres: []string{"localhost:9092"}, + Addres: []string{os.Getenv("KAFKA_BROKER_ADDRESS") + ":" + os.Getenv("KAFKA_BROKER_PORT")}, + PartitionsOnTopic: _numWorkers, + MaxNumTopic: 10, + }) + if err != nil { + return nil, fmt.Errorf("new kafka broker: %w", err) + } + + node.broker = broker + + for ind := range node.subscriptions { + node.subscriptions[ind] = make(SubscriberHub) + } + + node.numWorkers = _numWorkers + node.mu = sync.RWMutex{} + return node, nil +} + +func (n *Node) SendOut(channel Channel, message []byte) { + clients := []*Client{} + ind := index(channel.Name) + + n.mu.RLock() + if m, ok := n.subscriptions[ind][channel]; ok { + for _, client := range m { + clients = append(clients, client) + } + } + n.mu.RUnlock() + + for _, client := range clients { + mes := &rt.Message{} + err := proto.Unmarshal(message, mes) + if err != nil { + fmt.Println(err) + } + client.transport.Send(mes) + } +} + +func (n *Node) AddSubscriber(c *rt.Channel, client *Client) { + channel := Channel{ + Name: c.GetName(), + Topic: c.GetTopic(), + } + + ind := index(channel.Name) + + n.mu.Lock() + defer n.mu.Unlock() + subscribeChannel, ok := n.subscriptions[ind][channel] + if !ok { + subscribeChannel = make(map[string]*Client) + n.subscriptions[ind][channel] = subscribeChannel + } + subscribeChannel[client.id.String()] = client +} diff --git a/internal/microservices/realtime/server.go b/internal/microservices/realtime/server.go new file mode 100644 index 0000000..0574eef --- /dev/null +++ b/internal/microservices/realtime/server.go @@ -0,0 +1,61 @@ +package realtime + +import ( + "context" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" + "github.com/golang/protobuf/ptypes/empty" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +const _numWorkers = 64 + +type Server struct { + rt.UnimplementedRealTimeServer + + node *Node +} + +func NewServer(node *Node) Server { + return Server{ + UnimplementedRealTimeServer: rt.UnimplementedRealTimeServer{}, + node: node, + } +} + +type Broker interface { + Publish(topic string, channel string, message []byte) error + Close() +} + +func (s Server) Publish(ctx context.Context, pm *rt.PublishMessage) (*empty.Empty, error) { + message, err := proto.Marshal(pm.Message) + if err != nil { + return nil, status.Error(codes.Internal, "marshaling message") + } + if err := s.node.broker.Publish(pm.Channel.GetTopic(), pm.Channel.GetName(), message); err != nil { + return nil, status.Error(codes.Internal, "publish message") + } + return &empty.Empty{}, nil +} + +func (s Server) Subscribe(chans *rt.Channels, ss rt.RealTime_SubscribeServer) error { + id, err := uuid.NewRandom() + if err != nil { + return status.Error(codes.Internal, "generate uuid v4") + } + client := &Client{ + id: id, + transport: ss, + } + + for _, ch := range chans.GetChans() { + s.node.AddSubscriber(ch, client) + } + + <-ss.Context().Done() + return nil +} diff --git a/internal/microservices/realtime/types.go b/internal/microservices/realtime/types.go new file mode 100644 index 0000000..d0239e3 --- /dev/null +++ b/internal/microservices/realtime/types.go @@ -0,0 +1,27 @@ +package realtime + +import ( + "hash/fnv" + + "github.com/google/uuid" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" +) + +type Channel struct { + Name string + Topic string +} + +type SubscriberHub map[Channel]map[string]*Client + +type Client struct { + id uuid.UUID + transport rt.RealTime_SubscribeServer +} + +func index(nameChannel string) uint32 { + h := fnv.New32() + h.Write([]byte(nameChannel)) + return h.Sum32() % _numWorkers +} diff --git a/internal/pkg/delivery/http/v1/auth.go b/internal/pkg/delivery/http/v1/auth.go index 3fa680a..0c0149a 100644 --- a/internal/pkg/delivery/http/v1/auth.go +++ b/internal/pkg/delivery/http/v1/auth.go @@ -1,14 +1,15 @@ package v1 import ( - "encoding/json" "net/http" "time" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/session" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" usecase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" + "github.com/mailru/easyjson" ) // Login godoc @@ -55,8 +56,8 @@ func (h *HandlerHTTP) CheckLogin(w http.ResponseWriter, r *http.Request) { func (h *HandlerHTTP) Login(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) - params := usecase.UserCredentials{} - err := json.NewDecoder(r.Body).Decode(¶ms) + params := &usecase.UserCredentials{} + err := easyjson.UnmarshalFromReader(r.Body, params) defer r.Body.Close() if err != nil { logger.Info("failed to parse parameters", log.F{"error", err.Error()}) @@ -76,17 +77,7 @@ func (h *HandlerHTTP) Login(w http.ResponseWriter, r *http.Request) { return } - user, err := h.userCase.Authentication(r.Context(), params) - if err != nil { - logger.Warn(err.Error()) - err = responseError(w, "bad_credentials", "incorrect user credentials") - if err != nil { - logger.Error(err.Error()) - } - return - } - - session, err := h.sm.CreateNewSessionForUser(r.Context(), user.ID) + session, err := h.authCase.Login(r.Context(), params.Username, params.Password) if err != nil { logger.Error(err.Error()) err = responseError(w, "session", "failed to create a session for the user") @@ -131,7 +122,7 @@ func (h *HandlerHTTP) Signup(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) user := &user.User{} - err := json.NewDecoder(r.Body).Decode(user) + err := easyjson.UnmarshalFromReader(r.Body, user) defer r.Body.Close() if err != nil { logger.Info("failed to parse parameters", log.F{"error", err.Error()}) @@ -151,7 +142,7 @@ func (h *HandlerHTTP) Signup(w http.ResponseWriter, r *http.Request) { return } - err = h.userCase.Register(r.Context(), user) + err = h.authCase.Register(r.Context(), user) if err != nil { logger.Warn(err.Error()) err = responseError(w, "uniq_fields", "there is already an account with this username or email") @@ -178,6 +169,7 @@ func (h *HandlerHTTP) Signup(w http.ResponseWriter, r *http.Request) { // @Router /api/v1/auth/logout [delete] func (h *HandlerHTTP) Logout(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) + userID := r.Context().Value(auth.KeyCurrentUserID).(int) cookie, err := r.Cookie("session_key") if err != nil { @@ -193,7 +185,11 @@ func (h *HandlerHTTP) Logout(w http.ResponseWriter, r *http.Request) { cookie.Path = "/" http.SetCookie(w, cookie) - err = h.sm.DeleteUserSession(r.Context(), cookie.Value) + err = h.authCase.Logout(r.Context(), &session.Session{ + Key: cookie.Value, + UserID: userID, + Expire: cookie.Expires, + }) if err != nil { logger.Error(err.Error()) err = responseError(w, "session", "the user logged out, but his session did not end") diff --git a/internal/pkg/delivery/http/v1/auth_test.go b/internal/pkg/delivery/http/v1/auth_test.go index 29949ea..95c6744 100644 --- a/internal/pkg/delivery/http/v1/auth_test.go +++ b/internal/pkg/delivery/http/v1/auth_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/ramrepo" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/session" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" @@ -45,7 +46,7 @@ func TestCheckLogin(t *testing.T) { badCases := []struct { name string cookie *http.Cookie - expResp JsonErrResponse + expResp structs.JsonErrResponse }{ { "sending empty cookie", @@ -53,7 +54,7 @@ func TestCheckLogin(t *testing.T) { Name: "", Value: "", }, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "no user was found for this session", Code: "no_auth", @@ -65,7 +66,7 @@ func TestCheckLogin(t *testing.T) { Name: "session_key", Value: "doesn't exist", }, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "no user was found for this session", Code: "no_auth", @@ -77,7 +78,7 @@ func TestCheckLogin(t *testing.T) { Name: "session_key", Value: "f4280a941b664d02", }, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "no user was found for this session", Code: "no_auth", @@ -93,7 +94,7 @@ func TestCheckLogin(t *testing.T) { service.CheckLogin(w, req) - var actualResp JsonErrResponse + var actualResp structs.JsonErrResponse json.NewDecoder(w.Result().Body).Decode(&actualResp) require.Equal(t, tCase.expResp, actualResp) }) @@ -116,12 +117,12 @@ func testLogin(t *testing.T) { goodCases := []struct { name string rawBody string - expResp JsonResponse + expResp structs.JsonResponse }{ { "providing correct and valid user credentials", `{"username":"dogsLover", "password":"big_string"}`, - JsonResponse{ + structs.JsonResponse{ Status: "ok", Message: "a new session has been created for the user", Body: nil, @@ -136,7 +137,7 @@ func testLogin(t *testing.T) { service.Login(w, req) - var actualResp JsonResponse + var actualResp structs.JsonResponse json.NewDecoder(w.Result().Body).Decode(&actualResp) require.Equal(t, tCase.expResp, actualResp) require.True(t, checkAuthCookie(w.Result().Cookies())) @@ -146,12 +147,12 @@ func testLogin(t *testing.T) { badCases := []struct { name string rawBody string - expResp JsonErrResponse + expResp structs.JsonErrResponse }{ { "providing invalid credentials - broken body", "{'username': 'dogsLover', 'password': 'big_string'", - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "the correct username and password are expected to be received in JSON format", Code: "parse_body", @@ -160,7 +161,7 @@ func testLogin(t *testing.T) { { "providing invalid credentials - no username", `{"password":"big_string"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "invalid user credentials", Code: "invalid_credentials", @@ -169,7 +170,7 @@ func testLogin(t *testing.T) { { "providing invalid credentials - no password", `{"username":"dogsLover"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "invalid user credentials", Code: "invalid_credentials", @@ -178,7 +179,7 @@ func testLogin(t *testing.T) { { "providing invalid credentials - short username", `{"username":"do", "password":"big_string"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "invalid user credentials", Code: "invalid_credentials", @@ -187,7 +188,7 @@ func testLogin(t *testing.T) { { "providing invalid credentials - long username", `{"username":"dojsbrjfbdrjhbhjldrbgbdrhjgbdjrbgjdhbgjhdbrghbdhj,gbdhjrbgjhdbvkvghkevfghjdvrfhvdhrvbjdfgdrgdr","password":"big_string"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "invalid user credentials", Code: "invalid_credentials", @@ -196,7 +197,7 @@ func testLogin(t *testing.T) { { "providing invalid credentials - short password", `{"username":"dogsLover","password":"bi"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "invalid user credentials", Code: "invalid_credentials", @@ -205,7 +206,7 @@ func testLogin(t *testing.T) { { "providing invalid credentials - long password", `{"username":"dogsLover","password":"biyugsgrusgubskhvfhkdgvfgvdvrjgbsjhgjkshzkljfskfwjkhkfjisuidgoquakflsjuzeofiow3i"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "invalid user credentials", Code: "invalid_credentials", @@ -214,7 +215,7 @@ func testLogin(t *testing.T) { { "providing incorrect credentials - no user with such credentials", `{"username":"dogsLover", "password":"doesn't_exist"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "incorrect user credentials", Code: "bad_credentials", @@ -229,7 +230,7 @@ func testLogin(t *testing.T) { service.Login(w, req) - var actualResp JsonErrResponse + var actualResp structs.JsonErrResponse json.NewDecoder(w.Result().Body).Decode(&actualResp) require.Equal(t, tCase.expResp, actualResp) require.False(t, checkAuthCookie(w.Result().Cookies())) @@ -252,12 +253,12 @@ func testSignUp(t *testing.T) { goodCases := []struct { name string rawBody string - expResp JsonResponse + expResp structs.JsonResponse }{ { "providing correct and valid data for signup", `{"username":"newbie", "password":"getHigh123", "email":"world@uandex.ru"}`, - JsonResponse{ + structs.JsonResponse{ Status: "ok", Message: "the user has been successfully registered", Body: nil, @@ -272,7 +273,7 @@ func testSignUp(t *testing.T) { service.Signup(w, req) - var actualResp JsonResponse + var actualResp structs.JsonResponse json.NewDecoder(w.Result().Body).Decode(&actualResp) require.Equal(t, tCase.expResp, actualResp) }) @@ -281,12 +282,12 @@ func testSignUp(t *testing.T) { badCases := []struct { name string rawBody string - expResp JsonErrResponse + expResp structs.JsonErrResponse }{ { "user with such data already exists", `{"username":"dogsLover", "password":"big_string", "email":"dogslove@gmail.com"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "there is already an account with this username or email", Code: "uniq_fields", @@ -295,7 +296,7 @@ func testSignUp(t *testing.T) { { "invalid data - broken body", `{"username":"dogsLover", "password":"big_string", "email":"dogslove@gmail.com"`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "the correct username, email and password are expected to be received in JSON format", Code: "parse_body", @@ -304,7 +305,7 @@ func testSignUp(t *testing.T) { { "invalid data - no username", `{"password":"big_string", "email":"dogslove@gmail.com"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "username", Code: "invalid_params", @@ -313,7 +314,7 @@ func testSignUp(t *testing.T) { { "invalid data - no username, password", `{"email":"dogslove@gmail.com"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "password,username", Code: "invalid_params", @@ -322,7 +323,7 @@ func testSignUp(t *testing.T) { { "invalid data - short username", `{"username":"sh", "password":"big_string", "email":"dogslove@gmail.com"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "username", Code: "invalid_params", @@ -331,7 +332,7 @@ func testSignUp(t *testing.T) { { "invalid data - incorrect email", `{"username":"sh", "password":"big_string", "email":"dog"}`, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "email,username", Code: "invalid_params", @@ -346,7 +347,7 @@ func testSignUp(t *testing.T) { service.Signup(w, req) - var actualResp JsonErrResponse + var actualResp structs.JsonErrResponse json.NewDecoder(w.Result().Body).Decode(&actualResp) require.Equal(t, tCase.expResp, actualResp) }) @@ -368,7 +369,7 @@ func testLogout(t *testing.T) { goodCases := []struct { name string cookie *http.Cookie - expResp JsonResponse + expResp structs.JsonResponse }{ { "user is logged in - providing valid cookie", @@ -376,7 +377,7 @@ func testLogout(t *testing.T) { Name: "session_key", Value: "461afabf38b3147c", }, - JsonResponse{ + structs.JsonResponse{ Status: "ok", Message: "the user has successfully logged out", Body: nil, @@ -392,7 +393,7 @@ func testLogout(t *testing.T) { service.Logout(w, req) - var actualResp JsonResponse + var actualResp structs.JsonResponse json.NewDecoder(w.Result().Body).Decode(&actualResp) require.Equal(t, tCase.expResp, actualResp) }) @@ -401,7 +402,7 @@ func testLogout(t *testing.T) { badCases := []struct { name string cookie *http.Cookie - expResp JsonErrResponse + expResp structs.JsonErrResponse }{ { "user isn't logged in - providing invalid cookie", @@ -409,7 +410,7 @@ func testLogout(t *testing.T) { Name: "not_auth_cookie", Value: "blablalba", }, - JsonErrResponse{ + structs.JsonErrResponse{ Status: "error", Message: "to log out, you must first log in", Code: "no_auth", @@ -425,7 +426,7 @@ func testLogout(t *testing.T) { service.Logout(w, req) - var actualResp JsonErrResponse + var actualResp structs.JsonErrResponse json.NewDecoder(w.Result().Body).Decode(&actualResp) require.Equal(t, tCase.expResp, actualResp) }) diff --git a/internal/pkg/delivery/http/v1/board.go b/internal/pkg/delivery/http/v1/board.go index f4e5f4d..ba7e9e3 100644 --- a/internal/pkg/delivery/http/v1/board.go +++ b/internal/pkg/delivery/http/v1/board.go @@ -2,134 +2,35 @@ package v1 import ( "encoding/json" - "errors" - "fmt" "net/http" "strconv" "github.com/go-chi/chi/v5" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/board" + "github.com/mailru/easyjson" + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" - bCase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/board" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) var TimeFormat = "2006-01-02" -var ( - ErrEmptyTitle = errors.New("empty or null board title has been provided") - ErrEmptyPubOpt = errors.New("null public option has been provided") - ErrInvalidBoardTitle = errors.New("invalid or empty board title has been provided") - ErrInvalidTagTitles = errors.New("invalid tag titles have been provided") - ErrInvalidUsername = errors.New("invalid username has been provided") -) - -var ( - wrappedErrors = map[error]string{ErrInvalidTagTitles: "bad_Tagtitles"} - errCodeCompability = map[error]string{ - ErrInvalidBoardTitle: "bad_boardTitle", - ErrEmptyTitle: "empty_boardTitle", - ErrEmptyPubOpt: "bad_pubOpt", - ErrInvalidUsername: "bad_username", - bCase.ErrInvalidUsername: "non_existingUser", - bCase.ErrNoSuchBoard: "no_board", - bCase.ErrNoAccess: "no_access", - } -) - -// data for board creation/update -type BoardData struct { - Title *string `json:"title" example:"new board"` - Description *string `json:"description" example:"long desc"` - Public *bool `json:"public" example:"true"` - Tags []string `json:"tags" example:"['blue', 'car']"` -} - -// board view for delivery layer -type CertainBoard struct { - ID int `json:"board_id" example:"22"` - Title string `json:"title" example:"new board"` - Description string `json:"description" example:"long desc"` - CreatedAt string `json:"created_at" example:"07-11-2023"` - PinsNumber int `json:"pins_number" example:"12"` - Pins []string `json:"pins" example:"['/pic1', '/pic2']"` - Tags []string `json:"tags" example:"['love', 'green']"` -} - -func ToCertainBoardFromService(board entity.BoardWithContent) CertainBoard { - return CertainBoard{ - ID: board.BoardInfo.ID, - Title: board.BoardInfo.Title, - Description: board.BoardInfo.Description, - CreatedAt: board.BoardInfo.CreatedAt.Format(TimeFormat), - PinsNumber: board.PinsNumber, - Pins: board.Pins, - Tags: board.TagTitles, - } -} - -func (data *BoardData) Validate() error { - if data.Title == nil || *data.Title == "" { - return ErrInvalidBoardTitle - } - if data.Description == nil { - data.Description = new(string) - *data.Description = "" - } - if data.Public == nil { - return ErrEmptyPubOpt - } - if !isValidBoardTitle(*data.Title) { - return ErrInvalidBoardTitle - } - if err := checkIsValidTagTitles(data.Tags); err != nil { - return fmt.Errorf("%s: %w", err.Error(), ErrInvalidTagTitles) - } - return nil -} - -func getErrCodeMessage(err error) (string, string) { - var ( - code string - general, specific bool - ) - - code, general = generalErrCodeCompability[err] - if general { - return code, err.Error() - } - - code, specific = errCodeCompability[err] - if !specific { - for wrappedErr, code_ := range wrappedErrors { - if errors.Is(err, wrappedErr) { - specific = true - code = code_ - } - } - } - if specific { - return code, err.Error() - } - - return ErrInternalError.Error(), generalErrCodeCompability[ErrInternalError] -} - func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) - if contentType := w.Header().Get("Content-Type"); contentType != ApplicationJson { - code, message := getErrCodeMessage(ErrBadContentType) + if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadContentType) responseError(w, code, message) return } - var newBoard BoardData - err := json.NewDecoder(r.Body).Decode(&newBoard) + var newBoard structs.BoardData + err := easyjson.UnmarshalFromReader(r.Body, &newBoard) defer r.Body.Close() if err != nil { logger.Info("create board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadBody) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadBody) responseError(w, code, message) return } @@ -137,7 +38,7 @@ func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { err = newBoard.Validate() if err != nil { logger.Info("create board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -145,7 +46,6 @@ func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { tagTitles := make([]string, 0) if newBoard.Tags != nil { tagTitles = append(tagTitles, newBoard.Tags...) - } authorID := r.Context().Value(auth.KeyCurrentUserID).(int) @@ -158,7 +58,7 @@ func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Info("create board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -167,7 +67,7 @@ func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -176,8 +76,8 @@ func (h *HandlerHTTP) GetUserBoards(w http.ResponseWriter, r *http.Request) { username := chi.URLParam(r, "username") if !isValidUsername(username) { - logger.Info("update board", log.F{"message", ErrInvalidUsername.Error()}) - code, message := getErrCodeMessage(ErrInvalidUsername) + logger.Info("update board", log.F{"message", errHTTP.ErrInvalidUsername.Error()}) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrInvalidUsername) responseError(w, code, message) return } @@ -185,20 +85,20 @@ func (h *HandlerHTTP) GetUserBoards(w http.ResponseWriter, r *http.Request) { boards, err := h.boardCase.GetBoardsByUsername(r.Context(), username) if err != nil { logger.Info("get user boards", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } - userBoards := make([]CertainBoard, 0, len(boards)) + userBoards := make([]structs.CertainBoard, 0, len(boards)) for _, board := range boards { - userBoards = append(userBoards, ToCertainBoardFromService(board)) + userBoards = append(userBoards, h.converter.ToCertainBoardFromService(&board)) } err = responseOk(http.StatusOK, w, "got user boards successfully", userBoards) if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -208,24 +108,24 @@ func (h *HandlerHTTP) GetCertainBoard(w http.ResponseWriter, r *http.Request) { boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("get certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } - board, err := h.boardCase.GetCertainBoard(r.Context(), int(boardID)) + board, username, err := h.boardCase.GetCertainBoard(r.Context(), int(boardID)) if err != nil { logger.Info("get certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } - err = responseOk(http.StatusOK, w, "got certain board successfully", ToCertainBoardFromService(board)) + err = responseOk(http.StatusOK, w, "got certain board successfully", h.converter.ToCertainBoardUsernameFromService(&board, username)) if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -235,31 +135,31 @@ func (h *HandlerHTTP) GetBoardInfoForUpdate(w http.ResponseWriter, r *http.Reque boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("get certain board info for update", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } board, tagTitles, err := h.boardCase.GetBoardInfoForUpdate(r.Context(), int(boardID)) if err != nil { - logger.Info("get certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + logger.Info("get certain board info for update", log.F{"message", err.Error()}) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } - err = responseOk(http.StatusOK, w, "got certain board successfully", map[string]interface{}{"board": board, "tags": tagTitles}) + err = responseOk(http.StatusOK, w, "got certain board successfully", map[string]interface{}{"board": h.converter.ToBoardFromService(&board), "tags": tagTitles}) if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) - if contentType := w.Header().Get("Content-Type"); contentType != ApplicationJson { - code, message := getErrCodeMessage(ErrBadContentType) + if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadContentType) responseError(w, code, message) return } @@ -267,17 +167,17 @@ func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } - var updatedData BoardData - err = json.NewDecoder(r.Body).Decode(&updatedData) + var updatedData structs.BoardData + err = easyjson.UnmarshalFromReader(r.Body, &updatedData) defer r.Body.Close() if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadBody) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadBody) responseError(w, code, message) return } @@ -285,7 +185,7 @@ func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { err = updatedData.Validate() if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -304,7 +204,7 @@ func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { err = h.boardCase.UpdateBoardInfo(r.Context(), updatedBoard, tagTitles) if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -313,7 +213,7 @@ func (h *HandlerHTTP) UpdateBoardInfo(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -323,7 +223,7 @@ func (h *HandlerHTTP) DeleteBoard(w http.ResponseWriter, r *http.Request) { boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } @@ -331,7 +231,7 @@ func (h *HandlerHTTP) DeleteBoard(w http.ResponseWriter, r *http.Request) { err = h.boardCase.DeleteCertainBoard(r.Context(), int(boardID)) if err != nil { logger.Info("update certain board", log.F{"message", err.Error()}) - code, message := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -340,7 +240,7 @@ func (h *HandlerHTTP) DeleteBoard(w http.ResponseWriter, r *http.Request) { if err != nil { logger.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(ErrInternalError.Error())) + w.Write([]byte(errHTTP.ErrInternalError.Error())) } } @@ -401,3 +301,45 @@ func (h *HandlerHTTP) AddPinsToBoard(w http.ResponseWriter, r *http.Request) { logger.Error(err.Error()) } } + +func (h *HandlerHTTP) DeletePinFromBoard(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + + boardID, err := strconv.ParseInt(chi.URLParam(r, "boardID"), 10, 64) + if err != nil { + logger.Info("delete pin from board", log.F{"message", err.Error()}) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) + responseError(w, code, message) + return + } + + if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadContentType) + responseError(w, code, message) + return + } + + delPinFromBoard := structs.DeletePinFromBoard{} + err = easyjson.UnmarshalFromReader(r.Body, &delPinFromBoard) + defer r.Body.Close() + if err != nil { + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadBody) + responseError(w, code, message) + return + } + + err = h.boardCase.DeletePinFromBoard(r.Context(), int(boardID), delPinFromBoard.PinID) + if err != nil { + logger.Info("delete pin from board", log.F{"message", err.Error()}) + code, message := errHTTP.GetErrCodeMessage(err) + responseError(w, code, message) + return + } + + err = responseOk(http.StatusOK, w, "deleted pin from board successfully", nil) + if err != nil { + logger.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(errHTTP.ErrInternalError.Error())) + } +} diff --git a/internal/pkg/delivery/http/v1/board_validation.go b/internal/pkg/delivery/http/v1/board_validation.go deleted file mode 100644 index 0cabf18..0000000 --- a/internal/pkg/delivery/http/v1/board_validation.go +++ /dev/null @@ -1,48 +0,0 @@ -package v1 - -import ( - "fmt" - "unicode" -) - -func isValidTagTitle(title string) bool { - if len(title) > 20 { - return false - } - - for _, sym := range title { - if !(unicode.IsNumber(sym) || unicode.IsLetter(sym) || unicode.IsPunct(sym) || unicode.IsSpace(sym)) { - return false - } - } - return true -} - -func checkIsValidTagTitles(titles []string) error { - if len(titles) > 7 { - return fmt.Errorf("too many titles") - } - - invalidTitles := make([]string, 0) - for _, title := range titles { - if !isValidTagTitle(title) { - invalidTitles = append(invalidTitles, title) - } - } - if len(invalidTitles) > 0 { - return fmt.Errorf("%v", invalidTitles) - } - return nil -} - -func isValidBoardTitle(title string) bool { - if len(title) == 0 || len(title) > 40 { - return false - } - for _, sym := range title { - if !(unicode.IsNumber(sym) || unicode.IsLetter(sym) || unicode.IsPunct(sym) || unicode.IsSpace(sym)) { - return false - } - } - return true -} diff --git a/internal/pkg/delivery/http/v1/chat.go b/internal/pkg/delivery/http/v1/chat.go new file mode 100644 index 0000000..bf7b229 --- /dev/null +++ b/internal/pkg/delivery/http/v1/chat.go @@ -0,0 +1,179 @@ +package v1 + +import ( + "net/http" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" +) + +func (h *HandlerHTTP) FeedChats(w http.ResponseWriter, r *http.Request) { + log := h.getRequestLogger(r) + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + + count, lastID, err := FetchValidParamForLoadFeed(r.URL) + if err != nil { + log.Info(err.Error()) + err = responseError(w, "parse_url", "bad request url for getting feed chat") + if err != nil { + log.Error(err.Error()) + } + return + } + + chats, newLastID, err := h.messageCase.GetUserChatsWithOtherUsers(r.Context(), userID, count, lastID) + if err != nil { + log.Errorf(err.Error()) + } + err = responseOk(http.StatusOK, w, "success get feed user chats", map[string]any{ + "chats": chats, + "lastID": newLastID, + }) + if err != nil { + log.Fatal(err.Error()) + } +} + +func (h *HandlerHTTP) SendMessageToUser(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + + fromUserID := r.Context().Value(auth.KeyCurrentUserID).(int) + toUserID, err := fetchURLParamInt(r, "userID") + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "parse_url", "could not extract to whom the message is being sent") + if err != nil { + logger.Error(err.Error()) + } + return + } + + mes := &message.Message{} + err = decodeBody(r, mes) + defer r.Body.Close() + if err != nil { + logger.Info(err.Error()) + err = responseError(w, "parse_body", "invalid request body") + if err != nil { + logger.Error(err.Error()) + } + return + } + mes.From = fromUserID + mes.To = toUserID + + idNewMessage, err := h.messageCase.SendMessage(r.Context(), userID, mes) + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "send_message", "failed to send message") + } else { + err = responseOk(http.StatusCreated, w, "the message was sent successfully", + map[string]int{"id": idNewMessage}) + } + if err != nil { + logger.Error(err.Error()) + } +} + +func (h *HandlerHTTP) DeleteMessage(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + messageID, err := fetchURLParamInt(r, "messageID") + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "parse_url", "could not extract to whom the message is being sent") + if err != nil { + logger.Error(err.Error()) + } + return + } + + err = h.messageCase.DeleteMessage(r.Context(), userID, &message.Message{ID: messageID}) + if err != nil { + logger.Warn(err.Error()) + err = responseError(w, "delete_message", "fail deleting a message") + } else { + err = responseOk(http.StatusOK, w, "the message was successfully deleted", nil) + } + if err != nil { + logger.Error(err.Error()) + } +} + +func (h *HandlerHTTP) UpdateMessage(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + messageID, err := fetchURLParamInt(r, "messageID") + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "parse_url", "could not extract to whom the message is being sent") + if err != nil { + logger.Error(err.Error()) + } + return + } + + mes := &message.Message{} + err = decodeBody(r, mes) + defer r.Body.Close() + if err != nil { + logger.Info(err.Error()) + err = responseError(w, "parse_body", "invalid request body") + if err != nil { + logger.Error(err.Error()) + } + return + } + mes.ID = messageID + + err = h.messageCase.UpdateContentMessage(r.Context(), userID, mes) + if err != nil { + logger.Warn(err.Error()) + err = responseError(w, "update_message", "fail updating a message") + } else { + err = responseOk(http.StatusOK, w, "the message was successfully updated", nil) + } + if err != nil { + logger.Error(err.Error()) + } +} + +func (h *HandlerHTTP) GetMessagesFromChat(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + userID2, err := fetchURLParamInt(r, "userID") + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "parse_url", "could not extract to whom the message is being sent") + if err != nil { + logger.Error(err.Error()) + } + return + } + + count, lastID, err := FetchValidParamForLoadFeed(r.URL) + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "bad_request", "failed to get parameters for receiving a message from a chat") + if err != nil { + logger.Error(err.Error()) + } + return + } + + feed, newLastID, err := h.messageCase.GetMessagesFromChat(r.Context(), userID, message.Chat{userID, userID2}, count, lastID) + if err != nil { + logger.Warn(err.Error()) + } + err = responseOk(http.StatusOK, w, "messages received successfully", map[string]any{ + "messages": h.converter.ToMessagesFromService(feed), + "lastID": newLastID, + }) + if err != nil { + logger.Error(err.Error()) + } +} diff --git a/internal/pkg/delivery/http/v1/comment.go b/internal/pkg/delivery/http/v1/comment.go new file mode 100644 index 0000000..3f0e25f --- /dev/null +++ b/internal/pkg/delivery/http/v1/comment.go @@ -0,0 +1,118 @@ +package v1 + +import ( + "net/http" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" + "github.com/mailru/easyjson" +) + +func (h *HandlerHTTP) WriteComment(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + + pinID, err := fetchURLParamInt(r, "pinID") + if err != nil { + err = responseError(w, "parse_url", "the request url could not be get pin id") + if err != nil { + logger.Error(err.Error()) + return + } + } + + comment := &comment.Comment{} + err = easyjson.UnmarshalFromReader(r.Body, comment) + defer r.Body.Close() + if err != nil { + logger.Warn(err.Error()) + + err = responseError(w, "parse_body", "the request body could not be parsed to send a comment") + if err != nil { + logger.Error(err.Error()) + return + } + } + + comment.PinID = pinID + _, err = h.commentCase.PutCommentOnPin(r.Context(), userID, comment) + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "create_comment", "couldn't leave a comment under the selected pin") + } else { + err = responseOk(http.StatusCreated, w, "the comment has been added successfully", nil) + } + if err != nil { + logger.Error(err.Error()) + } +} + +func (h *HandlerHTTP) DeleteComment(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + + commentID, err := fetchURLParamInt(r, "commentID") + if err != nil { + err = responseError(w, "parse_url", "the request url could not be get pin id") + if err != nil { + logger.Error(err.Error()) + return + } + } + + err = h.commentCase.DeleteComment(r.Context(), userID, commentID) + if err != nil { + logger.Error(err.Error()) + err = responseError(w, "delete_comment", "couldn't delete pin comment") + } else { + err = responseOk(http.StatusOK, w, "the comment was successfully deleted", nil) + } + if err != nil { + logger.Error(err.Error()) + } +} + +func (h *HandlerHTTP) ViewFeedComment(w http.ResponseWriter, r *http.Request) { + logger := h.getRequestLogger(r) + userID, ok := r.Context().Value(auth.KeyCurrentUserID).(int) + if !ok { + userID = user.UserUnknown + } + + pinID, err := fetchURLParamInt(r, "pinID") + if err != nil { + err = responseError(w, "parse_url", "the request url could not be get pin id") + if err != nil { + logger.Error(err.Error()) + return + } + } + + count, lastID, err := FetchValidParamForLoadFeed(r.URL) + if err != nil { + err = responseError(w, "query_param", "the parameters for displaying the pin feed could not be extracted from the request") + if err != nil { + logger.Error(err.Error()) + return + } + } + + feed, newLastID, err := h.commentCase.GetFeedCommentOnPin(r.Context(), userID, pinID, count, lastID) + if err != nil && len(feed) == 0 { + err = responseError(w, "feed_view", "error displaying pin comments") + if err != nil { + logger.Error(err.Error()) + } + return + } + + if err != nil { + logger.Error(err.Error()) + } + + err = responseOk(http.StatusOK, w, "feed comment to pin", map[string]any{"comments": h.converter.ToCommentsFromService(feed), "lastID": newLastID}) + if err != nil { + logger.Error(err.Error()) + } +} diff --git a/internal/pkg/delivery/http/v1/converter.go b/internal/pkg/delivery/http/v1/converter.go new file mode 100644 index 0000000..c5c9346 --- /dev/null +++ b/internal/pkg/delivery/http/v1/converter.go @@ -0,0 +1,136 @@ +package v1 + +import ( + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/board" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" +) + +type converterHTTP struct { + sanitizer validation.SanitizerXSS + censor validation.ProfanityCensor +} + +func NewConverterHTTP(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) converterHTTP { + return converterHTTP{sanitizer, censor} +} + +func (c *converterHTTP) ToCertainBoardFromService(board *entity.BoardWithContent) structs.CertainBoard { + b := structs.CertainBoard{ + ID: board.BoardInfo.ID, + AuthorID: board.BoardInfo.AuthorID, + Title: board.BoardInfo.Title, + Description: board.BoardInfo.Description, + CreatedAt: board.BoardInfo.CreatedAt.Format(TimeFormat), + PinsNumber: board.PinsNumber, + Pins: board.Pins, + Tags: board.TagTitles, + } + b.Sanitize(c.sanitizer, c.censor) + return b +} + +func (c *converterHTTP) ToCertainBoardUsernameFromService(board *entity.BoardWithContent, username string) structs.CertainBoardWithUsername { + b := structs.CertainBoardWithUsername{ + ID: board.BoardInfo.ID, + AuthorID: board.BoardInfo.AuthorID, + AuthorUsername: username, + Title: board.BoardInfo.Title, + Description: board.BoardInfo.Description, + CreatedAt: board.BoardInfo.CreatedAt.Format(TimeFormat), + PinsNumber: board.PinsNumber, + Pins: board.Pins, + Tags: board.TagTitles, + } + b.Sanitize(c.sanitizer, c.censor) + return b +} + +func (c *converterHTTP) ToBoardFromService(board *entity.Board) *entity.Board { + board.Sanitize(c.sanitizer, c.censor) + return board +} + +func (c *converterHTTP) ToUsersForSearchFromService(users []search.UserForSearch) []search.UserForSearch { + for id := range users { + users[id].Sanitize(c.sanitizer, c.censor) + } + return users +} + +func (c *converterHTTP) ToBoardsForSearchFromService(boards []search.BoardForSearch) []search.BoardForSearch { + for id := range boards { + boards[id].Sanitize(c.sanitizer, c.censor) + } + return boards +} + +func (c *converterHTTP) ToPinsForSearchFromService(pins []search.PinForSearch) []search.PinForSearch { + for id := range pins { + pins[id].Sanitize(c.sanitizer, c.censor) + } + return pins +} + +func (c *converterHTTP) ToSubscriptionUsersFromService(users []user.SubscriptionUser) []user.SubscriptionUser { + for id := range users { + users[id].Sanitize(c.sanitizer, c.censor) + } + return users +} + +func (c *converterHTTP) ToUserInfoFromService(user *userEntity.User, isSubscribed bool, subsCount int) structs.UserInfo { + u := structs.UserInfo{ + ID: user.ID, + Username: user.Username, + Avatar: user.Avatar, + Name: user.Name.String, + Surname: user.Surname.String, + About: user.AboutMe.String, + IsSubscribed: isSubscribed, + SubsCount: subsCount, + } + u.Sanitize(c.sanitizer, c.censor) + return u +} + +func (c *converterHTTP) ToProfileInfoFromService(user *userEntity.User, subsCount int) structs.ProfileInfo { + p := structs.ProfileInfo{ + ID: user.ID, + Username: user.Username, + Avatar: user.Avatar, + SubsCount: subsCount, + } + p.Sanitize(c.sanitizer, c.censor) + return p +} + +func (c *converterHTTP) ToUserFromService(user *userEntity.User) *userEntity.User { + user.Sanitize(c.sanitizer, c.censor) + return user +} + +func (c *converterHTTP) ToPinFromService(pin *pin.Pin) *pin.Pin { + pin.Sanitize(c.sanitizer, c.censor) + return pin +} + +func (c *converterHTTP) ToMessagesFromService(mes []message.Message) []message.Message { + for id := range mes { + mes[id].Sanitize(c.sanitizer, c.censor) + } + return mes +} + +func (c *converterHTTP) ToCommentsFromService(comments []comment.Comment) []comment.Comment { + for id := range comments { + comments[id].Sanitize(c.sanitizer, c.censor) + } + return comments +} diff --git a/internal/pkg/delivery/http/v1/errors/board.go b/internal/pkg/delivery/http/v1/errors/board.go new file mode 100644 index 0000000..0d2106c --- /dev/null +++ b/internal/pkg/delivery/http/v1/errors/board.go @@ -0,0 +1,57 @@ +package errors + +import ( + "errors" + + bCase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/board" +) + +// for backward compatibility +var ( + ErrEmptyTitle = errors.New("empty or null board title has been provided") + ErrEmptyPubOpt = errors.New("null public option has been provided") + ErrInvalidBoardTitle = errors.New("invalid or empty board title has been provided") + ErrInvalidTagTitles = errors.New("invalid tag titles have been provided") + ErrInvalidUsername = errors.New("invalid username has been provided") +) + +var ( + WrappedErrors = map[error]string{ErrInvalidTagTitles: "bad_Tagtitles"} + ErrCodeCompability = map[error]string{ + ErrInvalidBoardTitle: "bad_boardTitle", + ErrEmptyTitle: "empty_boardTitle", + ErrEmptyPubOpt: "bad_pubOpt", + ErrInvalidUsername: "bad_username", + bCase.ErrInvalidUsername: "non_existingUser", + bCase.ErrNoSuchBoard: "no_board", + bCase.ErrNoAccess: "no_access", + bCase.ErrNoPinOnBoard: "no_pin", + } +) + +func GetErrCodeMessage(err error) (string, string) { + var ( + code string + general, specific bool + ) + + code, general = generalErrCodeCompability[err] + if general { + return code, err.Error() + } + + code, specific = ErrCodeCompability[err] + if !specific { + for wrappedErr, code_ := range WrappedErrors { + if errors.Is(err, wrappedErr) { + specific = true + code = code_ + } + } + } + if specific { + return code, err.Error() + } + + return ErrInternalError.Error(), generalErrCodeCompability[ErrInternalError] +} diff --git a/internal/pkg/delivery/http/v1/errors/general.go b/internal/pkg/delivery/http/v1/errors/general.go new file mode 100644 index 0000000..d60b0c3 --- /dev/null +++ b/internal/pkg/delivery/http/v1/errors/general.go @@ -0,0 +1,111 @@ +package errors + +import ( + "errors" + "fmt" + "net/http" + + errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" +) + +// for backward compatibility - begin +var ( + ErrBadBody = errors.New("can't parse body, JSON with correct data types is expected") + ErrBadUrlParam = errors.New("bad URL param has been provided") + ErrBadQueryParam = errors.New("invalid query parameters have been provided") + ErrInternalError = errors.New("internal server error occured") + ErrBadContentType = errors.New("application/json is expected") +) + +var ( + generalErrCodeCompability = map[error]string{ + ErrBadBody: "bad_body", + ErrBadQueryParam: "bad_queryParams", + ErrInternalError: "internal_error", + ErrBadContentType: "bad_contentType", + ErrBadUrlParam: "bad_urlParam", + } +) + +// for backward compatibility - end + +type ErrInvalidBody struct{} + +func (e *ErrInvalidBody) Error() string { + return "invalid body" +} + +func (e *ErrInvalidBody) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrInvalidQueryParam struct { + Params map[string]string +} + +func (e *ErrInvalidQueryParam) Error() string { + return fmt.Sprintf("invalid query params: %v", e.Params) +} + +func (e *ErrInvalidQueryParam) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrInvalidContentType struct { + PreferredType string +} + +func (e *ErrInvalidContentType) Error() string { + return fmt.Sprintf("invalid content type, should be %s", e.PreferredType) +} + +func (e *ErrInvalidContentType) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrInvalidUrlParams struct { + Params map[string]string +} + +func (e *ErrInvalidUrlParams) Error() string { + return fmt.Sprintf("invalid URL params: %v", e.Params) +} + +func (e *ErrInvalidUrlParams) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrMissingBodyParams struct { + Params []string +} + +func (e *ErrMissingBodyParams) Error() string { + return fmt.Sprintf("missing body params: %v", e.Params) +} + +func (e *ErrMissingBodyParams) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +func GetCodeStatusHttp(err error) (ErrCode string, httpStatus int) { + + var declaredErr errPkg.DeclaredError + if errors.As(err, &declaredErr) { + switch declaredErr.Type() { + case errPkg.ErrInvalidInput: + return "bad_input", http.StatusBadRequest + case errPkg.ErrNotFound: + return "not_found", http.StatusNotFound + case errPkg.ErrAlreadyExists: + return "already_exists", http.StatusConflict + case errPkg.ErrNoAuth: + return "no_auth", http.StatusUnauthorized + case errPkg.ErrNoAccess: + return "no_access", http.StatusForbidden + case errPkg.ErrTimeout: + return "timeout", http.StatusRequestTimeout + } + } + + return "internal_error", http.StatusInternalServerError +} diff --git a/internal/pkg/delivery/http/v1/errors/search.go b/internal/pkg/delivery/http/v1/errors/search.go new file mode 100644 index 0000000..74abdec --- /dev/null +++ b/internal/pkg/delivery/http/v1/errors/search.go @@ -0,0 +1,23 @@ +package errors + +import errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" + +type ErrNoData struct{} + +func (e *ErrNoData) Error() string { + return "Can't find any user/board/pin" +} + +func (e *ErrNoData) Type() errPkg.Type { + return errPkg.ErrNotFound +} + +type ErrInvalidTemplate struct{} + +func (e *ErrInvalidTemplate) Error() string { + return "Invalid template has been provided" +} + +func (e *ErrInvalidTemplate) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} diff --git a/internal/pkg/delivery/http/v1/feed.go b/internal/pkg/delivery/http/v1/feed.go index 4a32690..02d71a1 100644 --- a/internal/pkg/delivery/http/v1/feed.go +++ b/internal/pkg/delivery/http/v1/feed.go @@ -8,8 +8,8 @@ import ( "strconv" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" - usecase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/pin" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) @@ -17,7 +17,7 @@ func (h *HandlerHTTP) FeedPins(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) userID, isAuth := r.Context().Value(auth.KeyCurrentUserID).(int) if !isAuth { - userID = usecase.UserUnknown + userID = user.UserUnknown } logger.Info("request on getting feed of pins", log.F{"rawQuery", r.URL.RawQuery}) diff --git a/internal/pkg/delivery/http/v1/handler.go b/internal/pkg/delivery/http/v1/handler.go index 2500fb1..ef7cf15 100644 --- a/internal/pkg/delivery/http/v1/handler.go +++ b/internal/pkg/delivery/http/v1/handler.go @@ -1,27 +1,52 @@ package v1 import ( + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/auth" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/board" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/message" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/pin" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/session" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/search" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/subscription" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) type HandlerHTTP struct { - log *logger.Logger - userCase user.Usecase - pinCase pin.Usecase - boardCase board.Usecase - sm session.SessionManager + log *logger.Logger + converter converterHTTP + authCase auth.Usecase + userCase user.Usecase + pinCase pin.Usecase + boardCase board.Usecase + subCase subscription.Usecase + searchCase search.Usecase + messageCase message.Usecase + commentCase comment.Usecase } -func New(log *logger.Logger, sm session.SessionManager, user user.Usecase, pin pin.Usecase, board board.Usecase) *HandlerHTTP { +func New(log *logger.Logger, converter converterHTTP, hub UsecaseHub) *HandlerHTTP { return &HandlerHTTP{ - log: log, - userCase: user, - pinCase: pin, - boardCase: board, - sm: sm, + log: log, + converter: converter, + authCase: hub.AuhtCase, + userCase: hub.UserCase, + pinCase: hub.PinCase, + boardCase: hub.BoardCase, + subCase: hub.SubscriptionCase, + searchCase: hub.SearchCase, + messageCase: hub.MessageCase, + commentCase: hub.CommentCase, } } + +type UsecaseHub struct { + AuhtCase auth.Usecase + UserCase user.Usecase + PinCase pin.Usecase + BoardCase board.Usecase + SubscriptionCase subscription.Usecase + SearchCase search.Usecase + MessageCase message.Usecase + CommentCase comment.Usecase +} diff --git a/internal/pkg/delivery/http/v1/parse.go b/internal/pkg/delivery/http/v1/parse.go new file mode 100644 index 0000000..d200898 --- /dev/null +++ b/internal/pkg/delivery/http/v1/parse.go @@ -0,0 +1,25 @@ +package v1 + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" +) + +func fetchURLParamInt(r *http.Request, param string) (int, error) { + paramInt64, err := strconv.ParseInt(chi.URLParam(r, param), 10, 64) + if err != nil { + return 0, fmt.Errorf("fetch integer param from query request %s: %w", r.URL.RawQuery, err) + } + return int(paramInt64), nil +} + +func decodeBody(r *http.Request, v any) error { + if err := json.NewDecoder(r.Body).Decode(v); err != nil { + return fmt.Errorf("decode body: %w", err) + } + return nil +} diff --git a/internal/pkg/delivery/http/v1/pin.go b/internal/pkg/delivery/http/v1/pin.go index be7c2c9..783f811 100644 --- a/internal/pkg/delivery/http/v1/pin.go +++ b/internal/pkg/delivery/http/v1/pin.go @@ -1,16 +1,17 @@ package v1 import ( - "encoding/json" "net/http" "strconv" "strings" chi "github.com/go-chi/chi/v5" + "github.com/mailru/easyjson" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" + img "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/image" usecase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/pin" ) @@ -73,7 +74,11 @@ func (h *HandlerHTTP) CreateNewPin(w http.ResponseWriter, r *http.Request) { err = h.pinCase.CreateNewPin(r.Context(), newPin, mime.Header.Get("Content-Type"), mime.Size, picture) if err != nil { logger.Error(err.Error()) - err = responseError(w, "add_pin", "failed to create pin") + if err == img.ErrExplicitImage { + err = responseError(w, "explicit_pin", err.Error()) + } else { + err = responseError(w, "add_pin", "failed to create pin") + } } else { err = responseOk(http.StatusCreated, w, "pin successfully created", nil) } @@ -129,8 +134,7 @@ func (h *HandlerHTTP) EditPin(w http.ResponseWriter, r *http.Request) { _, _ = userID, pinID pinUpdate := &usecase.PinUpdateData{} - - err = json.NewDecoder(r.Body).Decode(pinUpdate) + err = easyjson.UnmarshalFromReader(r.Body, pinUpdate) defer r.Body.Close() if err != nil { logger.Info(err.Error()) @@ -168,14 +172,14 @@ func (h *HandlerHTTP) ViewPin(w http.ResponseWriter, r *http.Request) { userID, ok := r.Context().Value(auth.KeyCurrentUserID).(int) if !ok { - userID = usecase.UserUnknown + userID = user.UserUnknown } pin, err := h.pinCase.ViewAnPin(r.Context(), int(pinID), userID) if err != nil { logger.Error(err.Error()) err = responseError(w, "edit_pin", "internal error") } else { - err = responseOk(http.StatusOK, w, "pin was successfully received", pin) + err = responseOk(http.StatusOK, w, "pin was successfully received", h.converter.ToPinFromService(pin)) } if err != nil { logger.Error(err.Error()) diff --git a/internal/pkg/delivery/http/v1/pin_test.go b/internal/pkg/delivery/http/v1/pin_test.go index 9f5bf20..358d6d2 100644 --- a/internal/pkg/delivery/http/v1/pin_test.go +++ b/internal/pkg/delivery/http/v1/pin_test.go @@ -9,6 +9,7 @@ import ( "strconv" "testing" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/ramrepo" pinCase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/pin" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" @@ -30,11 +31,11 @@ func TestGetPins(t *testing.T) { badCases := []struct { rawURL string - expResp JsonErrResponse + expResp structs.JsonErrResponse }{ { rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, 0, 3), - expResp: JsonErrResponse{ + expResp: structs.JsonErrResponse{ Status: "error", Message: "expected parameters: count(positive integer: [1; 1000]), maxID, minID(positive integers, the absence of these parameters is equal to the value 0)", Code: "bad_params", @@ -42,7 +43,7 @@ func TestGetPins(t *testing.T) { }, { rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, -2, 3), - expResp: JsonErrResponse{ + expResp: structs.JsonErrResponse{ Status: "error", Message: "expected parameters: count(positive integer: [1; 1000]), maxID, minID(positive integers, the absence of these parameters is equal to the value 0)", Code: "bad_params", @@ -50,7 +51,7 @@ func TestGetPins(t *testing.T) { }, { rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, 213123, 3), - expResp: JsonErrResponse{ + expResp: structs.JsonErrResponse{ Status: "error", Message: "expected parameters: count(positive integer: [1; 1000]), maxID, minID(positive integers, the absence of these parameters is equal to the value 0)", Code: "bad_params", @@ -58,7 +59,7 @@ func TestGetPins(t *testing.T) { }, { rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, 0, -1), - expResp: JsonErrResponse{ + expResp: structs.JsonErrResponse{ Status: "error", Message: "expected parameters: count(positive integer: [1; 1000]), maxID, minID(positive integers, the absence of these parameters is equal to the value 0)", Code: "bad_params", @@ -66,7 +67,7 @@ func TestGetPins(t *testing.T) { }, { rawURL: fmt.Sprintf("%s?count=&lastID=%d", rawUrl, 3), - expResp: JsonErrResponse{ + expResp: structs.JsonErrResponse{ Status: "error", Message: "expected parameters: count(positive integer: [1; 1000]), maxID, minID(positive integers, the absence of these parameters is equal to the value 0)", Code: "bad_params", @@ -74,7 +75,7 @@ func TestGetPins(t *testing.T) { }, { rawURL: fmt.Sprintf("%s?lastID=%d", rawUrl, 3), - expResp: JsonErrResponse{ + expResp: structs.JsonErrResponse{ Status: "error", Message: "expected parameters: count(positive integer: [1; 1000]), maxID, minID(positive integers, the absence of these parameters is equal to the value 0)", Code: "bad_params", @@ -90,7 +91,7 @@ func TestGetPins(t *testing.T) { resp := w.Result() body, _ := io.ReadAll(resp.Body) - var actualResp JsonErrResponse + var actualResp structs.JsonErrResponse json.Unmarshal(body, &actualResp) require.Equal(t, tCase.expResp, actualResp) diff --git a/internal/pkg/delivery/http/v1/profile.go b/internal/pkg/delivery/http/v1/profile.go index 4a58a3d..7e812f6 100644 --- a/internal/pkg/delivery/http/v1/profile.go +++ b/internal/pkg/delivery/http/v1/profile.go @@ -1,22 +1,48 @@ package v1 import ( - "encoding/json" "fmt" "net/http" + "strconv" + "github.com/go-chi/chi/v5" + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" + "github.com/mailru/easyjson" ) +func (h *HandlerHTTP) GetUserInfo(w http.ResponseWriter, r *http.Request) { + userIdParam := chi.URLParam(r, "userID") + userID, err := strconv.ParseInt(userIdParam, 10, 64) + if err != nil { + h.responseErr(w, r, &errHTTP.ErrInvalidUrlParams{Params: map[string]string{"userID": userIdParam}}) + return + } + + if user, isSubscribed, subsCount, err := h.userCase.GetUserInfo(r.Context(), int(userID)); err != nil { + h.responseErr(w, r, err) + } else if err := responseOk(http.StatusOK, w, "got user info successfully", h.converter.ToUserInfoFromService(user, isSubscribed, subsCount)); err != nil { + h.responseErr(w, r, err) + } +} + +func (h *HandlerHTTP) GetProfileHeaderInfo(w http.ResponseWriter, r *http.Request) { + if user, subsCount, err := h.userCase.GetProfileInfo(r.Context()); err != nil { + h.responseErr(w, r, err) + } else if err := responseOk(http.StatusOK, w, "got profile info successfully", h.converter.ToProfileInfoFromService(user, subsCount)); err != nil { + h.responseErr(w, r, err) + } +} + func (h *HandlerHTTP) ProfileEditInfo(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) userID := r.Context().Value(auth.KeyCurrentUserID).(int) data := &user.ProfileUpdateData{} - err := json.NewDecoder(r.Body).Decode(data) + err := easyjson.UnmarshalFromReader(r.Body, data) defer r.Body.Close() if err != nil { logger.Info("json decode: " + err.Error()) @@ -98,7 +124,7 @@ func (h *HandlerHTTP) GetProfileInfo(w http.ResponseWriter, r *http.Request) { logger.Error(err.Error()) err = responseError(w, "get_info", "failed to get user information") } else { - err = responseOk(http.StatusOK, w, "user data has been successfully received", user) + err = responseOk(http.StatusOK, w, "user data has been successfully received", h.converter.ToUserFromService(user)) } if err != nil { diff --git a/internal/pkg/delivery/http/v1/profile_test.go b/internal/pkg/delivery/http/v1/profile_test.go index d927e7f..5c87e00 100644 --- a/internal/pkg/delivery/http/v1/profile_test.go +++ b/internal/pkg/delivery/http/v1/profile_test.go @@ -11,6 +11,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/user/mock" @@ -47,11 +48,11 @@ func TestGetProfileInfo(t *testing.T) { res := rec.Result() defer res.Body.Close() - actualBody := JsonResponse{Body: &user.User{}} + actualBody := structs.JsonResponse{Body: &user.User{}} err = json.NewDecoder(res.Body).Decode(&actualBody) require.NoError(t, err) fmt.Println(actualBody.Body) - wantBody := JsonResponse{ + wantBody := structs.JsonResponse{ Status: "ok", Message: "user data has been successfully received", Body: &wantUser, diff --git a/internal/pkg/delivery/http/v1/response.go b/internal/pkg/delivery/http/v1/response.go index ba0b3bd..7b77059 100644 --- a/internal/pkg/delivery/http/v1/response.go +++ b/internal/pkg/delivery/http/v1/response.go @@ -1,53 +1,26 @@ package v1 import ( - "encoding/json" - "errors" "fmt" "net/http" -) -var ( - ErrBadBody = errors.New("can't parse body, JSON with correct data types is expected") - ErrBadUrlParam = errors.New("bad URL param has been provided") - ErrBadQueryParam = errors.New("invalid query parameters have been provided") - ErrInternalError = errors.New("internal server error occured") - ErrBadContentType = errors.New("application/json is expected") + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" + "github.com/mailru/easyjson" ) -var ( - generalErrCodeCompability = map[error]string{ - ErrBadBody: "bad_body", - ErrBadQueryParam: "bad_queryParams", - ErrInternalError: "internal_error", - ErrBadContentType: "bad_contentType", - ErrBadUrlParam: "bad_urlParam", - } -) - -type JsonResponse struct { - Status string `json:"status" example:"ok"` - Message string `json:"message" example:"Response message"` - Body interface{} `json:"body" extensions:"x-omitempty"` -} // @name JsonResponse - -type JsonErrResponse struct { - Status string `json:"status" example:"error"` - Message string `json:"message" example:"Error description"` - Code string `json:"code"` -} // @name JsonErrResponse - func SetContentTypeJSON(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") } func responseOk(statusCode int, w http.ResponseWriter, message string, body any) error { - res := JsonResponse{ + res := structs.JsonResponse{ Status: "ok", Message: message, Body: body, } - resBytes, err := json.Marshal(res) + resBytes, err := easyjson.Marshal(res) if err != nil { w.WriteHeader(http.StatusInternalServerError) return fmt.Errorf("responseOk: %w", err) @@ -59,15 +32,41 @@ func responseOk(statusCode int, w http.ResponseWriter, message string, body any) } func responseError(w http.ResponseWriter, code, message string) error { - res := JsonErrResponse{ + res := structs.JsonErrResponse{ Status: "error", Message: message, Code: code, } - resBytes, err := json.Marshal(res) + resBytes, err := easyjson.Marshal(res) + if err != nil { + return fmt.Errorf("responseError: %w", err) + } + _, err = w.Write(resBytes) + return err +} + +func (h *HandlerHTTP) responseErr(w http.ResponseWriter, r *http.Request, err error) error { + log := logger.GetLoggerFromCtx(r.Context()) + + code, status := errHTTP.GetCodeStatusHttp(err) + var msg string + if status == http.StatusInternalServerError { + log.Warnf("unexpected application error: %s", err.Error()) + msg = "internal error occured" + } else { + msg = err.Error() + } + + res := structs.JsonErrResponse{ + Status: "error", + Message: msg, + Code: code, + } + resBytes, err := easyjson.Marshal(res) if err != nil { return fmt.Errorf("responseError: %w", err) } + w.WriteHeader(status) _, err = w.Write(resBytes) return err } diff --git a/internal/pkg/delivery/http/v1/search.go b/internal/pkg/delivery/http/v1/search.go new file mode 100644 index 0000000..ae72332 --- /dev/null +++ b/internal/pkg/delivery/http/v1/search.go @@ -0,0 +1,162 @@ +package v1 + +import ( + "net/http" + "strconv" + + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" +) + +var ( + defaultSearchCount = 20 + maxSearchCount = 50 + defaultSearchOffset = 0 + maxSearchOffset = 50 +) + +var ( + userSortOpts = []string{"id", "subscribers"} + boardSortOpts = []string{"id", "pins"} + pinSortOpts = []string{"id", "likes"} +) + +var ( + defaultSearchSort = "id" + defaultSortOder = "desc" +) + +func (h *HandlerHTTP) SearchUsers(w http.ResponseWriter, r *http.Request) { + opts, err := GetSearchOpts(r, userSortOpts, defaultSearchSort) + if err != nil { + h.responseErr(w, r, err) + return + } + + if users, err := h.searchCase.GetUsers(r.Context(), opts); err != nil { + h.responseErr(w, r, err) + } else if err := responseOk(http.StatusOK, w, "got users sucessfully", h.converter.ToUsersForSearchFromService(users)); err != nil { + h.responseErr(w, r, err) + } +} + +func (h *HandlerHTTP) SearchBoards(w http.ResponseWriter, r *http.Request) { + opts, err := GetSearchOpts(r, boardSortOpts, defaultSearchSort) + if err != nil { + h.responseErr(w, r, err) + return + } + + if boards, err := h.searchCase.GetBoards(r.Context(), opts); err != nil { + h.responseErr(w, r, err) + } else if err := responseOk(http.StatusOK, w, "got boards sucessfully", h.converter.ToBoardsForSearchFromService(boards)); err != nil { + h.responseErr(w, r, err) + } +} + +func (h *HandlerHTTP) SearchPins(w http.ResponseWriter, r *http.Request) { + opts, err := GetSearchOpts(r, pinSortOpts, defaultSearchSort) + if err != nil { + h.responseErr(w, r, err) + return + } + + if pins, err := h.searchCase.GetPins(r.Context(), opts); err != nil { + h.responseErr(w, r, err) + } else if err := responseOk(http.StatusOK, w, "got pins sucessfully", h.converter.ToPinsForSearchFromService(pins)); err != nil { + h.responseErr(w, r, err) + } +} + +func GetSearchOpts(r *http.Request, sortOpts []string, defaultSortOpt string) (*search.SearchOpts, error) { + opts := &search.SearchOpts{} + invalidParams := map[string]string{} + + generalOpts, err := GetGeneralOpts(r, invalidParams) + if err != nil { + return nil, err + } + opts.General = *generalOpts + + if sortBy := r.URL.Query().Get("sortBy"); sortBy != "" { + if !isCorrentSortOpt(sortOpts, sortBy) { + invalidParams["sortBy"] = sortBy + } else { + opts.SortBy = sortBy + } + } else { + opts.SortBy = defaultSortOpt + } + + if len(invalidParams) > 0 { + return nil, &errHTTP.ErrInvalidQueryParam{Params: invalidParams} + } + + return opts, nil +} + +func isCorrentSortOpt(correctOpts []string, opt string) bool { + for _, correctOpt := range correctOpts { + if opt == correctOpt { + return true + } + } + return false +} + +func GetGeneralOpts(r *http.Request, invalidParams map[string]string) (*search.GeneralOpts, error) { + opts := &search.GeneralOpts{} + + if templateParam := r.URL.Query().Get("template"); templateParam != "" { + if template := search.Template(templateParam); !template.Validate() { + invalidParams["template"] = string(template) + } else { + opts.Template = template + } + } else { + return nil, &errHTTP.ErrNoData{} + } + + if sortOrder := r.URL.Query().Get("order"); sortOrder != "" { + if sortOrder != "asc" && sortOrder != "desc" { + invalidParams["order"] = sortOrder + } else { + opts.SortOrder = sortOrder + } + } else { + opts.SortOrder = defaultSortOder + } + + if countParam := r.URL.Query().Get("count"); countParam != "" { + if count, err := strconv.ParseInt(countParam, 10, 64); err != nil || count < 0 { + invalidParams["count"] = countParam + } else { + opts.Count = int(count) + } + } else { + opts.Count = defaultSearchCount + } + + if offsetParam := r.URL.Query().Get("offset"); offsetParam != "" { + if offset, err := strconv.ParseInt(offsetParam, 10, 64); err != nil || offset < 0 { + invalidParams["offset"] = offsetParam + } else { + opts.Offset = int(offset) + } + } else { + opts.Offset = defaultSearchOffset + } + + if opts.Count > maxSearchCount { + opts.Count = maxSearchCount + } + if opts.Offset > maxSearchOffset { + opts.Offset = maxSearchOffset + } + + userID, _ := r.Context().Value(auth.KeyCurrentUserID).(int) + opts.CurrUserID = userID + + return opts, nil +} diff --git a/internal/pkg/delivery/http/v1/structs/board.go b/internal/pkg/delivery/http/v1/structs/board.go new file mode 100644 index 0000000..4c04755 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/board.go @@ -0,0 +1,136 @@ +package structs + +import ( + "fmt" + "unicode" + + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" +) + +//go:generate easyjson board.go + +// data for board creation/update +// +//easyjson:json +type BoardData struct { + Title *string `json:"title" example:"new board"` + Description *string `json:"description" example:"long desc"` + Public *bool `json:"public" example:"true"` + Tags []string `json:"tags" example:"['blue', 'car']"` +} + +// board view for delivery layer +// +//easyjson:json +type CertainBoard struct { + ID int `json:"board_id" example:"22"` + AuthorID int `json:"author_id" example:"22"` + Title string `json:"title" example:"new board"` + Description string `json:"description" example:"long desc"` + CreatedAt string `json:"created_at" example:"07-11-2023"` + PinsNumber int `json:"pins_number" example:"12"` + Pins []string `json:"pins" example:"['/pic1', '/pic2']"` + Tags []string `json:"tags" example:"['love', 'green']"` +} + +//easyjson:json +type CertainBoardWithUsername struct { + ID int `json:"board_id" example:"22"` + AuthorID int `json:"author_id" example:"22"` + AuthorUsername string `json:"author_username" example:"Bob"` + Title string `json:"title" example:"new board"` + Description string `json:"description" example:"long desc"` + CreatedAt string `json:"created_at" example:"07-11-2023"` + PinsNumber int `json:"pins_number" example:"12"` + Pins []string `json:"pins" example:"['/pic1', '/pic2']"` + Tags []string `json:"tags" example:"['love', 'green']"` +} + +func (b *CertainBoard) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if b != nil { + b.Title = sanitizer.Sanitize(censor.Sanitize(b.Title)) + b.Description = sanitizer.Sanitize(censor.Sanitize(b.Description)) + for id, title := range b.Tags { + b.Tags[id] = sanitizer.Sanitize(censor.Sanitize(title)) + } + } +} + +func (b *CertainBoardWithUsername) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if b != nil { + b.Title = sanitizer.Sanitize(censor.Sanitize(b.Title)) + b.Description = sanitizer.Sanitize(censor.Sanitize(b.Description)) + b.AuthorUsername = sanitizer.Sanitize(censor.Sanitize(b.AuthorUsername)) + for id, title := range b.Tags { + b.Tags[id] = sanitizer.Sanitize(censor.Sanitize(title)) + } + } +} + +//easyjson:json +type DeletePinFromBoard struct { + PinID int `json:"pin_id" example:"22"` +} + +func (data *BoardData) Validate() error { + if data.Title == nil || *data.Title == "" { + return errHTTP.ErrInvalidBoardTitle + } + if data.Description == nil { + data.Description = new(string) + *data.Description = "" + } + if data.Public == nil { + return errHTTP.ErrEmptyPubOpt + } + if !isValidBoardTitle(*data.Title) { + return errHTTP.ErrInvalidBoardTitle + } + if err := checkIsValidTagTitles(data.Tags); err != nil { + return fmt.Errorf("%s: %w", err.Error(), errHTTP.ErrInvalidTagTitles) + } + return nil +} + +func isValidTagTitle(title string) bool { + if len(title) > 20 { + return false + } + + for _, sym := range title { + if !(unicode.IsNumber(sym) || unicode.IsLetter(sym) || unicode.IsPunct(sym) || unicode.IsSpace(sym)) { + return false + } + } + return true +} + +func checkIsValidTagTitles(titles []string) error { + if len(titles) > 7 { + return fmt.Errorf("too many titles") + } + + invalidTitles := make([]string, 0) + for _, title := range titles { + if !isValidTagTitle(title) { + invalidTitles = append(invalidTitles, title) + } + } + if len(invalidTitles) > 0 { + return fmt.Errorf("%v", invalidTitles) + } + return nil +} + +func isValidBoardTitle(title string) bool { + if len(title) == 0 || len(title) > 40 { + return false + } + for _, sym := range title { + if !(unicode.IsNumber(sym) || unicode.IsLetter(sym) || unicode.IsPunct(sym) || unicode.IsSpace(sym)) { + return false + } + } + return true +} diff --git a/internal/pkg/delivery/http/v1/structs/board_easyjson.go b/internal/pkg/delivery/http/v1/structs/board_easyjson.go new file mode 100644 index 0000000..b847d66 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/board_easyjson.go @@ -0,0 +1,605 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package structs + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(in *jlexer.Lexer, out *DeletePinFromBoard) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "pin_id": + out.PinID = int(in.Int()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(out *jwriter.Writer, in DeletePinFromBoard) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"pin_id\":" + out.RawString(prefix[1:]) + out.Int(int(in.PinID)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v DeletePinFromBoard) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v DeletePinFromBoard) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *DeletePinFromBoard) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *DeletePinFromBoard) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(l, v) +} +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(in *jlexer.Lexer, out *CertainBoardWithUsername) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "board_id": + out.ID = int(in.Int()) + case "author_id": + out.AuthorID = int(in.Int()) + case "author_username": + out.AuthorUsername = string(in.String()) + case "title": + out.Title = string(in.String()) + case "description": + out.Description = string(in.String()) + case "created_at": + out.CreatedAt = string(in.String()) + case "pins_number": + out.PinsNumber = int(in.Int()) + case "pins": + if in.IsNull() { + in.Skip() + out.Pins = nil + } else { + in.Delim('[') + if out.Pins == nil { + if !in.IsDelim(']') { + out.Pins = make([]string, 0, 4) + } else { + out.Pins = []string{} + } + } else { + out.Pins = (out.Pins)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.Pins = append(out.Pins, v1) + in.WantComma() + } + in.Delim(']') + } + case "tags": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]string, 0, 4) + } else { + out.Tags = []string{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v2 string + v2 = string(in.String()) + out.Tags = append(out.Tags, v2) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(out *jwriter.Writer, in CertainBoardWithUsername) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"board_id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"author_id\":" + out.RawString(prefix) + out.Int(int(in.AuthorID)) + } + { + const prefix string = ",\"author_username\":" + out.RawString(prefix) + out.String(string(in.AuthorUsername)) + } + { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + out.String(string(in.Description)) + } + { + const prefix string = ",\"created_at\":" + out.RawString(prefix) + out.String(string(in.CreatedAt)) + } + { + const prefix string = ",\"pins_number\":" + out.RawString(prefix) + out.Int(int(in.PinsNumber)) + } + { + const prefix string = ",\"pins\":" + out.RawString(prefix) + if in.Pins == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v3, v4 := range in.Pins { + if v3 > 0 { + out.RawByte(',') + } + out.String(string(v4)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v5, v6 := range in.Tags { + if v5 > 0 { + out.RawByte(',') + } + out.String(string(v6)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v CertainBoardWithUsername) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v CertainBoardWithUsername) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *CertainBoardWithUsername) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *CertainBoardWithUsername) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(l, v) +} +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(in *jlexer.Lexer, out *CertainBoard) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "board_id": + out.ID = int(in.Int()) + case "author_id": + out.AuthorID = int(in.Int()) + case "title": + out.Title = string(in.String()) + case "description": + out.Description = string(in.String()) + case "created_at": + out.CreatedAt = string(in.String()) + case "pins_number": + out.PinsNumber = int(in.Int()) + case "pins": + if in.IsNull() { + in.Skip() + out.Pins = nil + } else { + in.Delim('[') + if out.Pins == nil { + if !in.IsDelim(']') { + out.Pins = make([]string, 0, 4) + } else { + out.Pins = []string{} + } + } else { + out.Pins = (out.Pins)[:0] + } + for !in.IsDelim(']') { + var v7 string + v7 = string(in.String()) + out.Pins = append(out.Pins, v7) + in.WantComma() + } + in.Delim(']') + } + case "tags": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]string, 0, 4) + } else { + out.Tags = []string{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v8 string + v8 = string(in.String()) + out.Tags = append(out.Tags, v8) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(out *jwriter.Writer, in CertainBoard) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"board_id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"author_id\":" + out.RawString(prefix) + out.Int(int(in.AuthorID)) + } + { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + out.String(string(in.Description)) + } + { + const prefix string = ",\"created_at\":" + out.RawString(prefix) + out.String(string(in.CreatedAt)) + } + { + const prefix string = ",\"pins_number\":" + out.RawString(prefix) + out.Int(int(in.PinsNumber)) + } + { + const prefix string = ",\"pins\":" + out.RawString(prefix) + if in.Pins == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v9, v10 := range in.Pins { + if v9 > 0 { + out.RawByte(',') + } + out.String(string(v10)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v11, v12 := range in.Tags { + if v11 > 0 { + out.RawByte(',') + } + out.String(string(v12)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v CertainBoard) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v CertainBoard) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *CertainBoard) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *CertainBoard) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs2(l, v) +} +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(in *jlexer.Lexer, out *BoardData) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "title": + if in.IsNull() { + in.Skip() + out.Title = nil + } else { + if out.Title == nil { + out.Title = new(string) + } + *out.Title = string(in.String()) + } + case "description": + if in.IsNull() { + in.Skip() + out.Description = nil + } else { + if out.Description == nil { + out.Description = new(string) + } + *out.Description = string(in.String()) + } + case "public": + if in.IsNull() { + in.Skip() + out.Public = nil + } else { + if out.Public == nil { + out.Public = new(bool) + } + *out.Public = bool(in.Bool()) + } + case "tags": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]string, 0, 4) + } else { + out.Tags = []string{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v13 string + v13 = string(in.String()) + out.Tags = append(out.Tags, v13) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(out *jwriter.Writer, in BoardData) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"title\":" + out.RawString(prefix[1:]) + if in.Title == nil { + out.RawString("null") + } else { + out.String(string(*in.Title)) + } + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + if in.Description == nil { + out.RawString("null") + } else { + out.String(string(*in.Description)) + } + } + { + const prefix string = ",\"public\":" + out.RawString(prefix) + if in.Public == nil { + out.RawString("null") + } else { + out.Bool(bool(*in.Public)) + } + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v14, v15 := range in.Tags { + if v14 > 0 { + out.RawByte(',') + } + out.String(string(v15)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BoardData) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BoardData) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BoardData) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BoardData) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs3(l, v) +} diff --git a/internal/pkg/delivery/http/v1/structs/response.go b/internal/pkg/delivery/http/v1/structs/response.go new file mode 100644 index 0000000..2be893b --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/response.go @@ -0,0 +1,17 @@ +package structs + +//go:generate easyjson response.go + +//easyjson:json +type JsonResponse struct { + Status string `json:"status" example:"ok"` + Message string `json:"message" example:"Response message"` + Body interface{} `json:"body" extensions:"x-omitempty"` +} // @name JsonResponse + +//easyjson:json +type JsonErrResponse struct { + Status string `json:"status" example:"error"` + Message string `json:"message" example:"Error description"` + Code string `json:"code"` +} // @name JsonErrResponse diff --git a/internal/pkg/delivery/http/v1/structs/response_easyjson.go b/internal/pkg/delivery/http/v1/structs/response_easyjson.go new file mode 100644 index 0000000..91dfdd8 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/response_easyjson.go @@ -0,0 +1,191 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package structs + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(in *jlexer.Lexer, out *JsonResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "status": + out.Status = string(in.String()) + case "message": + out.Message = string(in.String()) + case "body": + if m, ok := out.Body.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := out.Body.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + out.Body = in.Interface() + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(out *jwriter.Writer, in JsonResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"status\":" + out.RawString(prefix[1:]) + out.String(string(in.Status)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + { + const prefix string = ",\"body\":" + out.RawString(prefix) + if m, ok := in.Body.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := in.Body.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(in.Body)) + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v JsonResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v JsonResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *JsonResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *JsonResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(l, v) +} +func easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(in *jlexer.Lexer, out *JsonErrResponse) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "status": + out.Status = string(in.String()) + case "message": + out.Message = string(in.String()) + case "code": + out.Code = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(out *jwriter.Writer, in JsonErrResponse) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"status\":" + out.RawString(prefix[1:]) + out.String(string(in.Status)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + { + const prefix string = ",\"code\":" + out.RawString(prefix) + out.String(string(in.Code)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v JsonErrResponse) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v JsonErrResponse) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6ff3ac1dEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *JsonErrResponse) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *JsonErrResponse) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6ff3ac1dDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(l, v) +} diff --git a/internal/pkg/delivery/http/v1/structs/subscription.go b/internal/pkg/delivery/http/v1/structs/subscription.go new file mode 100644 index 0000000..73bee4d --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/subscription.go @@ -0,0 +1,17 @@ +package structs + +import errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + +//go:generate easyjson subscription.go + +//easyjson:json +type SubscriptionAction struct { + To *int `json:"to" example:"2"` +} + +func (s *SubscriptionAction) Validate() error { + if s.To == nil { + return &errHTTP.ErrMissingBodyParams{Params: []string{"to"}} + } + return nil +} diff --git a/internal/pkg/delivery/http/v1/structs/subscription_easyjson.go b/internal/pkg/delivery/http/v1/structs/subscription_easyjson.go new file mode 100644 index 0000000..b1e0fd9 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/subscription_easyjson.go @@ -0,0 +1,97 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package structs + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonFfbd3743DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(in *jlexer.Lexer, out *SubscriptionAction) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "to": + if in.IsNull() { + in.Skip() + out.To = nil + } else { + if out.To == nil { + out.To = new(int) + } + *out.To = int(in.Int()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonFfbd3743EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(out *jwriter.Writer, in SubscriptionAction) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"to\":" + out.RawString(prefix[1:]) + if in.To == nil { + out.RawString("null") + } else { + out.Int(int(*in.To)) + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v SubscriptionAction) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonFfbd3743EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v SubscriptionAction) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonFfbd3743EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *SubscriptionAction) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonFfbd3743DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *SubscriptionAction) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonFfbd3743DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(l, v) +} diff --git a/internal/pkg/delivery/http/v1/structs/user.go b/internal/pkg/delivery/http/v1/structs/user.go new file mode 100644 index 0000000..ab7d8f8 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/user.go @@ -0,0 +1,42 @@ +package structs + +import ( + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" +) + +//go:generate easyjson user.go + +//easyjson:json +type UserInfo struct { + ID int `json:"id" example:"123"` + Username string `json:"username" example:"Snapshot"` + Avatar string `json:"avatar" example:"/pic1"` + Name string `json:"name" example:"Bob"` + Surname string `json:"surname" example:"Dylan"` + About string `json:"about" example:"Cool guy"` + IsSubscribed bool `json:"is_subscribed" example:"true"` + SubsCount int `json:"subscribers" example:"23"` +} + +func (u *UserInfo) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if u != nil { + u.Username = sanitizer.Sanitize(censor.Sanitize(u.Username)) + u.Name = sanitizer.Sanitize(censor.Sanitize(u.Name)) + u.Surname = sanitizer.Sanitize(censor.Sanitize(u.Surname)) + u.About = sanitizer.Sanitize(censor.Sanitize(u.About)) + } +} + +//easyjson:json +type ProfileInfo struct { + ID int `json:"id" example:"1"` + Username string `json:"username" example:"baobab"` + Avatar string `json:"avatar" example:"/pic1"` + SubsCount int `json:"subscribers" example:"12"` +} + +func (p *ProfileInfo) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if p != nil { + p.Username = sanitizer.Sanitize(censor.Sanitize(p.Username)) + } +} diff --git a/internal/pkg/delivery/http/v1/structs/user_easyjson.go b/internal/pkg/delivery/http/v1/structs/user_easyjson.go new file mode 100644 index 0000000..e62ee17 --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/user_easyjson.go @@ -0,0 +1,221 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package structs + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(in *jlexer.Lexer, out *UserInfo) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "username": + out.Username = string(in.String()) + case "avatar": + out.Avatar = string(in.String()) + case "name": + out.Name = string(in.String()) + case "surname": + out.Surname = string(in.String()) + case "about": + out.About = string(in.String()) + case "is_subscribed": + out.IsSubscribed = bool(in.Bool()) + case "subscribers": + out.SubsCount = int(in.Int()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(out *jwriter.Writer, in UserInfo) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"username\":" + out.RawString(prefix) + out.String(string(in.Username)) + } + { + const prefix string = ",\"avatar\":" + out.RawString(prefix) + out.String(string(in.Avatar)) + } + { + const prefix string = ",\"name\":" + out.RawString(prefix) + out.String(string(in.Name)) + } + { + const prefix string = ",\"surname\":" + out.RawString(prefix) + out.String(string(in.Surname)) + } + { + const prefix string = ",\"about\":" + out.RawString(prefix) + out.String(string(in.About)) + } + { + const prefix string = ",\"is_subscribed\":" + out.RawString(prefix) + out.Bool(bool(in.IsSubscribed)) + } + { + const prefix string = ",\"subscribers\":" + out.RawString(prefix) + out.Int(int(in.SubsCount)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v UserInfo) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v UserInfo) MarshalEasyJSON(w *jwriter.Writer) { + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *UserInfo) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *UserInfo) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs(l, v) +} +func easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(in *jlexer.Lexer, out *ProfileInfo) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "username": + out.Username = string(in.String()) + case "avatar": + out.Avatar = string(in.String()) + case "subscribers": + out.SubsCount = int(in.Int()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(out *jwriter.Writer, in ProfileInfo) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"username\":" + out.RawString(prefix) + out.String(string(in.Username)) + } + { + const prefix string = ",\"avatar\":" + out.RawString(prefix) + out.String(string(in.Avatar)) + } + { + const prefix string = ",\"subscribers\":" + out.RawString(prefix) + out.Int(int(in.SubsCount)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ProfileInfo) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ProfileInfo) MarshalEasyJSON(w *jwriter.Writer) { + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ProfileInfo) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ProfileInfo) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryHttpV1Structs1(l, v) +} diff --git a/internal/pkg/delivery/http/v1/subscription.go b/internal/pkg/delivery/http/v1/subscription.go new file mode 100644 index 0000000..7e18482 --- /dev/null +++ b/internal/pkg/delivery/http/v1/subscription.go @@ -0,0 +1,145 @@ +package v1 + +import ( + "net/http" + "strconv" + + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/structs" + userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" + "github.com/mailru/easyjson" +) + +var ( + defaultSubCount = 20 + defaultSubLastID = 1 << 30 + subscriptionsView = "subscriptions" + subscribersView = "subscribers" + maxCount = 50 +) + +func (h *HandlerHTTP) Subscribe(w http.ResponseWriter, r *http.Request) { + if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { + h.responseErr(w, r, &errHTTP.ErrInvalidContentType{PreferredType: ApplicationJson}) + return + } + + sub := structs.SubscriptionAction{} + + if err := easyjson.UnmarshalFromReader(r.Body, &sub); err != nil { + h.responseErr(w, r, &errHTTP.ErrInvalidBody{}) + return + } + defer r.Body.Close() + if err := sub.Validate(); err != nil { + h.responseErr(w, r, err) + return + } + + from := r.Context().Value(auth.KeyCurrentUserID).(int) + if err := h.subCase.SubscribeToUser(r.Context(), from, *sub.To); err != nil { + h.responseErr(w, r, err) + } else if err := responseOk(http.StatusOK, w, "subscribed successfully", nil); err != nil { + h.responseErr(w, r, err) + } + +} + +func (h *HandlerHTTP) Unsubscribe(w http.ResponseWriter, r *http.Request) { + if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { + h.responseErr(w, r, &errHTTP.ErrInvalidContentType{PreferredType: ApplicationJson}) + return + } + + sub := structs.SubscriptionAction{} + if err := easyjson.UnmarshalFromReader(r.Body, &sub); err != nil { + h.responseErr(w, r, &errHTTP.ErrInvalidBody{}) + return + } + defer r.Body.Close() + if err := sub.Validate(); err != nil { + h.responseErr(w, r, err) + return + } + + from := r.Context().Value(auth.KeyCurrentUserID).(int) + if err := h.subCase.UnsubscribeFromUser(r.Context(), from, *sub.To); err != nil { + h.responseErr(w, r, err) + } else if err := responseOk(http.StatusOK, w, "unsubscribed successfully", nil); err != nil { + h.responseErr(w, r, err) + } +} + +func (h *HandlerHTTP) GetSubscriptionInfoForUser(w http.ResponseWriter, r *http.Request) { + opts, err := GetOpts(r) + if err != nil { + h.responseErr(w, r, err) + return + } + + if users, err := h.subCase.GetSubscriptionInfoForUser(r.Context(), opts); err != nil { + h.responseErr(w, r, err) + } else if err := responseOk(http.StatusOK, w, "got subscription info successfully", h.converter.ToSubscriptionUsersFromService(users)); err != nil { + h.responseErr(w, r, err) + } +} + +func GetOpts(r *http.Request) (*userEntity.SubscriptionOpts, error) { + opts := &userEntity.SubscriptionOpts{} + invalidParams := map[string]string{} + + var ( + userID, count, lastID int64 + filter string + err error + ) + if userIdParam := r.URL.Query().Get("userID"); userIdParam != "" { + if userID, err = strconv.ParseInt(userIdParam, 10, 64); err != nil || userID < 0 { + invalidParams["userID"] = userIdParam + } else { + opts.UserID = int(userID) + } + } else { + opts.UserID, _ = r.Context().Value(auth.KeyCurrentUserID).(int) + } + + if countParam := r.URL.Query().Get("count"); countParam != "" { + if count, err = strconv.ParseInt(countParam, 10, 64); err != nil || count < 0 { + invalidParams["count"] = countParam + } else { + opts.Count = int(count) + } + } else { + opts.Count = defaultSubCount + } + + if lastIdParam := r.URL.Query().Get("lastID"); lastIdParam != "" { + if lastID, err = strconv.ParseInt(lastIdParam, 10, 64); err != nil || lastID < 0 { + invalidParams["lastID"] = lastIdParam + } else { + opts.LastID = int(lastID) + } + } else { + opts.LastID = defaultSubLastID + } + + if filter = r.URL.Query().Get("view"); filter != "" { + if filter != subscriptionsView && filter != subscribersView { + invalidParams["view"] = filter + } else { + opts.Filter = filter + } + } else { + invalidParams["view"] = filter + } + + if opts.Count > maxCount { + opts.Count = maxCount + } + if len(invalidParams) > 0 { + return nil, &errHTTP.ErrInvalidQueryParam{invalidParams} + } + return opts, nil +} diff --git a/internal/pkg/delivery/http/v1/validation.go b/internal/pkg/delivery/http/v1/validation.go index 5d8bfb5..9d73fce 100644 --- a/internal/pkg/delivery/http/v1/validation.go +++ b/internal/pkg/delivery/http/v1/validation.go @@ -35,32 +35,25 @@ func (b *errorFields) Err() error { return b } -func FetchValidParamForLoadTape(u *url.URL) (count int, minID int, maxID int, err error) { +func FetchValidParamForLoadFeed(u *url.URL) (count, lastID int, err error) { if param := u.Query().Get("count"); len(param) > 0 { c, err := strconv.ParseInt(param, 10, 64) if err != nil { - return 0, 0, 0, fmt.Errorf("fetch count param for load tape: %w", err) + return 0, 0, fmt.Errorf("fetch count param for load tape: %w", err) } count = int(c) } else { - return 0, 0, 0, ErrCountParameterMissing + return 0, 0, ErrCountParameterMissing } - if param := u.Query().Get("minID"); len(param) > 0 { + if param := u.Query().Get("lastID"); len(param) > 0 { id, err := strconv.ParseInt(param, 10, 64) if err != nil { - return 0, 0, 0, fmt.Errorf("fetch lastID param for load tape: %w", err) + return 0, 0, fmt.Errorf("fetch lastID param for load tape: %w", err) } - minID = int(id) + lastID = int(id) } - if param := u.Query().Get("maxID"); len(param) > 0 { - id, err := strconv.ParseInt(param, 10, 64) - if err != nil { - return 0, 0, 0, fmt.Errorf("fetch lastID param for load tape: %w", err) - } - maxID = int(id) - } - if count <= 0 || count > 1000 || minID < 0 || maxID < 0 { - return 0, 0, 0, ErrBadParams + if count <= 0 || count > 1000 || lastID < 0 { + return 0, 0, ErrBadParams } return } diff --git a/internal/pkg/delivery/websocket/chat.go b/internal/pkg/delivery/websocket/chat.go new file mode 100644 index 0000000..50b0f21 --- /dev/null +++ b/internal/pkg/delivery/websocket/chat.go @@ -0,0 +1,114 @@ +package websocket + +import ( + "context" + "fmt" + "net/http" + + ws "nhooyr.io/websocket" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" +) + +func (h *HandlerWebSocket) Chat(w http.ResponseWriter, r *http.Request) { + conn, err := h.upgradeWSConnect(w, r) + if err != nil { + h.log.Error(err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"status":"error","code":"websocket_connect","message":"fail connect"}`)) + return + } + defer conn.CloseNow() + + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + ctx, cancel := context.WithTimeout(context.Background(), _ctxOnServeConnect) + defer cancel() + + socket := newSocketJSON(conn) + + err = h.subscribeOnChat(ctx, socket, userID) + if err != nil { + h.log.Error(err.Error()) + conn.Close(ws.StatusInternalError, "subscribe_fail") + return + } + + err = h.serveChat(ctx, socket, userID) + if err != nil && ws.CloseStatus(err) == -1 { + h.log.Error(err.Error()) + conn.Close(ws.StatusInternalError, "serve_chat") + } +} + +func (h *HandlerWebSocket) serveChat(ctx context.Context, rw CtxReadWriter, userID int) error { + request := &PublishRequest{} + var err error + for { + err = rw.Read(ctx, request) + if err != nil { + h.log.Error(err.Error()) + return fmt.Errorf("read message: %w", err) + } + + h.handlePublishRequestMessage(ctx, rw, userID, request) + } +} + +func (h *HandlerWebSocket) handlePublishRequestMessage(ctx context.Context, w CtxWriter, userID int, req *PublishRequest) { + fmt.Println(req) + switch req.Message.Type { + case "create": + req.Message.Message.From = userID + id, err := h.messageCase.SendMessage(ctx, userID, &req.Message.Message) + if err != nil { + h.log.Warn(err.Error()) + return + } + w.Write(ctx, newResponseOnRequest(req.ID, "ok", "", "publish success", map[string]any{"id": id, "eventType": "create"})) + + case "update": + err := h.messageCase.UpdateContentMessage(ctx, userID, &req.Message.Message) + if err != nil { + h.log.Warn(err.Error()) + return + } + w.Write(ctx, newResponseOnRequest(req.ID, "ok", "", "publish success", map[string]string{"eventType": "update"})) + + case "delete": + err := h.messageCase.DeleteMessage(ctx, userID, &req.Message.Message) + if err != nil { + h.log.Warn(err.Error()) + return + } + w.Write(ctx, newResponseOnRequest(req.ID, "ok", "", "publish success", map[string]string{"eventType": "delete"})) + + default: + w.Write(ctx, newResponseOnRequest(req.ID, "error", "unsupported", "unsupported eventType", nil)) + } +} + +func (h *HandlerWebSocket) subscribeOnChat(ctx context.Context, w CtxWriter, userID int) error { + chanEvMsg, err := h.messageCase.SubscribeUserToAllChats(ctx, userID) + if err != nil { + return fmt.Errorf("subscribe user on chat: %w", err) + } + + go func() { + for eventMessage := range chanEvMsg { + if eventMessage.Err != nil { + h.log.Error(eventMessage.Err.Error()) + return + } + + err = w.Write(ctx, newMessageFromChannel("ok", "", Object{ + Type: eventMessage.Type, + Message: *eventMessage.Message, + })) + if err != nil { + h.log.Error(err.Error()) + return + } + } + }() + return nil +} diff --git a/internal/pkg/delivery/websocket/notification.go b/internal/pkg/delivery/websocket/notification.go new file mode 100644 index 0000000..a24852e --- /dev/null +++ b/internal/pkg/delivery/websocket/notification.go @@ -0,0 +1,54 @@ +package websocket + +import ( + "context" + "fmt" + "net/http" + + ws "nhooyr.io/websocket" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" +) + +func (h *HandlerWebSocket) Notification(w http.ResponseWriter, r *http.Request) { + conn, err := h.upgradeWSConnect(w, r) + if err != nil { + h.log.Error(err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"status":"error","code":"websocket_connect","message":"fail connect"}`)) + return + } + defer conn.CloseNow() + + userID := r.Context().Value(auth.KeyCurrentUserID).(int) + ctx, cancel := context.WithTimeout(context.Background(), _ctxOnServeConnect) + defer cancel() + + socket := newSocketJSON(conn) + + err = h.subscribeOnNotificationAndServe(ctx, socket, userID) + if err != nil && ws.CloseStatus(err) == -1 { + h.log.Error(err.Error()) + conn.Close(ws.StatusInternalError, "subscribe_fail") + } +} + +func (h *HandlerWebSocket) subscribeOnNotificationAndServe(ctx context.Context, w CtxWriter, userID int) error { + chanNotify, err := h.notifySub.SubscribeOnAllNotifications(ctx, userID) + if err != nil { + return fmt.Errorf("subscribe on Notification") + } + + for notify := range chanNotify { + if notify.Err() != nil { + return notify.Err() + } + + err = w.Write(ctx, notify) + if err != nil { + h.log.Error(err.Error()) + } + } + + return nil +} diff --git a/internal/pkg/delivery/websocket/socket.go b/internal/pkg/delivery/websocket/socket.go new file mode 100644 index 0000000..4269c6a --- /dev/null +++ b/internal/pkg/delivery/websocket/socket.go @@ -0,0 +1,37 @@ +package websocket + +import ( + "context" + + ws "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" +) + +type CtxReader interface { + Read(ctx context.Context, v any) error +} + +type CtxWriter interface { + Write(ctx context.Context, v any) error +} + +type CtxReadWriter interface { + CtxReader + CtxWriter +} + +type socketJSON struct { + *ws.Conn +} + +func newSocketJSON(conn *ws.Conn) socketJSON { + return socketJSON{conn} +} + +func (s socketJSON) Write(ctx context.Context, v any) error { + return wsjson.Write(ctx, s.Conn, v) +} + +func (s socketJSON) Read(ctx context.Context, v any) error { + return wsjson.Read(ctx, s.Conn, v) +} diff --git a/internal/pkg/delivery/websocket/types.go b/internal/pkg/delivery/websocket/types.go new file mode 100644 index 0000000..1271683 --- /dev/null +++ b/internal/pkg/delivery/websocket/types.go @@ -0,0 +1,65 @@ +package websocket + +import "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + +//go:generate easyjson --all + +type Object struct { + Type string `json:"eventType,omitempty"` + Message message.Message `json:"message"` +} + +type PublishRequest struct { + ID int `json:"requestID"` + Message Object `json:"message"` +} + +type MessageFromChannel struct { + Type string `json:"type"` + Message ResponseMessage `json:"message"` +} + +type ResponseMessage struct { + Object + Status string `json:"status"` + Code string `json:"code,omitempty"` + MessageText string `json:"messageText,omitempty"` +} + +type ResponseOnRequest struct { + ID int `json:"requestID"` + Type string `json:"type"` + Status string `json:"status"` + Code string `json:"code,omitempty"` + Message string `json:"message"` + Body any `json:"body,omitempty"` +} + +func newResponseOnRequest(id int, status, code, message string, body any) *ResponseOnRequest { + return &ResponseOnRequest{ + ID: id, + Type: "response", + Status: status, + Code: code, + Message: message, + Body: body, + } +} + +func newMessageFromChannel(status, code string, v any) *MessageFromChannel { + mes := &MessageFromChannel{ + Type: "event", + Message: ResponseMessage{ + Status: status, + Code: code, + }, + } + if v, ok := v.(Object); ok { + mes.Message.Object = v + return mes + } + if v, ok := v.(string); ok { + mes.Message.MessageText = v + } + return mes +} diff --git a/internal/pkg/delivery/websocket/types_easyjson.go b/internal/pkg/delivery/websocket/types_easyjson.go new file mode 100644 index 0000000..11db088 --- /dev/null +++ b/internal/pkg/delivery/websocket/types_easyjson.go @@ -0,0 +1,451 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package websocket + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(in *jlexer.Lexer, out *ResponseOnRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "requestID": + out.ID = int(in.Int()) + case "type": + out.Type = string(in.String()) + case "status": + out.Status = string(in.String()) + case "code": + out.Code = string(in.String()) + case "message": + out.Message = string(in.String()) + case "body": + if m, ok := out.Body.(easyjson.Unmarshaler); ok { + m.UnmarshalEasyJSON(in) + } else if m, ok := out.Body.(json.Unmarshaler); ok { + _ = m.UnmarshalJSON(in.Raw()) + } else { + out.Body = in.Interface() + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(out *jwriter.Writer, in ResponseOnRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"requestID\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"type\":" + out.RawString(prefix) + out.String(string(in.Type)) + } + { + const prefix string = ",\"status\":" + out.RawString(prefix) + out.String(string(in.Status)) + } + if in.Code != "" { + const prefix string = ",\"code\":" + out.RawString(prefix) + out.String(string(in.Code)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + out.String(string(in.Message)) + } + if in.Body != nil { + const prefix string = ",\"body\":" + out.RawString(prefix) + if m, ok := in.Body.(easyjson.Marshaler); ok { + m.MarshalEasyJSON(out) + } else if m, ok := in.Body.(json.Marshaler); ok { + out.Raw(m.MarshalJSON()) + } else { + out.Raw(json.Marshal(in.Body)) + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ResponseOnRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ResponseOnRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ResponseOnRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ResponseOnRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket(l, v) +} +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(in *jlexer.Lexer, out *ResponseMessage) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "status": + out.Status = string(in.String()) + case "code": + out.Code = string(in.String()) + case "messageText": + out.MessageText = string(in.String()) + case "eventType": + out.Type = string(in.String()) + case "message": + (out.Message).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(out *jwriter.Writer, in ResponseMessage) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"status\":" + out.RawString(prefix[1:]) + out.String(string(in.Status)) + } + if in.Code != "" { + const prefix string = ",\"code\":" + out.RawString(prefix) + out.String(string(in.Code)) + } + if in.MessageText != "" { + const prefix string = ",\"messageText\":" + out.RawString(prefix) + out.String(string(in.MessageText)) + } + if in.Type != "" { + const prefix string = ",\"eventType\":" + out.RawString(prefix) + out.String(string(in.Type)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + (in.Message).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ResponseMessage) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ResponseMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ResponseMessage) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ResponseMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket1(l, v) +} +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(in *jlexer.Lexer, out *PublishRequest) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "requestID": + out.ID = int(in.Int()) + case "message": + (out.Message).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(out *jwriter.Writer, in PublishRequest) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"requestID\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + (in.Message).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v PublishRequest) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v PublishRequest) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *PublishRequest) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *PublishRequest) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket2(l, v) +} +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(in *jlexer.Lexer, out *Object) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "eventType": + out.Type = string(in.String()) + case "message": + (out.Message).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(out *jwriter.Writer, in Object) { + out.RawByte('{') + first := true + _ = first + if in.Type != "" { + const prefix string = ",\"eventType\":" + first = false + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + { + const prefix string = ",\"message\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + (in.Message).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Object) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Object) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Object) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Object) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket3(l, v) +} +func easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(in *jlexer.Lexer, out *MessageFromChannel) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "message": + (out.Message).UnmarshalEasyJSON(in) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(out *jwriter.Writer, in MessageFromChannel) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + { + const prefix string = ",\"message\":" + out.RawString(prefix) + (in.Message).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v MessageFromChannel) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v MessageFromChannel) MarshalEasyJSON(w *jwriter.Writer) { + easyjson6601e8cdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *MessageFromChannel) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *MessageFromChannel) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson6601e8cdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgDeliveryWebsocket4(l, v) +} diff --git a/internal/pkg/delivery/websocket/websocket.go b/internal/pkg/delivery/websocket/websocket.go new file mode 100644 index 0000000..bd7db73 --- /dev/null +++ b/internal/pkg/delivery/websocket/websocket.go @@ -0,0 +1,53 @@ +package websocket + +import ( + "context" + "fmt" + "net/http" + "time" + + ws "nhooyr.io/websocket" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" + usecase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/message" + log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +type notifySubscriber interface { + SubscribeOnAllNotifications(ctx context.Context, userID int) (<-chan *notification.NotifyMessage, error) +} + +type HandlerWebSocket struct { + originPatterns []string + log *log.Logger + messageCase usecase.Usecase + notifySub notifySubscriber +} + +type Option func(h *HandlerWebSocket) + +const _ctxOnServeConnect = 24 * time.Hour + +func SetOriginPatterns(patterns []string) Option { + return func(h *HandlerWebSocket) { + h.originPatterns = patterns + } +} + +func New(log *log.Logger, mesCase usecase.Usecase, notify notifySubscriber, opts ...Option) *HandlerWebSocket { + handlerWS := &HandlerWebSocket{log: log, messageCase: mesCase, notifySub: notify} + + for _, opt := range opts { + opt(handlerWS) + } + + return handlerWS +} + +func (h *HandlerWebSocket) upgradeWSConnect(w http.ResponseWriter, r *http.Request) (*ws.Conn, error) { + conn, err := ws.Accept(w, r, &ws.AcceptOptions{OriginPatterns: h.originPatterns}) + if err != nil { + return nil, fmt.Errorf("upgrade to websocket connect: %w", err) + } + return conn, nil +} diff --git a/internal/pkg/entity/board/board.go b/internal/pkg/entity/board/board.go index 92c37b6..237cbc6 100644 --- a/internal/pkg/entity/board/board.go +++ b/internal/pkg/entity/board/board.go @@ -3,12 +3,15 @@ package board import ( "time" - "github.com/microcosm-cc/bluemonday" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" ) +//go:generate easyjson board.go + +//easyjson:json type Board struct { ID int `json:"id,omitempty" example:"15"` - AuthorID int `json:"-"` + AuthorID int `json:"author_id,omitempty"` Title string `json:"title" example:"Sunny places"` Description string `json:"description" example:"Sunny places desc"` Public bool `json:"public" example:"true"` @@ -17,6 +20,7 @@ type Board struct { DeletedAt *time.Time `json:"-"` } +//easyjson:json type BoardWithContent struct { BoardInfo Board PinsNumber int @@ -24,14 +28,9 @@ type BoardWithContent struct { TagTitles []string } -func (b *Board) Sanitize(sanitizer *bluemonday.Policy) { - sanitizer.Sanitize(b.Title) - sanitizer.Sanitize(b.Description) -} - -func (b *BoardWithContent) Sanitize(sanitizer *bluemonday.Policy) { - b.BoardInfo.Sanitize(sanitizer) - for id, title := range b.TagTitles { - b.TagTitles[id] = sanitizer.Sanitize(title) +func (b *Board) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if b != nil { + b.Title = sanitizer.Sanitize(censor.Sanitize(b.Title)) + b.Description = sanitizer.Sanitize(censor.Sanitize(b.Description)) } } diff --git a/internal/pkg/entity/board/board_easyjson.go b/internal/pkg/entity/board/board_easyjson.go new file mode 100644 index 0000000..12d08ef --- /dev/null +++ b/internal/pkg/entity/board/board_easyjson.go @@ -0,0 +1,293 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package board + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" + time "time" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(in *jlexer.Lexer, out *BoardWithContent) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "BoardInfo": + (out.BoardInfo).UnmarshalEasyJSON(in) + case "PinsNumber": + out.PinsNumber = int(in.Int()) + case "Pins": + if in.IsNull() { + in.Skip() + out.Pins = nil + } else { + in.Delim('[') + if out.Pins == nil { + if !in.IsDelim(']') { + out.Pins = make([]string, 0, 4) + } else { + out.Pins = []string{} + } + } else { + out.Pins = (out.Pins)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.Pins = append(out.Pins, v1) + in.WantComma() + } + in.Delim(']') + } + case "TagTitles": + if in.IsNull() { + in.Skip() + out.TagTitles = nil + } else { + in.Delim('[') + if out.TagTitles == nil { + if !in.IsDelim(']') { + out.TagTitles = make([]string, 0, 4) + } else { + out.TagTitles = []string{} + } + } else { + out.TagTitles = (out.TagTitles)[:0] + } + for !in.IsDelim(']') { + var v2 string + v2 = string(in.String()) + out.TagTitles = append(out.TagTitles, v2) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(out *jwriter.Writer, in BoardWithContent) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"BoardInfo\":" + out.RawString(prefix[1:]) + (in.BoardInfo).MarshalEasyJSON(out) + } + { + const prefix string = ",\"PinsNumber\":" + out.RawString(prefix) + out.Int(int(in.PinsNumber)) + } + { + const prefix string = ",\"Pins\":" + out.RawString(prefix) + if in.Pins == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v3, v4 := range in.Pins { + if v3 > 0 { + out.RawByte(',') + } + out.String(string(v4)) + } + out.RawByte(']') + } + } + { + const prefix string = ",\"TagTitles\":" + out.RawString(prefix) + if in.TagTitles == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v5, v6 := range in.TagTitles { + if v5 > 0 { + out.RawByte(',') + } + out.String(string(v6)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BoardWithContent) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BoardWithContent) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BoardWithContent) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BoardWithContent) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard(l, v) +} +func easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(in *jlexer.Lexer, out *Board) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "author_id": + out.AuthorID = int(in.Int()) + case "title": + out.Title = string(in.String()) + case "description": + out.Description = string(in.String()) + case "public": + out.Public = bool(in.Bool()) + case "created_at": + if in.IsNull() { + in.Skip() + out.CreatedAt = nil + } else { + if out.CreatedAt == nil { + out.CreatedAt = new(time.Time) + } + if data := in.Raw(); in.Ok() { + in.AddError((*out.CreatedAt).UnmarshalJSON(data)) + } + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(out *jwriter.Writer, in Board) { + out.RawByte('{') + first := true + _ = first + if in.ID != 0 { + const prefix string = ",\"id\":" + first = false + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + if in.AuthorID != 0 { + const prefix string = ",\"author_id\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int(int(in.AuthorID)) + } + { + const prefix string = ",\"title\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Title)) + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + out.String(string(in.Description)) + } + { + const prefix string = ",\"public\":" + out.RawString(prefix) + out.Bool(bool(in.Public)) + } + if in.CreatedAt != nil { + const prefix string = ",\"created_at\":" + out.RawString(prefix) + out.Raw((*in.CreatedAt).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Board) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Board) MarshalEasyJSON(w *jwriter.Writer) { + easyjson202377feEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Board) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Board) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson202377feDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityBoard1(l, v) +} diff --git a/internal/pkg/entity/comment/comment.go b/internal/pkg/entity/comment/comment.go new file mode 100644 index 0000000..bd076cd --- /dev/null +++ b/internal/pkg/entity/comment/comment.go @@ -0,0 +1,29 @@ +package comment + +import ( + "github.com/jackc/pgx/v5/pgtype" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" +) + +//go:generate easyjson comment.go +//easyjson:json +type Comment struct { + ID int `json:"id"` + Author *user.User `json:"author"` + PinID int `json:"pinID"` + Content pgtype.Text `json:"content"` +} + +func (c *Comment) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if c != nil { + if c.Author != nil { + c.Author.Sanitize(sanitizer, censor) + } + c.Content = pgtype.Text{ + String: sanitizer.Sanitize(censor.Sanitize(c.Content.String)), + Valid: c.Content.Valid, + } + } +} diff --git a/internal/pkg/entity/comment/comment_easyjson.go b/internal/pkg/entity/comment/comment_easyjson.go new file mode 100644 index 0000000..78723f0 --- /dev/null +++ b/internal/pkg/entity/comment/comment_easyjson.go @@ -0,0 +1,121 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package comment + +import ( + json "encoding/json" + user "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonE9abebc9DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(in *jlexer.Lexer, out *Comment) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "author": + if in.IsNull() { + in.Skip() + out.Author = nil + } else { + if out.Author == nil { + out.Author = new(user.User) + } + (*out.Author).UnmarshalEasyJSON(in) + } + case "pinID": + out.PinID = int(in.Int()) + case "content": + if data := in.Raw(); in.Ok() { + in.AddError((out.Content).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonE9abebc9EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(out *jwriter.Writer, in Comment) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"author\":" + out.RawString(prefix) + if in.Author == nil { + out.RawString("null") + } else { + (*in.Author).MarshalEasyJSON(out) + } + } + { + const prefix string = ",\"pinID\":" + out.RawString(prefix) + out.Int(int(in.PinID)) + } + { + const prefix string = ",\"content\":" + out.RawString(prefix) + out.Raw((in.Content).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Comment) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonE9abebc9EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Comment) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonE9abebc9EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Comment) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonE9abebc9DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Comment) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonE9abebc9DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityComment(l, v) +} diff --git a/internal/pkg/entity/message/message.go b/internal/pkg/entity/message/message.go new file mode 100644 index 0000000..6855287 --- /dev/null +++ b/internal/pkg/entity/message/message.go @@ -0,0 +1,40 @@ +package message + +import ( + "github.com/jackc/pgx/v5/pgtype" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" +) + +//go:generate easyjson message.go + +type Chat [2]int + +//easyjson:json +type Message struct { + ID int `json:"id,omitempty"` + From int `json:"from"` + To int `json:"to"` + Content pgtype.Text `json:"content"` +} + +func (m *Message) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if m != nil { + m.Content = pgtype.Text{ + String: sanitizer.Sanitize(m.Content.String), + Valid: m.Content.Valid, + } + } +} + +func (m Message) WhatChat() Chat { + return Chat{m.From, m.To} +} + +type FeedUserChats []ChatWithUser + +type ChatWithUser struct { + MessageLastID int `json:"-"` + WichWhomChat user.User `json:"user"` +} diff --git a/internal/pkg/entity/message/message_easyjson.go b/internal/pkg/entity/message/message_easyjson.go new file mode 100644 index 0000000..91a2ec5 --- /dev/null +++ b/internal/pkg/entity/message/message_easyjson.go @@ -0,0 +1,114 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package message + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(in *jlexer.Lexer, out *Message) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "from": + out.From = int(in.Int()) + case "to": + out.To = int(in.Int()) + case "content": + if data := in.Raw(); in.Ok() { + in.AddError((out.Content).UnmarshalJSON(data)) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(out *jwriter.Writer, in Message) { + out.RawByte('{') + first := true + _ = first + if in.ID != 0 { + const prefix string = ",\"id\":" + first = false + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"from\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int(int(in.From)) + } + { + const prefix string = ",\"to\":" + out.RawString(prefix) + out.Int(int(in.To)) + } + { + const prefix string = ",\"content\":" + out.RawString(prefix) + out.Raw((in.Content).MarshalJSON()) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v Message) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v Message) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *Message) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *Message) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityMessage(l, v) +} diff --git a/internal/pkg/entity/notification/message.go b/internal/pkg/entity/notification/message.go new file mode 100644 index 0000000..aaac18b --- /dev/null +++ b/internal/pkg/entity/notification/message.go @@ -0,0 +1,26 @@ +package notification + +//go:generate easyjson +//easyjson:json +type NotifyMessage struct { + Type string `json:"type"` + Content string `json:"content"` + err error +} + +func (n *NotifyMessage) Err() error { + return n.err +} + +func NewNotifyMessage(t NotifyType, content string) *NotifyMessage { + return &NotifyMessage{ + Type: TypeString(t), + Content: content, + } +} + +func NewNotifyMessageWithError(err error) *NotifyMessage { + return &NotifyMessage{ + err: err, + } +} diff --git a/internal/pkg/entity/notification/message_easyjson.go b/internal/pkg/entity/notification/message_easyjson.go new file mode 100644 index 0000000..a774aab --- /dev/null +++ b/internal/pkg/entity/notification/message_easyjson.go @@ -0,0 +1,92 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package notification + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(in *jlexer.Lexer, out *NotifyMessage) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "content": + out.Content = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(out *jwriter.Writer, in NotifyMessage) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + { + const prefix string = ",\"content\":" + out.RawString(prefix) + out.String(string(in.Content)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v NotifyMessage) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v NotifyMessage) MarshalEasyJSON(w *jwriter.Writer) { + easyjson4086215fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *NotifyMessage) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *NotifyMessage) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson4086215fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityNotification(l, v) +} diff --git a/internal/pkg/entity/notification/notification.go b/internal/pkg/entity/notification/notification.go new file mode 100644 index 0000000..a21f772 --- /dev/null +++ b/internal/pkg/entity/notification/notification.go @@ -0,0 +1,86 @@ +package notification + +import ( + "bytes" + "fmt" + "sync" + "text/template" +) + +type NotifyType uint8 + +const _defaultCapBuffer = 128 + +const ( + _ NotifyType = iota + NotifyComment + + _notifyCustom +) + +type notify struct { + NotifyType NotifyType + buf *sync.Pool + tmp *template.Template +} + +func NewWithTemplate(tmp *template.Template) notify { + return notify{ + NotifyType: _notifyCustom, + buf: &sync.Pool{ + New: func() any { return bytes.NewBuffer(make([]byte, 0, _defaultCapBuffer)) }, + }, + tmp: tmp, + } +} + +func NewWithType(t NotifyType) (notify, error) { + content, ok := notifyTypeTemplate[t] + if !ok { + return notify{}, fmt.Errorf("new notify with type %s: %w", TypeString(t), ErrUnknownNotifyType) + } + + res := notify{ + NotifyType: t, + buf: &sync.Pool{ + New: func() any { return bytes.NewBuffer(make([]byte, 0, _defaultCapBuffer)) }, + }, + } + + tmp, err := template.New(TypeString(t)).Parse(content) + if err != nil { + return notify{}, fmt.Errorf("new notify with type %s: %w", TypeString(t), err) + } + + res.tmp = tmp + return res, nil +} + +func (n notify) Type() NotifyType { + return n.NotifyType +} + +func (n notify) BuildNotifyMessage(data any) (*NotifyMessage, error) { + content, err := n.FormatContent(data) + if err != nil { + return nil, fmt.Errorf("build notify message: %w", err) + } + + return NewNotifyMessage(n.NotifyType, content), nil +} + +func (n notify) FormatContent(data any) (string, error) { + buf := n.buf.Get().(*bytes.Buffer) + + defer func() { + buf.Reset() + n.buf.Put(buf) + }() + + err := n.tmp.Execute(buf, data) + if err != nil { + return "", fmt.Errorf("") + } + + return buf.String(), nil +} diff --git a/internal/pkg/entity/notification/template.go b/internal/pkg/entity/notification/template.go new file mode 100644 index 0000000..55ad3cb --- /dev/null +++ b/internal/pkg/entity/notification/template.go @@ -0,0 +1,5 @@ +package notification + +var notifyTypeTemplate = map[NotifyType]string{ + NotifyComment: `Пользователь {{.Username}} оставил комментарий под пином "{{.TitlePin}}".`, +} diff --git a/internal/pkg/entity/notification/type.go b/internal/pkg/entity/notification/type.go new file mode 100644 index 0000000..3050265 --- /dev/null +++ b/internal/pkg/entity/notification/type.go @@ -0,0 +1,20 @@ +package notification + +import "errors" + +var ErrUnknownNotifyType = errors.New("unknown notify type") + +func TypeString(t NotifyType) string { + switch t { + case NotifyComment: + return "comment" + case _notifyCustom: + return "custom" + } + + return "" +} + +func NotifyTemplateByType(t NotifyType) string { + return notifyTypeTemplate[t] +} diff --git a/internal/pkg/entity/pin/pin.go b/internal/pkg/entity/pin/pin.go index f547059..b655d1d 100644 --- a/internal/pkg/entity/pin/pin.go +++ b/internal/pkg/entity/pin/pin.go @@ -4,6 +4,7 @@ import ( "github.com/jackc/pgx/v5/pgtype" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" ) type Pin struct { @@ -20,6 +21,22 @@ type Pin struct { DeletedAt pgtype.Timestamptz `json:"-"` } //@name Pin +func (p *Pin) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if p != nil { + if p.Author != nil { + p.Author.Sanitize(sanitizer, censor) + } + p.Title = pgtype.Text{ + String: sanitizer.Sanitize(censor.Sanitize(p.Title.String)), + Valid: p.Title.Valid, + } + p.Description = pgtype.Text{ + String: sanitizer.Sanitize(censor.Sanitize(p.Description.String)), + Valid: p.Description.Valid, + } + } +} + func (p *Pin) SetTitle(title string) { p.Title = pgtype.Text{ String: title, diff --git a/internal/pkg/entity/search/search.go b/internal/pkg/entity/search/search.go new file mode 100644 index 0000000..218b7cc --- /dev/null +++ b/internal/pkg/entity/search/search.go @@ -0,0 +1,84 @@ +package search + +import ( + "strings" + "unicode" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/board" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" +) + +//go:generate easyjson search.go + +type Template string + +func (t *Template) Validate() bool { + if len(*t) == 0 || len(*t) > 40 { + return false + } + for _, sym := range *t { + if !(unicode.IsNumber(sym) || unicode.IsLetter(sym) || unicode.IsPunct(sym) || unicode.IsSpace(sym)) { + return false + } + } + return true +} + +func (t *Template) GetSubStrings(sep string) []string { + return strings.Split(string(*t), sep) +} + +//easyjson:json +type BoardForSearch struct { + BoardHeader board.Board + PinsNumber int `json:"pins_number"` + PreviewPins []string `json:"pins"` +} + +//easyjson:json +type PinForSearch struct { + ID int `json:"id"` + Title string `json:"title"` + Picture string `json:"picture"` + Likes int `json:"likes"` +} + +//easyjson:json +type UserForSearch struct { + ID int `json:"id"` + Username string `json:"username"` + Avatar string `json:"avatar"` + SubsCount int `json:"subscribers"` + HasSubscribeFromCurUser bool `json:"is_subscribed"` +} + +func (u *UserForSearch) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if u != nil { + u.Username = sanitizer.Sanitize(censor.Sanitize(u.Username)) + } +} + +func (b *BoardForSearch) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if b != nil { + b.BoardHeader.Sanitize(sanitizer, censor) + } +} + +func (p *PinForSearch) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if p != nil { + p.Title = sanitizer.Sanitize(censor.Sanitize(p.Title)) + } +} + +type SearchOpts struct { + General GeneralOpts + SortBy string +} + +type GeneralOpts struct { + Template Template + SortOrder string + CurrUserID int + Count int + Offset int +} diff --git a/internal/pkg/entity/search/search_easyjson.go b/internal/pkg/entity/search/search_easyjson.go new file mode 100644 index 0000000..497b75f --- /dev/null +++ b/internal/pkg/entity/search/search_easyjson.go @@ -0,0 +1,312 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package search + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(in *jlexer.Lexer, out *UserForSearch) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "username": + out.Username = string(in.String()) + case "avatar": + out.Avatar = string(in.String()) + case "subscribers": + out.SubsCount = int(in.Int()) + case "is_subscribed": + out.HasSubscribeFromCurUser = bool(in.Bool()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(out *jwriter.Writer, in UserForSearch) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"username\":" + out.RawString(prefix) + out.String(string(in.Username)) + } + { + const prefix string = ",\"avatar\":" + out.RawString(prefix) + out.String(string(in.Avatar)) + } + { + const prefix string = ",\"subscribers\":" + out.RawString(prefix) + out.Int(int(in.SubsCount)) + } + { + const prefix string = ",\"is_subscribed\":" + out.RawString(prefix) + out.Bool(bool(in.HasSubscribeFromCurUser)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v UserForSearch) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v UserForSearch) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *UserForSearch) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *UserForSearch) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch(l, v) +} +func easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(in *jlexer.Lexer, out *PinForSearch) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "title": + out.Title = string(in.String()) + case "picture": + out.Picture = string(in.String()) + case "likes": + out.Likes = int(in.Int()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(out *jwriter.Writer, in PinForSearch) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"title\":" + out.RawString(prefix) + out.String(string(in.Title)) + } + { + const prefix string = ",\"picture\":" + out.RawString(prefix) + out.String(string(in.Picture)) + } + { + const prefix string = ",\"likes\":" + out.RawString(prefix) + out.Int(int(in.Likes)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v PinForSearch) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v PinForSearch) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *PinForSearch) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *PinForSearch) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch1(l, v) +} +func easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(in *jlexer.Lexer, out *BoardForSearch) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "BoardHeader": + (out.BoardHeader).UnmarshalEasyJSON(in) + case "pins_number": + out.PinsNumber = int(in.Int()) + case "pins": + if in.IsNull() { + in.Skip() + out.PreviewPins = nil + } else { + in.Delim('[') + if out.PreviewPins == nil { + if !in.IsDelim(']') { + out.PreviewPins = make([]string, 0, 4) + } else { + out.PreviewPins = []string{} + } + } else { + out.PreviewPins = (out.PreviewPins)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.PreviewPins = append(out.PreviewPins, v1) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(out *jwriter.Writer, in BoardForSearch) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"BoardHeader\":" + out.RawString(prefix[1:]) + (in.BoardHeader).MarshalEasyJSON(out) + } + { + const prefix string = ",\"pins_number\":" + out.RawString(prefix) + out.Int(int(in.PinsNumber)) + } + { + const prefix string = ",\"pins\":" + out.RawString(prefix) + if in.PreviewPins == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v2, v3 := range in.PreviewPins { + if v2 > 0 { + out.RawByte(',') + } + out.String(string(v3)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v BoardForSearch) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v BoardForSearch) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonD4176298EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *BoardForSearch) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *BoardForSearch) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonD4176298DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntitySearch2(l, v) +} diff --git a/internal/pkg/entity/user/user.go b/internal/pkg/entity/user/user.go index 4b70349..d12f37a 100644 --- a/internal/pkg/entity/user/user.go +++ b/internal/pkg/entity/user/user.go @@ -1,14 +1,62 @@ package user -import "github.com/jackc/pgx/v5/pgtype" +import ( + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" + "github.com/jackc/pgx/v5/pgtype" +) +//go:generate easyjson user.go + +const UserUnknown = -1 + +//easyjson:json type User struct { - ID int `json:"id" example:"123"` + ID int `json:"id,omitempty" example:"123"` Username string `json:"username" example:"Green"` - Name pgtype.Text `json:"name" example:"Peter"` - Surname pgtype.Text `json:"surname" example:"Green"` + Name pgtype.Text `json:"name,omitempty" example:"Peter"` + Surname pgtype.Text `json:"surname,omitempty" example:"Green"` Email string `json:"email,omitempty" example:"digital@gmail.com"` Avatar string `json:"avatar" example:"pinspire.online/avatars/avatar.jpg"` - AboutMe pgtype.Text `json:"about_me"` + AboutMe pgtype.Text `json:"about_me,omitempty"` Password string `json:"password,omitempty" example:"pass123"` } // @name User + +func (u *User) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if u != nil { + u.Username = sanitizer.Sanitize(censor.Sanitize(u.Username)) + u.Email = sanitizer.Sanitize(censor.Sanitize(u.Email)) + u.Name = pgtype.Text{ + String: sanitizer.Sanitize(censor.Sanitize(u.Name.String)), + Valid: u.Name.Valid, + } + u.Surname = pgtype.Text{ + String: sanitizer.Sanitize(censor.Sanitize(u.Surname.String)), + Valid: u.Surname.Valid, + } + u.AboutMe = pgtype.Text{ + String: sanitizer.Sanitize(censor.Sanitize(u.AboutMe.String)), + Valid: u.AboutMe.Valid, + } + } +} + +//easyjson:json +type SubscriptionUser struct { + ID int `json:"id"` + Username string `json:"username"` + Avatar string `json:"avatar"` + HasSubscribeFromCurUser bool `json:"is_subscribed"` +} + +func (u *SubscriptionUser) Sanitize(sanitizer validation.SanitizerXSS, censor validation.ProfanityCensor) { + if u != nil { + u.Username = sanitizer.Sanitize(censor.Sanitize(u.Username)) + } +} + +type SubscriptionOpts struct { + UserID int + Count int + LastID int + Filter string +} diff --git a/internal/pkg/entity/user/user_easyjson.go b/internal/pkg/entity/user/user_easyjson.go new file mode 100644 index 0000000..e8b0dae --- /dev/null +++ b/internal/pkg/entity/user/user_easyjson.go @@ -0,0 +1,233 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package user + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(in *jlexer.Lexer, out *User) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "username": + out.Username = string(in.String()) + case "name": + if data := in.Raw(); in.Ok() { + in.AddError((out.Name).UnmarshalJSON(data)) + } + case "surname": + if data := in.Raw(); in.Ok() { + in.AddError((out.Surname).UnmarshalJSON(data)) + } + case "email": + out.Email = string(in.String()) + case "avatar": + out.Avatar = string(in.String()) + case "about_me": + if data := in.Raw(); in.Ok() { + in.AddError((out.AboutMe).UnmarshalJSON(data)) + } + case "password": + out.Password = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(out *jwriter.Writer, in User) { + out.RawByte('{') + first := true + _ = first + if in.ID != 0 { + const prefix string = ",\"id\":" + first = false + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"username\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Username)) + } + if true { + const prefix string = ",\"name\":" + out.RawString(prefix) + out.Raw((in.Name).MarshalJSON()) + } + if true { + const prefix string = ",\"surname\":" + out.RawString(prefix) + out.Raw((in.Surname).MarshalJSON()) + } + if in.Email != "" { + const prefix string = ",\"email\":" + out.RawString(prefix) + out.String(string(in.Email)) + } + { + const prefix string = ",\"avatar\":" + out.RawString(prefix) + out.String(string(in.Avatar)) + } + if true { + const prefix string = ",\"about_me\":" + out.RawString(prefix) + out.Raw((in.AboutMe).MarshalJSON()) + } + if in.Password != "" { + const prefix string = ",\"password\":" + out.RawString(prefix) + out.String(string(in.Password)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v User) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v User) MarshalEasyJSON(w *jwriter.Writer) { + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *User) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *User) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser(l, v) +} +func easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(in *jlexer.Lexer, out *SubscriptionUser) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "id": + out.ID = int(in.Int()) + case "username": + out.Username = string(in.String()) + case "avatar": + out.Avatar = string(in.String()) + case "is_subscribed": + out.HasSubscribeFromCurUser = bool(in.Bool()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(out *jwriter.Writer, in SubscriptionUser) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"id\":" + out.RawString(prefix[1:]) + out.Int(int(in.ID)) + } + { + const prefix string = ",\"username\":" + out.RawString(prefix) + out.String(string(in.Username)) + } + { + const prefix string = ",\"avatar\":" + out.RawString(prefix) + out.String(string(in.Avatar)) + } + { + const prefix string = ",\"is_subscribed\":" + out.RawString(prefix) + out.Bool(bool(in.HasSubscribeFromCurUser)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v SubscriptionUser) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v SubscriptionUser) MarshalEasyJSON(w *jwriter.Writer) { + easyjson9e1087fdEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *SubscriptionUser) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *SubscriptionUser) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson9e1087fdDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgEntityUser1(l, v) +} diff --git a/internal/pkg/errors/types.go b/internal/pkg/errors/types.go new file mode 100644 index 0000000..815bd5b --- /dev/null +++ b/internal/pkg/errors/types.go @@ -0,0 +1,70 @@ +package errors + +import "fmt" + +type Type uint8 + +type Layer string + +const ( + Repo Layer = "Repository" + Usecase Layer = "Usecase" + Delivery Layer = "Delivery" +) + +const ( + _ Type = iota + ErrNotFound + ErrAlreadyExists + ErrInvalidInput + ErrNoAccess + ErrNoAuth + ErrNotImplemented + ErrTimeout +) + +type DeclaredError interface { + Type() Type +} + +// general application errors +type ErrNotAuthenticated struct{} + +func (e *ErrNotAuthenticated) Error() string { + return "Auth required" +} + +func (e *ErrNotAuthenticated) Type() Type { + return ErrNoAuth +} + +type InternalError struct { + Message string + Layer string +} + +func (e *InternalError) Error() string { + return fmt.Sprintf("Internal error occured. Message: '%s'. Layer: %s", e.Message, e.Layer) +} + +type ErrorNotImplemented struct { +} + +func (e *ErrorNotImplemented) Error() string { + return "Functionality not implemented" +} + +func (e *ErrorNotImplemented) Type() Type { + return ErrNotImplemented +} + +type ErrTimeoutExceeded struct { +} + +func (e *ErrTimeoutExceeded) Error() string { + return "timeout exceeded" +} + +func (e *ErrTimeoutExceeded) Type() Type { + return ErrTimeout +} diff --git a/internal/pkg/metrics/grpc/metrics.go b/internal/pkg/metrics/grpc/metrics.go new file mode 100644 index 0000000..45cf2c8 --- /dev/null +++ b/internal/pkg/metrics/grpc/metrics.go @@ -0,0 +1,82 @@ +package messenger + +import ( + "fmt" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +type metrics struct { + prefix string + totalHits prometheus.Counter + hitsDetailOk *prometheus.CounterVec + hitsDetailErr *prometheus.CounterVec + timeMeasurement *prometheus.HistogramVec +} + +func (m metrics) AddRequest(handler string, ok bool, executed time.Duration) { + m.totalHits.Inc() + if ok { + m.hitsDetailOk.WithLabelValues(handler).Inc() + } else { + m.hitsDetailErr.WithLabelValues(handler).Inc() + } + m.timeMeasurement.WithLabelValues(handler).Observe(float64(executed.Milliseconds())) +} + +func New(prefix string) metrics { + return metrics{ + prefix: prefix, + totalHits: prometheus.NewCounter(prometheus.CounterOpts{ + Name: prefix + "_total_hits", + Help: "number of all requests", + }), + hitsDetailOk: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: prefix + "_hits_detail_ok", + Help: "the number of requests indicating handler with normal status", + }, []string{"handler"}), + hitsDetailErr: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: prefix + "_hits_detail_err", + Help: "the number of requests indicating its handler with error status", + }, []string{"handler"}), + timeMeasurement: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: prefix + "_request_time_execution", + Help: "time of request execution with indication of its handler", + Buckets: []float64{10, 100, 500, 1000, 5000}, + }, []string{"handler"}), + } +} + +func (m metrics) Registry() (err error) { + defer func() { + if err != nil { + prometheus.Unregister(m.totalHits) + prometheus.Unregister(m.hitsDetailOk) + prometheus.Unregister(m.timeMeasurement) + prometheus.Unregister(m.hitsDetailErr) + } + }() + + err = prometheus.Register(m.totalHits) + if err != nil { + return fmt.Errorf("registry metric total hits: %w", err) + } + + err = prometheus.Register(m.hitsDetailOk) + if err != nil { + return fmt.Errorf("registry metric hits detail ok: %w", err) + } + + err = prometheus.Register(m.hitsDetailErr) + if err != nil { + return fmt.Errorf("registry metric hits detail error: %w", err) + } + + err = prometheus.Register(m.timeMeasurement) + if err != nil { + return fmt.Errorf("registry metric time measurement: %w", err) + } + + return nil +} diff --git a/internal/pkg/metrics/metrics.go b/internal/pkg/metrics/metrics.go new file mode 100644 index 0000000..9a7afe8 --- /dev/null +++ b/internal/pkg/metrics/metrics.go @@ -0,0 +1,81 @@ +package metrics + +import ( + "fmt" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +type metrics struct { + prefix string + totalHits prometheus.Counter + hitsDetail *prometheus.CounterVec + timeMeasurement *prometheus.HistogramVec +} + +func (m metrics) AddRequest(method, path string, statusResponse int, executed time.Duration) { + var labelStatus string + switch { + case statusResponse < 200: + labelStatus = "100" + case statusResponse < 300: + labelStatus = "200" + case statusResponse < 400: + labelStatus = "300" + case statusResponse < 500: + labelStatus = "400" + default: + labelStatus = "500" + } + + m.totalHits.Inc() + m.hitsDetail.WithLabelValues(method+" "+path, labelStatus).Inc() + m.timeMeasurement.WithLabelValues(method+" "+path, labelStatus).Observe(float64(executed.Milliseconds())) +} + +func New(prefix string) metrics { + return metrics{ + prefix: prefix, + totalHits: prometheus.NewCounter(prometheus.CounterOpts{ + Name: prefix + "_total_hits", + Help: "number of all requests", + }), + hitsDetail: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: prefix + "_hits_detail", + Help: "the number of requests indicating its method, path, and response status", + }, []string{"handler", "status"}), + timeMeasurement: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: prefix + "_request_time_execution", + Help: "time of request execution with indication of its method, path and response status", + Buckets: []float64{10, 100, 500, 1000, 5000}, + }, []string{"handler", "status"}), + } +} + +func (m metrics) Registry() (err error) { + defer func() { + if err != nil { + prometheus.Unregister(m.totalHits) + prometheus.Unregister(m.hitsDetail) + prometheus.Unregister(m.timeMeasurement) + } + }() + + err = prometheus.Register(m.totalHits) + if err != nil { + return fmt.Errorf("registry metric total hits: %w", err) + } + + err = prometheus.Register(m.hitsDetail) + if err != nil { + return fmt.Errorf("registry metric hits detail: %w", err) + } + + err = prometheus.Register(m.timeMeasurement) + if err != nil { + return fmt.Errorf("registry metric time measurement: %w", err) + } + + return nil +} diff --git a/internal/pkg/middleware/auth/auth.go b/internal/pkg/middleware/auth/auth.go index 09b50ec..30d26dd 100644 --- a/internal/pkg/middleware/auth/auth.go +++ b/internal/pkg/middleware/auth/auth.go @@ -4,7 +4,8 @@ import ( "context" "net/http" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/session" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/session" + authCase "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/auth" ) type authContextValueKey string @@ -16,17 +17,17 @@ const ( ) type authMiddleware struct { - sm session.SessionManager + authCase authCase.Usecase } -func NewAuthMiddleware(sm session.SessionManager) authMiddleware { - return authMiddleware{sm} +func NewAuthMiddleware(auth authCase.Usecase) authMiddleware { + return authMiddleware{auth} } func (am authMiddleware) ContextWithUserID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie(SessionCookieName); err == nil { - if userID, err := am.sm.GetUserIDBySessionKey(r.Context(), cookie.Value); err == nil { + if userID, err := am.authCase.GetUserIDBySession(r.Context(), &session.Session{Key: cookie.Value, Expire: cookie.Expires}); err == nil { r = r.WithContext(context.WithValue(r.Context(), KeyCurrentUserID, userID)) } } diff --git a/internal/pkg/middleware/grpc/interceptor/auth.go b/internal/pkg/middleware/grpc/interceptor/auth.go new file mode 100644 index 0000000..8fd862f --- /dev/null +++ b/internal/pkg/middleware/grpc/interceptor/auth.go @@ -0,0 +1,27 @@ +package interceptor + +import ( + "context" + "strconv" + + messMS "github.com/go-park-mail-ru/2023_2_OND_team/internal/microservices/messenger/delivery/grpc" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +func Auth() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + md := metadata.ValueFromIncomingContext(ctx, messMS.AuthenticatedMetadataKey) + if len(md) != 1 { + return nil, status.Error(codes.Unauthenticated, "unauthenticated") + } + userID, err := strconv.ParseInt(md[0], 10, 64) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "unauthenticated") + } + return handler(context.WithValue(ctx, auth.KeyCurrentUserID, int(userID)), req) + } +} diff --git a/internal/pkg/middleware/grpc/interceptor/logger.go b/internal/pkg/middleware/grpc/interceptor/logger.go new file mode 100644 index 0000000..089dfe0 --- /dev/null +++ b/internal/pkg/middleware/grpc/interceptor/logger.go @@ -0,0 +1,19 @@ +package interceptor + +import ( + "context" + "time" + + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" + "google.golang.org/grpc" +) + +func Logger(log *logger.Logger) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + defer func(t time.Time) { + log.Info("call rpc", logger.F{"handler", info.FullMethod}, logger.F{"time_execution", time.Since(t).Milliseconds()}) + }(time.Now()) + res, err := handler(ctx, req) + return res, err + } +} diff --git a/internal/pkg/middleware/grpc/interceptor/metrics.go b/internal/pkg/middleware/grpc/interceptor/metrics.go new file mode 100644 index 0000000..2e24abc --- /dev/null +++ b/internal/pkg/middleware/grpc/interceptor/metrics.go @@ -0,0 +1,33 @@ +package interceptor + +import ( + "context" + "net/http" + "time" + + "github.com/prometheus/client_golang/prometheus/promhttp" + "google.golang.org/grpc" +) + +type Metrics interface { + AddRequest(handler string, ok bool, executed time.Duration) +} + +func Monitoring(m Metrics, addres string) grpc.UnaryServerInterceptor { + serv := http.Server{ + Addr: addres, + Handler: promhttp.Handler(), + } + go func() { serv.ListenAndServe() }() + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + var ok = true + defer func(t time.Time) { + m.AddRequest(info.FullMethod, ok, time.Since(t)) + }(time.Now()) + res, err := handler(ctx, req) + if err != nil { + ok = false + } + return res, err + } +} diff --git a/internal/pkg/middleware/monitoring/metrics.go b/internal/pkg/middleware/monitoring/metrics.go new file mode 100644 index 0000000..b8645f7 --- /dev/null +++ b/internal/pkg/middleware/monitoring/metrics.go @@ -0,0 +1,43 @@ +package monitoring + +import ( + "net/http" + "time" + + mw "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type Metrics interface { + AddRequest(method, path string, statusResponse int, executed time.Duration) +} + +type StatusReceiver interface { + StatusCode() int +} + +func Monitoring(pathExporter string, metrics Metrics) func(http.Handler) http.Handler { + instrumentMetricHandler := promhttp.Handler() + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == pathExporter { + instrumentMetricHandler.ServeHTTP(w, r) + return + } + + stat, ok := w.(StatusReceiver) + if !ok { + wrapResponse := mw.NewWrapResponseWriter(w) + w = wrapResponse + stat = wrapResponse + } + + defer func(method, path string, t time.Time) { + metrics.AddRequest(method, path, stat.StatusCode(), time.Since(t)) + }(r.Method, r.URL.Path, time.Now()) + + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/pkg/middleware/timeout.go b/internal/pkg/middleware/timeout.go new file mode 100644 index 0000000..02f5d0f --- /dev/null +++ b/internal/pkg/middleware/timeout.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "context" + "net/http" + "strconv" + "strings" + "time" +) + +func SetRequestTimeout(timeout time.Duration) Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if clientTimeout := extractTimeout(r); clientTimeout != 0 { + timeout = time.Duration(clientTimeout) + } + ctx, cancel := context.WithTimeout(r.Context(), timeout) + defer cancel() + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func extractTimeout(r *http.Request) int { + var keepAlive string + if keepAlive = r.Header.Get("Keep-Alive"); keepAlive == "" { + return *new(int) + } + + if options := strings.Split(keepAlive, " "); len(options) > 0 && len(options) <= 2 { + if timeoutOpt := options[0]; strings.Contains(timeoutOpt, "timeout") { + if timeout, err := strconv.ParseInt(strings.Split(timeoutOpt, "=")[1], 10, 64); err == nil { + return int(timeout) + } + } + } + return *new(int) +} diff --git a/internal/pkg/middleware/wrap_response.go b/internal/pkg/middleware/wrap_response.go index 552218f..cea28c8 100644 --- a/internal/pkg/middleware/wrap_response.go +++ b/internal/pkg/middleware/wrap_response.go @@ -1,10 +1,15 @@ package middleware import ( + "bufio" + "errors" "io" + "net" "net/http" ) +var ErrUnimplementedMethod = errors.New("unimplemented method") + type wrapResponseWriter struct { http.ResponseWriter statusCode int @@ -33,3 +38,24 @@ func (w *wrapResponseWriter) WriteString(data string) (written int, err error) { w.written += written return } + +func (w *wrapResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hijacker, ok := w.ResponseWriter.(http.Hijacker); ok { + return hijacker.Hijack() + } + return nil, nil, ErrUnimplementedMethod +} + +func (w *wrapResponseWriter) Flush() { + if flusher, ok := w.ResponseWriter.(http.Flusher); ok { + flusher.Flush() + } +} + +func (w *wrapResponseWriter) StatusCode() int { + return w.statusCode +} + +func (w *wrapResponseWriter) Written() int { + return w.written +} diff --git a/internal/pkg/notification/comment/comment.go b/internal/pkg/notification/comment/comment.go new file mode 100644 index 0000000..cc7c5c9 --- /dev/null +++ b/internal/pkg/notification/comment/comment.go @@ -0,0 +1,57 @@ +package comment + +import ( + "context" + "fmt" + "strconv" + + comm "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/notification" +) + +type commentGetter interface { + GetCommentWithAuthor(ctx context.Context, commentID int) (*comm.Comment, error) +} + +type pinGetter interface { + GetPinWithAuthor(ctx context.Context, pinID int) (*pin.Pin, error) +} + +type commentNotify struct { + notification.NotifyBuilder + + com commentGetter + pin pinGetter +} + +func NewCommentNotify(builder notification.NotifyBuilder, com commentGetter, pin pinGetter) commentNotify { + return commentNotify{builder, com, pin} +} + +func (c commentNotify) Type() entity.NotifyType { + return c.NotifyBuilder.Type() +} + +func (c commentNotify) MessageNotify(data notification.M) (*entity.NotifyMessage, error) { + return c.NotifyBuilder.BuildNotifyMessage(data) +} + +func (c commentNotify) ChannelsNameForSubscribe(_ context.Context, userID int) ([]string, error) { + return []string{strconv.Itoa(userID)}, nil +} + +func (c commentNotify) ChannelNameForPublishWithData(ctx context.Context, commentID int) (string, notification.M, error) { + com, err := c.com.GetCommentWithAuthor(ctx, commentID) + if err != nil { + return "", nil, fmt.Errorf("get comment for receive channel name on publish: %w", err) + } + + pin, err := c.pin.GetPinWithAuthor(ctx, com.PinID) + if err != nil { + return "", nil, fmt.Errorf("get pin for receive channel name on publish: %w", err) + } + + return strconv.Itoa(pin.Author.ID), notification.M{"Username": com.Author.Username, "TitlePin": pin.Title.String}, nil +} diff --git a/internal/pkg/notification/notifier.go b/internal/pkg/notification/notifier.go new file mode 100644 index 0000000..5f3bac8 --- /dev/null +++ b/internal/pkg/notification/notifier.go @@ -0,0 +1,27 @@ +package notification + +import ( + "context" + + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" +) + +type M map[string]string + +type TypeNotifier interface { + Type() entity.NotifyType +} + +type Notifier interface { + TypeNotifier + + ChannelNameForPublishWithData(ctx context.Context, entityID int) (string, M, error) + ChannelsNameForSubscribe(ctx context.Context, userID int) ([]string, error) + MessageNotify(data M) (*entity.NotifyMessage, error) +} + +type NotifyBuilder interface { + TypeNotifier + + BuildNotifyMessage(data any) (*entity.NotifyMessage, error) +} diff --git a/internal/pkg/repository/board/mock/board_mock.go b/internal/pkg/repository/board/mock/board_mock.go index d2af370..d1317aa 100644 --- a/internal/pkg/repository/board/mock/board_mock.go +++ b/internal/pkg/repository/board/mock/board_mock.go @@ -80,6 +80,20 @@ func (mr *MockRepositoryMockRecorder) DeleteBoardByID(ctx, boardID interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBoardByID", reflect.TypeOf((*MockRepository)(nil).DeleteBoardByID), ctx, boardID) } +// DeletePinFromBoard mocks base method. +func (m *MockRepository) DeletePinFromBoard(ctx context.Context, boardID, pinID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePinFromBoard", ctx, boardID, pinID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePinFromBoard indicates an expected call of DeletePinFromBoard. +func (mr *MockRepositoryMockRecorder) DeletePinFromBoard(ctx, boardID, pinID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePinFromBoard", reflect.TypeOf((*MockRepository)(nil).DeletePinFromBoard), ctx, boardID, pinID) +} + // GetBoardAuthorByBoardID mocks base method. func (m *MockRepository) GetBoardAuthorByBoardID(ctx context.Context, boardID int) (int, error) { m.ctrl.T.Helper() @@ -96,12 +110,13 @@ func (mr *MockRepositoryMockRecorder) GetBoardAuthorByBoardID(ctx, boardID inter } // GetBoardByID mocks base method. -func (m *MockRepository) GetBoardByID(ctx context.Context, boardID int, hasAccess bool) (board.BoardWithContent, error) { +func (m *MockRepository) GetBoardByID(ctx context.Context, boardID int, hasAccess bool) (board.BoardWithContent, string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetBoardByID", ctx, boardID, hasAccess) ret0, _ := ret[0].(board.BoardWithContent) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetBoardByID indicates an expected call of GetBoardByID. diff --git a/internal/pkg/repository/board/postgres/queries.go b/internal/pkg/repository/board/postgres/queries.go index d86f049..0dfc71b 100644 --- a/internal/pkg/repository/board/postgres/queries.go +++ b/internal/pkg/repository/board/postgres/queries.go @@ -9,6 +9,7 @@ const ( GetContributorBoardsIDs = "SELECT board_id FROM contributor WHERE user_id = $1;" DeleteBoardByIdQuery = "UPDATE board SET deleted_at = $1 WHERE id = $2 AND deleted_at IS NULL;" DeleteCurrentBoardTags = "DELETE FROM board_tag WHERE board_id = $1;" + DeletePinFromBoard = "DELETE FROM membership m WHERE m.board_id = $1 AND m.pin_id = $2 AND (SELECT deleted_at IS NULL FROM pin p WHERE p.id = $2);" SelectAuthorOrContributorRole = `SELECT board.author, role.name FROM board LEFT JOIN contributor ON contributor.board_id = board.id AND contributor.user_id = $1 LEFT JOIN role ON contributor.role_id = role.id diff --git a/internal/pkg/repository/board/postgres/repo.go b/internal/pkg/repository/board/postgres/repo.go index 681dc57..80b5325 100644 --- a/internal/pkg/repository/board/postgres/repo.go +++ b/internal/pkg/repository/board/postgres/repo.go @@ -114,11 +114,12 @@ func (boardRepo *boardRepoPG) GetBoardsByUserID(ctx context.Context, userID int, return boards, nil } -func (repo *boardRepoPG) GetBoardByID(ctx context.Context, boardID int, hasAccess bool) (board entity.BoardWithContent, err error) { +func (repo *boardRepoPG) GetBoardByID(ctx context.Context, boardID int, hasAccess bool) (board entity.BoardWithContent, username string, err error) { getBoardByIdQuery := repo.sqlBuilder. Select( "board.id", "board.author", + "profile.username", "board.title", "COALESCE(board.description, '')", "board.created_at", @@ -126,6 +127,7 @@ func (repo *boardRepoPG) GetBoardByID(ctx context.Context, boardID int, hasAcces "COALESCE((ARRAY_AGG(DISTINCT pin.picture) FILTER (WHERE pin.deleted_at IS NULL AND pin.picture IS NOT NULL)), ARRAY[]::TEXT[]) AS pins", "COALESCE(ARRAY_AGG(DISTINCT tag.title) FILTER (WHERE tag.title IS NOT NULL), ARRAY[]::TEXT[]) AS tag_titles"). From("board"). + LeftJoin("profile ON board.author = profile.id"). LeftJoin("board_tag ON board.id = board_tag.board_id"). LeftJoin("tag ON board_tag.tag_id = tag.id"). LeftJoin("membership ON board.id = membership.board_id"). @@ -139,6 +141,7 @@ func (repo *boardRepoPG) GetBoardByID(ctx context.Context, boardID int, hasAcces getBoardByIdQuery = getBoardByIdQuery.GroupBy( "board.id", "board.author", + "profile.username", "board.title", "board.description", "board.created_at"). @@ -146,22 +149,21 @@ func (repo *boardRepoPG) GetBoardByID(ctx context.Context, boardID int, hasAcces sqlRow, args, err := getBoardByIdQuery.ToSql() if err != nil { - return entity.BoardWithContent{}, fmt.Errorf("building get board by id query: %w", err) + return entity.BoardWithContent{}, "", fmt.Errorf("building get board by id query: %w", err) } row := repo.db.QueryRow(ctx, sqlRow, args...) board = entity.BoardWithContent{} - err = row.Scan(&board.BoardInfo.ID, &board.BoardInfo.AuthorID, &board.BoardInfo.Title, &board.BoardInfo.Description, &board.BoardInfo.CreatedAt, &board.PinsNumber, &board.Pins, &board.TagTitles) + err = row.Scan(&board.BoardInfo.ID, &board.BoardInfo.AuthorID, &username, &board.BoardInfo.Title, &board.BoardInfo.Description, &board.BoardInfo.CreatedAt, &board.PinsNumber, &board.Pins, &board.TagTitles) if err != nil { switch err { case pgx.ErrNoRows: - return entity.BoardWithContent{}, repository.ErrNoData + return entity.BoardWithContent{}, "", repository.ErrNoData default: - return entity.BoardWithContent{}, fmt.Errorf("scan result of get board by id query: %w", err) + return entity.BoardWithContent{}, "", fmt.Errorf("scan result of get board by id query: %w", err) } } - - return board, nil + return board, username, nil } func (repo *boardRepoPG) GetBoardInfoForUpdate(ctx context.Context, boardID int, hasAccess bool) (entity.Board, []string, error) { @@ -339,6 +341,31 @@ func (repo *boardRepoPG) AddPinsOnBoard(ctx context.Context, boardID int, pinIds return nil } +func (repo *boardRepoPG) DeletePinFromBoard(ctx context.Context, boardID, pinID int) error { + tx, err := repo.db.Begin(ctx) + if err != nil { + return fmt.Errorf("delete pin from board - start tx: %w", err) + } + + status, err := tx.Exec(ctx, DeletePinFromBoard, boardID, pinID) + if err != nil { + if err := tx.Rollback(ctx); err != nil { + return fmt.Errorf("delete pin from board - rollback tx: %w", err) + } + return fmt.Errorf("delete pin from board - exec: %w", err) + } + + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("delete pin from board - commit tx: %w", err) + } + + if status.RowsAffected() == 0 { + return repository.ErrNoDataAffected + } + + return nil +} + func (b *boardRepoPG) GetProtectionStatusBoard(ctx context.Context, boardID int) (repoBoard.ProtectionBoard, error) { var isPublic bool err := b.db.QueryRow(ctx, SelectProtectionStatusBoard, boardID).Scan(&isPublic) diff --git a/internal/pkg/repository/board/repo.go b/internal/pkg/repository/board/repo.go index 5d1fb4c..d6a2b39 100644 --- a/internal/pkg/repository/board/repo.go +++ b/internal/pkg/repository/board/repo.go @@ -11,7 +11,7 @@ import ( type Repository interface { CreateBoard(ctx context.Context, board entity.Board, tagTitles []string) (int, error) GetBoardsByUserID(ctx context.Context, userID int, isAuthor bool, accessableBoardsIDs []int) ([]entity.BoardWithContent, error) - GetBoardByID(ctx context.Context, boardID int, hasAccess bool) (board entity.BoardWithContent, err error) + GetBoardByID(ctx context.Context, boardID int, hasAccess bool) (board entity.BoardWithContent, username string, err error) GetBoardAuthorByBoardID(ctx context.Context, boardID int) (int, error) GetContributorsByBoardID(ctx context.Context, boardID int) ([]uEntity.User, error) GetContributorBoardsIDs(ctx context.Context, contributorID int) ([]int, error) @@ -20,6 +20,7 @@ type Repository interface { DeleteBoardByID(ctx context.Context, boardID int) error RoleUserHaveOnThisBoard(ctx context.Context, boardID int, userID int) (UserRole, error) AddPinsOnBoard(ctx context.Context, boardID int, pinIds []int) error + DeletePinFromBoard(ctx context.Context, boardID, pinID int) error GetProtectionStatusBoard(ctx context.Context, boardID int) (ProtectionBoard, error) } diff --git a/internal/pkg/repository/comment/mock/comment_mock.go b/internal/pkg/repository/comment/mock/comment_mock.go new file mode 100644 index 0000000..21cd598 --- /dev/null +++ b/internal/pkg/repository/comment/mock/comment_mock.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repo.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + comment "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + gomock "github.com/golang/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// AddComment mocks base method. +func (m *MockRepository) AddComment(ctx context.Context, comment *comment.Comment) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddComment", ctx, comment) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddComment indicates an expected call of AddComment. +func (mr *MockRepositoryMockRecorder) AddComment(ctx, comment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddComment", reflect.TypeOf((*MockRepository)(nil).AddComment), ctx, comment) +} + +// EditStatusCommentOnDeletedByID mocks base method. +func (m *MockRepository) EditStatusCommentOnDeletedByID(ctx context.Context, id int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EditStatusCommentOnDeletedByID", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// EditStatusCommentOnDeletedByID indicates an expected call of EditStatusCommentOnDeletedByID. +func (mr *MockRepositoryMockRecorder) EditStatusCommentOnDeletedByID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditStatusCommentOnDeletedByID", reflect.TypeOf((*MockRepository)(nil).EditStatusCommentOnDeletedByID), ctx, id) +} + +// GetCommensToPin mocks base method. +func (m *MockRepository) GetCommensToPin(ctx context.Context, pinID, lastID, count int) ([]comment.Comment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCommensToPin", ctx, pinID, lastID, count) + ret0, _ := ret[0].([]comment.Comment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCommensToPin indicates an expected call of GetCommensToPin. +func (mr *MockRepositoryMockRecorder) GetCommensToPin(ctx, pinID, lastID, count interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommensToPin", reflect.TypeOf((*MockRepository)(nil).GetCommensToPin), ctx, pinID, lastID, count) +} + +// GetCommentByID mocks base method. +func (m *MockRepository) GetCommentByID(ctx context.Context, id int) (*comment.Comment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCommentByID", ctx, id) + ret0, _ := ret[0].(*comment.Comment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCommentByID indicates an expected call of GetCommentByID. +func (mr *MockRepositoryMockRecorder) GetCommentByID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommentByID", reflect.TypeOf((*MockRepository)(nil).GetCommentByID), ctx, id) +} diff --git a/internal/pkg/repository/comment/queries.go b/internal/pkg/repository/comment/queries.go new file mode 100644 index 0000000..c944664 --- /dev/null +++ b/internal/pkg/repository/comment/queries.go @@ -0,0 +1,19 @@ +package comment + +const ( + InsertNewComment = "INSERT INTO comment (author, pin_id, content) VALUES ($1, $2, $3) RETURNING id;" + + UpdateCommentOnDeleted = "UPDATE comment SET deleted_at = now() WHERE id = $1;" + + SelectCommentByID = `SELECT p.id, p.username, p.avatar, c.pin_id, c.content + FROM comment AS c INNER JOIN profile AS p + ON c.author = p.id + WHERE c.id = $1 AND c.deleted_at IS NULL;` + + SelectCommentsByPinID = `SELECT c.id, p.id, p.username, p.avatar, c.content + FROM comment AS c INNER JOIN profile AS p + ON c.author = p.id + WHERE c.pin_id = $1 AND (c.id < $2 OR $2 = 0) AND c.deleted_at IS NULL + ORDER BY c.id DESC + LIMIT $3;` +) diff --git a/internal/pkg/repository/comment/repo.go b/internal/pkg/repository/comment/repo.go new file mode 100644 index 0000000..f0b3715 --- /dev/null +++ b/internal/pkg/repository/comment/repo.go @@ -0,0 +1,87 @@ +package comment + +import ( + "context" + "errors" + "fmt" + + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/internal/pgtype" +) + +//go:generate mockgen -destination=./mock/comment_mock.go -package=mock -source=repo.go Repository +type Repository interface { + AddComment(ctx context.Context, comment *entity.Comment) (int, error) + GetCommentByID(ctx context.Context, id int) (*entity.Comment, error) + EditStatusCommentOnDeletedByID(ctx context.Context, id int) error + GetCommensToPin(ctx context.Context, pinID, lastID, count int) ([]entity.Comment, error) +} + +var ErrUserRequired = errors.New("the comment does not have its author specified") + +type commentRepoPG struct { + db pgtype.PgxPoolIface +} + +func NewCommentRepoPG(db pgtype.PgxPoolIface) *commentRepoPG { + return &commentRepoPG{db} +} + +func (c *commentRepoPG) AddComment(ctx context.Context, comment *entity.Comment) (int, error) { + if comment.Author == nil { + return 0, ErrUserRequired + } + + var idInsertedComment int + err := c.db.QueryRow(ctx, InsertNewComment, comment.Author.ID, comment.PinID, comment.Content). + Scan(&idInsertedComment) + if err != nil { + return 0, fmt.Errorf("add comment in storage: %w", err) + } + return idInsertedComment, nil +} + +func (c *commentRepoPG) GetCommentByID(ctx context.Context, id int) (*entity.Comment, error) { + comment := &entity.Comment{ID: id, Author: &user.User{}} + + err := c.db.QueryRow(ctx, SelectCommentByID, id). + Scan(&comment.Author.ID, &comment.Author.Username, &comment.Author.Avatar, &comment.PinID, &comment.Content) + if err != nil { + return nil, fmt.Errorf("get comment by id from storage: %w", err) + } + + return comment, nil +} + +func (c *commentRepoPG) EditStatusCommentOnDeletedByID(ctx context.Context, id int) error { + if _, err := c.db.Exec(ctx, UpdateCommentOnDeleted, id); err != nil { + return fmt.Errorf("edit status comment on deleted comment by id from storage: %w", err) + } + return nil +} + +func (c *commentRepoPG) GetCommensToPin(ctx context.Context, pinID, lastID, count int) ([]entity.Comment, error) { + rows, err := c.db.Query(ctx, SelectCommentsByPinID, pinID, lastID, count) + if err != nil { + return nil, fmt.Errorf("get comments to pin from storage: %w", err) + } + defer rows.Close() + + cmts := make([]entity.Comment, 0, count) + cmt := entity.Comment{ + Author: &user.User{}, + PinID: pinID, + } + + for rows.Next() { + cmt.Author = &user.User{} + err = rows.Scan(&cmt.ID, &cmt.Author.ID, &cmt.Author.Username, &cmt.Author.Avatar, &cmt.Content) + if err != nil { + return cmts, fmt.Errorf("scan a comment when getting comments on a pin: %w", err) + } + + cmts = append(cmts, cmt) + } + return cmts, nil +} diff --git a/internal/pkg/repository/repo.go b/internal/pkg/repository/errors.go similarity index 82% rename from internal/pkg/repository/repo.go rename to internal/pkg/repository/errors.go index eb11314..8b2a226 100644 --- a/internal/pkg/repository/repo.go +++ b/internal/pkg/repository/errors.go @@ -1,11 +1,10 @@ package repository -import "errors" - -const ( - TimeFormat = "02.01.2006" +import ( + "errors" ) +// for backward compatibility var ( ErrMethodUnimplemented = errors.New("unimplemented") ErrNoData = errors.New("got no data from repository layer") diff --git a/internal/pkg/repository/message/mock/message_mock.go b/internal/pkg/repository/message/mock/message_mock.go new file mode 100644 index 0000000..b7b9a0e --- /dev/null +++ b/internal/pkg/repository/message/mock/message_mock.go @@ -0,0 +1,124 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repo.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + message "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + gomock "github.com/golang/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// AddNewMessage mocks base method. +func (m *MockRepository) AddNewMessage(ctx context.Context, mes *message.Message) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddNewMessage", ctx, mes) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddNewMessage indicates an expected call of AddNewMessage. +func (mr *MockRepositoryMockRecorder) AddNewMessage(ctx, mes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddNewMessage", reflect.TypeOf((*MockRepository)(nil).AddNewMessage), ctx, mes) +} + +// DelMessage mocks base method. +func (m *MockRepository) DelMessage(ctx context.Context, messageID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DelMessage", ctx, messageID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DelMessage indicates an expected call of DelMessage. +func (mr *MockRepositoryMockRecorder) DelMessage(ctx, messageID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelMessage", reflect.TypeOf((*MockRepository)(nil).DelMessage), ctx, messageID) +} + +// GetMessageByID mocks base method. +func (m *MockRepository) GetMessageByID(ctx context.Context, mesID int) (*message.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMessageByID", ctx, mesID) + ret0, _ := ret[0].(*message.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMessageByID indicates an expected call of GetMessageByID. +func (mr *MockRepositoryMockRecorder) GetMessageByID(ctx, mesID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessageByID", reflect.TypeOf((*MockRepository)(nil).GetMessageByID), ctx, mesID) +} + +// GetMessages mocks base method. +func (m *MockRepository) GetMessages(ctx context.Context, chat message.Chat, count, lastID int) ([]message.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMessages", ctx, chat, count, lastID) + ret0, _ := ret[0].([]message.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMessages indicates an expected call of GetMessages. +func (mr *MockRepositoryMockRecorder) GetMessages(ctx, chat, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessages", reflect.TypeOf((*MockRepository)(nil).GetMessages), ctx, chat, count, lastID) +} + +// GetUserChats mocks base method. +func (m *MockRepository) GetUserChats(ctx context.Context, userID, count, lastID int) (message.FeedUserChats, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserChats", ctx, userID, count, lastID) + ret0, _ := ret[0].(message.FeedUserChats) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserChats indicates an expected call of GetUserChats. +func (mr *MockRepositoryMockRecorder) GetUserChats(ctx, userID, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserChats", reflect.TypeOf((*MockRepository)(nil).GetUserChats), ctx, userID, count, lastID) +} + +// UpdateContentMessage mocks base method. +func (m *MockRepository) UpdateContentMessage(ctx context.Context, messageID int, newContent string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateContentMessage", ctx, messageID, newContent) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateContentMessage indicates an expected call of UpdateContentMessage. +func (mr *MockRepositoryMockRecorder) UpdateContentMessage(ctx, messageID, newContent interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContentMessage", reflect.TypeOf((*MockRepository)(nil).UpdateContentMessage), ctx, messageID, newContent) +} diff --git a/internal/pkg/repository/message/queries.go b/internal/pkg/repository/message/queries.go new file mode 100644 index 0000000..6e3e1f8 --- /dev/null +++ b/internal/pkg/repository/message/queries.go @@ -0,0 +1,22 @@ +package message + +const ( + SelectMessageByID = "SELECT user_from, user_to, content FROM message WHERE id = $1 AND deleted_at IS NULL;" + SelectUserChats = `SELECT max(message.id) AS mmid, profile.id, username, avatar + FROM message INNER JOIN profile ON (user_to = $1 AND user_from = profile.id) OR (user_to = profile.id AND user_from = $1) + WHERE (message.id < $2 OR $2 = 0) + GROUP BY profile.id + ORDER BY mmid DESC + LIMIT $3;` + SelectMessageFromChat = `SELECT id, user_from, user_to, content + FROM message + WHERE deleted_at IS NULL AND (id < $1 OR $1 = 0) AND + (user_from = $2 AND user_to = $3 OR user_from = $3 AND user_to = $2) + ORDER BY id DESC + LIMIT $4;` + + InsertMessage = "INSERT INTO message (user_from, user_to, content) VALUES ($1, $2, $3) RETURNING id;" + + UpdateMessageContent = "UPDATE message SET content = $1 WHERE id = $2;" + UpdateMessageStatusToDeleted = "UPDATE message SET deleted_at = now() WHERE id = $1 AND deleted_at IS NULL;" +) diff --git a/internal/pkg/repository/message/repo.go b/internal/pkg/repository/message/repo.go new file mode 100644 index 0000000..f556efb --- /dev/null +++ b/internal/pkg/repository/message/repo.go @@ -0,0 +1,98 @@ +package message + +import ( + "context" + "fmt" + + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/internal/pgtype" +) + +//go:generate mockgen -destination=./mock/message_mock.go -package=mock -source=repo.go Repository +type Repository interface { + GetMessageByID(ctx context.Context, mesID int) (*entity.Message, error) + AddNewMessage(ctx context.Context, mes *entity.Message) (int, error) + GetMessages(ctx context.Context, chat entity.Chat, count, lastID int) ([]entity.Message, error) + UpdateContentMessage(ctx context.Context, messageID int, newContent string) error + DelMessage(ctx context.Context, messageID int) error + GetUserChats(ctx context.Context, userID, count, lastID int) (entity.FeedUserChats, error) +} + +type messageRepo struct { + db pgtype.PgxPoolIface +} + +func NewMessageRepo(db pgtype.PgxPoolIface) *messageRepo { + return &messageRepo{db} +} + +func (m *messageRepo) GetMessageByID(ctx context.Context, mesID int) (*entity.Message, error) { + message := &entity.Message{ID: mesID} + err := m.db.QueryRow(ctx, SelectMessageByID, mesID).Scan(&message.From, &message.To, &message.Content) + if err != nil { + return nil, fmt.Errorf("get message by id from storage: %w", err) + } + return message, nil +} + +func (m *messageRepo) AddNewMessage(ctx context.Context, mes *entity.Message) (int, error) { + err := m.db.QueryRow(ctx, InsertMessage, mes.From, mes.To, mes.Content).Scan(&mes.ID) + if err != nil { + return 0, fmt.Errorf("add new message in storage: %w", err) + } + return mes.ID, nil +} + +func (m *messageRepo) GetMessages(ctx context.Context, chat entity.Chat, count, lastID int) ([]entity.Message, error) { + rows, err := m.db.Query(ctx, SelectMessageFromChat, lastID, chat[0], chat[1], count) + if err != nil { + return nil, fmt.Errorf("get message for chat from storage: %w", err) + } + + message := entity.Message{} + messages := make([]entity.Message, 0, count) + for rows.Next() { + err = rows.Scan(&message.ID, &message.From, &message.To, &message.Content) + if err != nil { + return messages, fmt.Errorf("scan selected message: %w", err) + } + messages = append(messages, message) + } + return messages, nil +} + +func (m *messageRepo) UpdateContentMessage(ctx context.Context, messageID int, newContent string) error { + _, err := m.db.Exec(ctx, UpdateMessageContent, newContent, messageID) + if err != nil { + return fmt.Errorf("update content message in storage: %w", err) + } + return nil +} + +func (m *messageRepo) DelMessage(ctx context.Context, messageID int) error { + _, err := m.db.Exec(ctx, UpdateMessageStatusToDeleted, messageID) + if err != nil { + return fmt.Errorf("delete message from storage: %w", err) + } + return nil +} + +func (m *messageRepo) GetUserChats(ctx context.Context, userID, count, lastID int) (entity.FeedUserChats, error) { + rows, err := m.db.Query(ctx, SelectUserChats, userID, lastID, count) + if err != nil { + return nil, fmt.Errorf("get user chats in storage: %w", err) + } + defer rows.Close() + + feed := make(entity.FeedUserChats, 0, count) + chat := entity.ChatWithUser{} + for rows.Next() { + if err = rows.Scan(&chat.MessageLastID, &chat.WichWhomChat.ID, + &chat.WichWhomChat.Username, &chat.WichWhomChat.Avatar); err != nil { + + return feed, fmt.Errorf("scan chat with user for feed: %w", err) + } + feed = append(feed, chat) + } + return feed, nil +} diff --git a/internal/pkg/repository/pin/mock/pin_mock.go b/internal/pkg/repository/pin/mock/pin_mock.go index 416cc16..1db2477 100644 --- a/internal/pkg/repository/pin/mock/pin_mock.go +++ b/internal/pkg/repository/pin/mock/pin_mock.go @@ -81,17 +81,17 @@ func (mr *MockRepositoryMockRecorder) DeletePin(ctx, pinID, userID interface{}) } // EditPin mocks base method. -func (m *MockRepository) EditPin(ctx context.Context, pinID int, updateData pin0.S, titleTags []string) error { +func (m *MockRepository) EditPin(ctx context.Context, pinID, userID int, updateData pin0.S, titleTags []string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EditPin", ctx, pinID, updateData, titleTags) + ret := m.ctrl.Call(m, "EditPin", ctx, pinID, userID, updateData, titleTags) ret0, _ := ret[0].(error) return ret0 } // EditPin indicates an expected call of EditPin. -func (mr *MockRepositoryMockRecorder) EditPin(ctx, pinID, updateData, titleTags interface{}) *gomock.Call { +func (mr *MockRepositoryMockRecorder) EditPin(ctx, pinID, userID, updateData, titleTags interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditPin", reflect.TypeOf((*MockRepository)(nil).EditPin), ctx, pinID, updateData, titleTags) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EditPin", reflect.TypeOf((*MockRepository)(nil).EditPin), ctx, pinID, userID, updateData, titleTags) } // GetAuthorPin mocks base method. diff --git a/internal/pkg/repository/search/errors.go b/internal/pkg/repository/search/errors.go new file mode 100644 index 0000000..141884c --- /dev/null +++ b/internal/pkg/repository/search/errors.go @@ -0,0 +1,36 @@ +package search + +import errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" + +type ErrNoUsers struct { +} + +func (e *ErrNoUsers) Error() string { + return "Can't find any user" +} + +func (e *ErrNoUsers) Type() errPkg.Type { + return errPkg.ErrNotFound +} + +type ErrNoBoards struct { +} + +func (e *ErrNoBoards) Error() string { + return "Can't find any board" +} + +func (e *ErrNoBoards) Type() errPkg.Type { + return errPkg.ErrNotFound +} + +type ErrNoPins struct { +} + +func (e *ErrNoPins) Error() string { + return "Can't find any pin" +} + +func (e *ErrNoPins) Type() errPkg.Type { + return errPkg.ErrNotFound +} diff --git a/internal/pkg/repository/search/mock/search_mock.go b/internal/pkg/repository/search/mock/search_mock.go new file mode 100644 index 0000000..026411d --- /dev/null +++ b/internal/pkg/repository/search/mock/search_mock.go @@ -0,0 +1,81 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repo.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + search "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" + gomock "github.com/golang/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// GetFilteredBoards mocks base method. +func (m *MockRepository) GetFilteredBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFilteredBoards", ctx, opts) + ret0, _ := ret[0].([]search.BoardForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFilteredBoards indicates an expected call of GetFilteredBoards. +func (mr *MockRepositoryMockRecorder) GetFilteredBoards(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilteredBoards", reflect.TypeOf((*MockRepository)(nil).GetFilteredBoards), ctx, opts) +} + +// GetFilteredPins mocks base method. +func (m *MockRepository) GetFilteredPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFilteredPins", ctx, opts) + ret0, _ := ret[0].([]search.PinForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFilteredPins indicates an expected call of GetFilteredPins. +func (mr *MockRepositoryMockRecorder) GetFilteredPins(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilteredPins", reflect.TypeOf((*MockRepository)(nil).GetFilteredPins), ctx, opts) +} + +// GetFilteredUsers mocks base method. +func (m *MockRepository) GetFilteredUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFilteredUsers", ctx, opts) + ret0, _ := ret[0].([]search.UserForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFilteredUsers indicates an expected call of GetFilteredUsers. +func (mr *MockRepositoryMockRecorder) GetFilteredUsers(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilteredUsers", reflect.TypeOf((*MockRepository)(nil).GetFilteredUsers), ctx, opts) +} diff --git a/internal/pkg/repository/search/postgres/builder.go b/internal/pkg/repository/search/postgres/builder.go new file mode 100644 index 0000000..989c22c --- /dev/null +++ b/internal/pkg/repository/search/postgres/builder.go @@ -0,0 +1,131 @@ +package search + +import ( + "fmt" + + "github.com/Masterminds/squirrel" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" +) + +func SetUserSortType(sq squirrel.SelectBuilder, opts *search.SearchOpts) squirrel.SelectBuilder { + switch opts.SortBy { + case "subscribers": + return sq.OrderBy(fmt.Sprintf("subscribers %s", opts.General.SortOrder)) + default: + return sq.OrderBy(fmt.Sprintf("p1.id %s", opts.General.SortOrder)) + } +} + +func SetBoardSortType(sq squirrel.SelectBuilder, opts *search.SearchOpts) squirrel.SelectBuilder { + switch opts.SortBy { + case "pins": + return sq.OrderBy(fmt.Sprintf("pins_number %s", opts.General.SortOrder)) + default: + return sq.OrderBy(fmt.Sprintf("board.id %s", opts.General.SortOrder)) + } +} + +func SetPinSortType(sq squirrel.SelectBuilder, opts *search.SearchOpts) squirrel.SelectBuilder { + switch opts.SortBy { + case "likes": + return sq.OrderBy(fmt.Sprintf("likes %s", opts.General.SortOrder)) + default: + return sq.OrderBy(fmt.Sprintf("p.id %s", opts.General.SortOrder)) + } +} + +func (r *searchRepoPG) SelectBoardsForSearch(opts *search.SearchOpts) (string, []interface{}, error) { + SelectBoardsForSearch := r.sqlBuilder.Select( + "board.id", + "board.title", + "board.created_at", + "COUNT(DISTINCT pin.id) FILTER (WHERE pin.deleted_at IS NULL) AS pins_number", + "COALESCE((ARRAY_AGG(DISTINCT pin.picture) FILTER (WHERE pin.deleted_at IS NULL AND pin.picture IS NOT NULL))[:3], ARRAY[]::TEXT[]) AS pins", + ).From( + "board", + ).LeftJoin( + "membership ON board.id = membership.board_id", + ).LeftJoin( + "pin ON membership.pin_id = pin.id", + ).Where( + squirrel.Eq{"board.deleted_at": nil}, + ).Where( + squirrel.ILike{"board.title": defaultSearchTemplate(opts.General.Template).GetTempl()}, + ).Where( + fmt.Sprintf("(board.public OR board.author = %d OR %d IN (SELECT user_id FROM contributor WHERE board_id = board.id))", opts.General.CurrUserID, opts.General.CurrUserID), + ).GroupBy( + "board.id", + "board.title", + "board.created_at", + ) + + SelectBoardsForSearch = SetBoardSortType(SelectBoardsForSearch, opts) + SelectBoardsForSearch = SelectBoardsForSearch.Limit(uint64(opts.General.Count)).Offset(uint64(opts.General.Offset)) + + return SelectBoardsForSearch.ToSql() +} + +func (r *searchRepoPG) SelectUsersForSearch(opts *search.SearchOpts) (string, []interface{}, error) { + SelectUsersForSearch := r.sqlBuilder.Select( + "p1.id", + "p1.username", + "p1.avatar", + "COUNT(s1.who) AS subscribers", + "s2.who IS NOT NULL AS is_subscribed", + ).From( + "profile p1", + ).LeftJoin( + "subscription_user s1 ON p1.id = s1.whom", + ).LeftJoin( + "profile p2 ON s1.who = p2.id", + ).LeftJoin( + fmt.Sprintf("subscription_user s2 ON p1.id = s2.whom AND s2.who = %d", opts.General.CurrUserID), + ).Where( + squirrel.And{ + squirrel.Eq{"p1.deleted_at": nil}, + squirrel.Eq{"p2.deleted_at": nil}, + squirrel.ILike{"p1.username": defaultSearchTemplate(opts.General.Template).GetTempl()}, + }, + ).GroupBy( + "p1.id", + "p1.username", + "p1.avatar", + "s2.who IS NOT NULL", + ) + + SelectUsersForSearch = SetUserSortType(SelectUsersForSearch, opts) + SelectUsersForSearch = SelectUsersForSearch.Limit(uint64(opts.General.Count)).Offset(uint64(opts.General.Offset)) + + return SelectUsersForSearch.ToSql() +} + +func (r *searchRepoPG) SelectPinsForSearch(opts *search.SearchOpts) (string, []interface{}, error) { + SelectPinsForSearch := r.sqlBuilder.Select( + "p.id", + "p.title", + "p.picture", + "COUNT(pin_id) AS likes", + ).From( + "pin p", + ).LeftJoin( + "like_pin lp ON p.id = lp.pin_id", + ).Where( + squirrel.And{ + squirrel.Eq{"p.deleted_at": nil}, + squirrel.Or{ + squirrel.Eq{"p.public": true}, + squirrel.Eq{"p.author": opts.General.CurrUserID}, + }, + squirrel.ILike{"p.title": defaultSearchTemplate(opts.General.Template).GetTempl()}, + }, + ).GroupBy( + "p.id", + "p.title", + "p.picture", + ) + + SelectPinsForSearch = SetPinSortType(SelectPinsForSearch, opts) + SelectPinsForSearch = SelectPinsForSearch.Limit(uint64(opts.General.Count)).Offset(uint64(opts.General.Offset)) + + return SelectPinsForSearch.ToSql() +} diff --git a/internal/pkg/repository/search/postgres/repo.go b/internal/pkg/repository/search/postgres/repo.go new file mode 100644 index 0000000..bc4a7e5 --- /dev/null +++ b/internal/pkg/repository/search/postgres/repo.go @@ -0,0 +1,129 @@ +package search + +import ( + "context" + "errors" + "fmt" + + "github.com/Masterminds/squirrel" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" + errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/internal/pgtype" + searchRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/search" + "github.com/jackc/pgx/v5/pgconn" +) + +type defaultSearchTemplate string + +func (t defaultSearchTemplate) GetTempl() string { + return fmt.Sprintf("%%%s%%", t) +} + +type searchRepoPG struct { + db pgtype.PgxPoolIface + sqlBuilder squirrel.StatementBuilderType +} + +func NewSearchRepoPG(db pgtype.PgxPoolIface) *searchRepoPG { + return &searchRepoPG{db, squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)} +} + +func convertErrorPostgres(err error) error { + + switch err { + case context.DeadlineExceeded: + return &errPkg.ErrTimeoutExceeded{} + } + + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + switch pgErr.SQLState() { + } + } + + return &errPkg.InternalError{Message: err.Error(), Layer: string(errPkg.Repo)} +} + +func (r *searchRepoPG) GetFilteredUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) { + sqlRow, args, err := r.SelectUsersForSearch(opts) + if err != nil { + return nil, convertErrorPostgres(err) + } + rows, err := r.db.Query(ctx, sqlRow, args...) + if err != nil { + return nil, convertErrorPostgres(err) + } + defer rows.Close() + + users := make([]search.UserForSearch, 0) + for rows.Next() { + user := search.UserForSearch{} + if err := rows.Scan(&user.ID, &user.Username, &user.Avatar, &user.SubsCount, &user.HasSubscribeFromCurUser); err != nil { + return nil, convertErrorPostgres(err) + } + users = append(users, user) + } + + if len(users) == 0 && opts.General.Offset == 0 { + return nil, &searchRepo.ErrNoUsers{} + } + + return users, nil +} + +func (r *searchRepoPG) GetFilteredBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) { + + sqlRow, args, err := r.SelectBoardsForSearch(opts) + if err != nil { + return nil, convertErrorPostgres(err) + } + rows, err := r.db.Query(ctx, sqlRow, args...) + if err != nil { + return nil, convertErrorPostgres(err) + } + defer rows.Close() + + boards := make([]search.BoardForSearch, 0) + for rows.Next() { + board := search.BoardForSearch{} + if err := rows.Scan(&board.BoardHeader.ID, &board.BoardHeader.Title, &board.BoardHeader.CreatedAt, &board.PinsNumber, &board.PreviewPins); err != nil { + return nil, convertErrorPostgres(err) + } + boards = append(boards, board) + } + + if len(boards) == 0 && opts.General.Offset == 0 { + return nil, &searchRepo.ErrNoBoards{} + } + + return boards, nil +} + +func (r *searchRepoPG) GetFilteredPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) { + + sqlRow, args, err := r.SelectPinsForSearch(opts) + if err != nil { + return nil, convertErrorPostgres(err) + } + rows, err := r.db.Query(ctx, sqlRow, args...) + if err != nil { + return nil, convertErrorPostgres(err) + } + defer rows.Close() + + pins := make([]search.PinForSearch, 0) + for rows.Next() { + pin := search.PinForSearch{} + if err := rows.Scan(&pin.ID, &pin.Title, &pin.Picture, &pin.Likes); err != nil { + return nil, convertErrorPostgres(err) + } + pins = append(pins, pin) + } + + if len(pins) == 0 && opts.General.Offset == 0 { + return nil, &searchRepo.ErrNoPins{} + } + + return pins, nil + +} diff --git a/internal/pkg/repository/search/repo.go b/internal/pkg/repository/search/repo.go new file mode 100644 index 0000000..b49c815 --- /dev/null +++ b/internal/pkg/repository/search/repo.go @@ -0,0 +1,14 @@ +package search + +import ( + "context" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" +) + +//go:generate mockgen -destination=./mock/search_mock.go -package=mock -source=repo.go Repository +type Repository interface { + GetFilteredUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) + GetFilteredPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) + GetFilteredBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) +} diff --git a/internal/pkg/repository/subscription/errors.go b/internal/pkg/repository/subscription/errors.go new file mode 100644 index 0000000..79d236d --- /dev/null +++ b/internal/pkg/repository/subscription/errors.go @@ -0,0 +1,23 @@ +package subscription + +import errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" + +type ErrSubscriptionAlreadyExist struct{} + +func (e *ErrSubscriptionAlreadyExist) Error() string { + return "subscription on that user already exists" +} + +func (e *ErrSubscriptionAlreadyExist) Type() errPkg.Type { + return errPkg.ErrAlreadyExists +} + +type ErrNonExistingSubscription struct{} + +func (e *ErrNonExistingSubscription) Error() string { + return "such subscription doesn't exist" +} + +func (e *ErrNonExistingSubscription) Type() errPkg.Type { + return errPkg.ErrNotFound +} diff --git a/internal/pkg/repository/subscription/mock/subscription_mock.go b/internal/pkg/repository/subscription/mock/subscription_mock.go new file mode 100644 index 0000000..28f115a --- /dev/null +++ b/internal/pkg/repository/subscription/mock/subscription_mock.go @@ -0,0 +1,94 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repo.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + user "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + gomock "github.com/golang/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// CreateSubscriptionUser mocks base method. +func (m *MockRepository) CreateSubscriptionUser(ctx context.Context, from, to int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSubscriptionUser", ctx, from, to) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateSubscriptionUser indicates an expected call of CreateSubscriptionUser. +func (mr *MockRepositoryMockRecorder) CreateSubscriptionUser(ctx, from, to interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscriptionUser", reflect.TypeOf((*MockRepository)(nil).CreateSubscriptionUser), ctx, from, to) +} + +// DeleteSubscriptionUser mocks base method. +func (m *MockRepository) DeleteSubscriptionUser(ctx context.Context, from, to int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSubscriptionUser", ctx, from, to) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSubscriptionUser indicates an expected call of DeleteSubscriptionUser. +func (mr *MockRepositoryMockRecorder) DeleteSubscriptionUser(ctx, from, to interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubscriptionUser", reflect.TypeOf((*MockRepository)(nil).DeleteSubscriptionUser), ctx, from, to) +} + +// GetUserSubscribers mocks base method. +func (m *MockRepository) GetUserSubscribers(ctx context.Context, userID, count, lastID, currUserID int) ([]user.SubscriptionUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserSubscribers", ctx, userID, count, lastID, currUserID) + ret0, _ := ret[0].([]user.SubscriptionUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserSubscribers indicates an expected call of GetUserSubscribers. +func (mr *MockRepositoryMockRecorder) GetUserSubscribers(ctx, userID, count, lastID, currUserID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSubscribers", reflect.TypeOf((*MockRepository)(nil).GetUserSubscribers), ctx, userID, count, lastID, currUserID) +} + +// GetUserSubscriptions mocks base method. +func (m *MockRepository) GetUserSubscriptions(ctx context.Context, userID, count, lastID, currUserID int) ([]user.SubscriptionUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserSubscriptions", ctx, userID, count, lastID, currUserID) + ret0, _ := ret[0].([]user.SubscriptionUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserSubscriptions indicates an expected call of GetUserSubscriptions. +func (mr *MockRepositoryMockRecorder) GetUserSubscriptions(ctx, userID, count, lastID, currUserID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSubscriptions", reflect.TypeOf((*MockRepository)(nil).GetUserSubscriptions), ctx, userID, count, lastID, currUserID) +} diff --git a/internal/pkg/repository/subscription/postgres/queries.go b/internal/pkg/repository/subscription/postgres/queries.go new file mode 100644 index 0000000..4a90c47 --- /dev/null +++ b/internal/pkg/repository/subscription/postgres/queries.go @@ -0,0 +1,36 @@ +package subscription + +var ( + CreateSubscriptionUser = "INSERT INTO subscription_user (who, whom) values ($1, $2);" + DeleteSubscriptionUser = "DELETE FROM subscription_user WHERE who = $1 AND whom = $2;" + GetUserSubscriptions = ` + SELECT + p.id, p.username, p.avatar, s.who IS NOT NULL AS is_subscribed + FROM + subscription_user f + LEFT JOIN + profile p ON f.whom = p.id + LEFT JOIN + subscription_user s ON f.whom = s.whom AND s.who = $1 + WHERE + f.who = $2 AND p.deleted_at IS NULL AND f.whom < $3 + ORDER BY + f.whom DESC + LIMIT + $4;` + GetUserSubscribers = ` + SELECT + p.id, p.username, p.avatar, s.who IS NOT NULL AS is_subscribed + FROM + subscription_user f + LEFT JOIN + profile p ON f.who = p.id + LEFT JOIN + subscription_user s ON f.who = s.whom AND s.who = $1 + WHERE + f.whom = $2 AND p.deleted_at IS NULL AND f.who < $3 + ORDER BY + f.who DESC + LIMIT + $4;` +) diff --git a/internal/pkg/repository/subscription/postgres/repo.go b/internal/pkg/repository/subscription/postgres/repo.go new file mode 100644 index 0000000..49737de --- /dev/null +++ b/internal/pkg/repository/subscription/postgres/repo.go @@ -0,0 +1,120 @@ +package subscription + +import ( + "context" + "errors" + "strconv" + + "github.com/Masterminds/squirrel" + userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/internal/pgtype" + subRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/subscription" + "github.com/jackc/pgx/v5/pgconn" +) + +type subscriptionRepoPG struct { + db pgtype.PgxPoolIface + sqlBuilder squirrel.StatementBuilderType +} + +func NewSubscriptionRepoPG(db pgtype.PgxPoolIface) subRepo.Repository { + return &subscriptionRepoPG{db: db, sqlBuilder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)} +} + +func convertErrorPostgres(err error) error { + + switch err { + case context.DeadlineExceeded: + return &errPkg.ErrTimeoutExceeded{} + } + + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + switch pgErr.SQLState() { + case strconv.Itoa(23505): + return &subRepo.ErrSubscriptionAlreadyExist{} + } + } + return &errPkg.InternalError{Message: err.Error(), Layer: string(errPkg.Repo)} +} + +func (r *subscriptionRepoPG) CreateSubscriptionUser(ctx context.Context, from, to int) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return convertErrorPostgres(err) + } + + if _, err = tx.Exec(ctx, CreateSubscriptionUser, from, to); err != nil { + if err := tx.Rollback(ctx); err != nil { + return convertErrorPostgres(err) + } + return convertErrorPostgres(err) + } + + if err = tx.Commit(ctx); err != nil { + return convertErrorPostgres(err) + } + return nil +} + +func (r *subscriptionRepoPG) DeleteSubscriptionUser(ctx context.Context, from, to int) error { + tx, err := r.db.Begin(ctx) + if err != nil { + return convertErrorPostgres(err) + } + + status, err := tx.Exec(ctx, DeleteSubscriptionUser, from, to) + if err != nil { + if err := tx.Rollback(ctx); err != nil { + return convertErrorPostgres(err) + } + return convertErrorPostgres(err) + } + + if err = tx.Commit(ctx); err != nil { + return convertErrorPostgres(err) + } + if status.RowsAffected() == 0 { + return &subRepo.ErrNonExistingSubscription{} + } + return nil +} + +func (r *subscriptionRepoPG) GetUserSubscriptions(ctx context.Context, userID, count, lastID int, currUserID int) ([]userEntity.SubscriptionUser, error) { + + rows, err := r.db.Query(ctx, GetUserSubscriptions, currUserID, userID, lastID, count) + if err != nil { + return nil, convertErrorPostgres(err) + } + defer rows.Close() + + subscriptions := make([]userEntity.SubscriptionUser, 0) + for rows.Next() { + var subscription userEntity.SubscriptionUser + if err = rows.Scan(&subscription.ID, &subscription.Username, &subscription.Avatar, &subscription.HasSubscribeFromCurUser); err != nil { + return nil, convertErrorPostgres(err) + } + subscriptions = append(subscriptions, subscription) + } + return subscriptions, nil +} + +func (r *subscriptionRepoPG) GetUserSubscribers(ctx context.Context, userID, count, lastID int, currUserID int) ([]userEntity.SubscriptionUser, error) { + + rows, err := r.db.Query(ctx, GetUserSubscribers, currUserID, userID, lastID, count) + if err != nil { + return nil, convertErrorPostgres(err) + } + defer rows.Close() + + subscribers := make([]userEntity.SubscriptionUser, 0) + for rows.Next() { + var subscriber userEntity.SubscriptionUser + if err = rows.Scan(&subscriber.ID, &subscriber.Username, &subscriber.Avatar, &subscriber.HasSubscribeFromCurUser); err != nil { + return nil, convertErrorPostgres(err) + } + subscribers = append(subscribers, subscriber) + } + return subscribers, nil +} diff --git a/internal/pkg/repository/subscription/repo.go b/internal/pkg/repository/subscription/repo.go new file mode 100644 index 0000000..6c8c841 --- /dev/null +++ b/internal/pkg/repository/subscription/repo.go @@ -0,0 +1,15 @@ +package subscription + +import ( + "context" + + userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" +) + +//go:generate mockgen -destination=./mock/subscription_mock.go -package=mock -source=repo.go Repository +type Repository interface { + CreateSubscriptionUser(ctx context.Context, from, to int) error + DeleteSubscriptionUser(ctx context.Context, from, to int) error + GetUserSubscriptions(ctx context.Context, userID, count, lastID int, currUserID int) ([]userEntity.SubscriptionUser, error) + GetUserSubscribers(ctx context.Context, userID, count, lastID int, currUserID int) ([]userEntity.SubscriptionUser, error) +} diff --git a/internal/pkg/repository/user/errors.go b/internal/pkg/repository/user/errors.go new file mode 100644 index 0000000..e998ec3 --- /dev/null +++ b/internal/pkg/repository/user/errors.go @@ -0,0 +1,13 @@ +package user + +import errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" + +type ErrNonExistingUser struct{} + +func (e *ErrNonExistingUser) Error() string { + return "user doesn't exist" +} + +func (e *ErrNonExistingUser) Type() errPkg.Type { + return errPkg.ErrNotFound +} diff --git a/internal/pkg/repository/user/mock/user_mock.go b/internal/pkg/repository/user/mock/user_mock.go index 3fae4f7..2ecaa66 100644 --- a/internal/pkg/repository/user/mock/user_mock.go +++ b/internal/pkg/repository/user/mock/user_mock.go @@ -50,6 +50,20 @@ func (mr *MockRepositoryMockRecorder) AddNewUser(ctx, user interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddNewUser", reflect.TypeOf((*MockRepository)(nil).AddNewUser), ctx, user) } +// CheckUserExistence mocks base method. +func (m *MockRepository) CheckUserExistence(ctx context.Context, userID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckUserExistence", ctx, userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckUserExistence indicates an expected call of CheckUserExistence. +func (mr *MockRepositoryMockRecorder) CheckUserExistence(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckUserExistence", reflect.TypeOf((*MockRepository)(nil).CheckUserExistence), ctx, userID) +} + // EditUserAvatar mocks base method. func (m *MockRepository) EditUserAvatar(ctx context.Context, userID int, avatar string) error { m.ctrl.T.Helper() @@ -93,6 +107,22 @@ func (mr *MockRepositoryMockRecorder) GetAllUserData(ctx, userID interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllUserData", reflect.TypeOf((*MockRepository)(nil).GetAllUserData), ctx, userID) } +// GetProfileData mocks base method. +func (m *MockRepository) GetProfileData(ctx context.Context, userID int) (*user.User, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProfileData", ctx, userID) + ret0, _ := ret[0].(*user.User) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetProfileData indicates an expected call of GetProfileData. +func (mr *MockRepositoryMockRecorder) GetProfileData(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfileData", reflect.TypeOf((*MockRepository)(nil).GetProfileData), ctx, userID) +} + // GetUserByUsername mocks base method. func (m *MockRepository) GetUserByUsername(ctx context.Context, username string) (*user.User, error) { m.ctrl.T.Helper() @@ -108,6 +138,23 @@ func (mr *MockRepositoryMockRecorder) GetUserByUsername(ctx, username interface{ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByUsername", reflect.TypeOf((*MockRepository)(nil).GetUserByUsername), ctx, username) } +// GetUserData mocks base method. +func (m *MockRepository) GetUserData(ctx context.Context, userID, currUserID int) (*user.User, bool, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserData", ctx, userID, currUserID) + ret0, _ := ret[0].(*user.User) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(int) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// GetUserData indicates an expected call of GetUserData. +func (mr *MockRepositoryMockRecorder) GetUserData(ctx, userID, currUserID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserData", reflect.TypeOf((*MockRepository)(nil).GetUserData), ctx, userID, currUserID) +} + // GetUserIdByUsername mocks base method. func (m *MockRepository) GetUserIdByUsername(ctx context.Context, username string) (int, error) { m.ctrl.T.Helper() diff --git a/internal/pkg/repository/user/queries.go b/internal/pkg/repository/user/queries.go index f94d2b9..677b99c 100644 --- a/internal/pkg/repository/user/queries.go +++ b/internal/pkg/repository/user/queries.go @@ -10,4 +10,35 @@ var ( UpdateAvatarProfile = "UPDATE profile SET avatar = $1 WHERE id = $2;" SelectUserIdByUsername = "SELECT id FROM profile WHERE username = $1;" SelectLastUserID = "SELECT id FROM profile ORDER BY id DESC LIMIT 1;" + CheckUserExistence = "SELECT username FROM profile WHERE id = $1 AND deleted_at IS NULL;" + GetUserInfo = ` + SELECT + p1.id, p1.username, p1.avatar, COALESCE(p1.name, '') name, COALESCE(p1.surname, '') surname, COALESCE(p1.about_me, '') about_me, s2.who IS NOT NULL as is_subscribed, COUNT(s1.who) subscribers + FROM + profile p1 + LEFT JOIN + subscription_user s1 ON p1.id = s1.whom + LEFT JOIN + profile p2 ON s1.who = p2.id + LEFT JOIN + subscription_user s2 ON s1.whom = s2.whom AND s2.who = $1 + WHERE + p1.id = $2 AND p1.deleted_at IS NULL AND p2.deleted_at IS NULL + GROUP BY + p1.id, p1.username,p1.avatar, p1.name, p1.surname, p1.about_me, s2.who IS NOT NULL; + ` + GetProfileInfo = ` + SELECT + p1.id, p1.username, p1.avatar, COUNT(s.who) subscribers + FROM + profile p1 + LEFT JOIN + subscription_user s ON p1.id = s.whom + LEFT JOIN + profile p2 ON s.who = p2.id + WHERE + p1.id = $1 AND p1.deleted_at IS NULL AND p2.deleted_at IS NULL + GROUP BY + p1.id, p1.username, p1.avatar; + ` ) diff --git a/internal/pkg/repository/user/repo.go b/internal/pkg/repository/user/repo.go index 1fb9deb..ea4d0be 100644 --- a/internal/pkg/repository/user/repo.go +++ b/internal/pkg/repository/user/repo.go @@ -2,12 +2,15 @@ package user import ( "context" + "errors" "fmt" sq "github.com/Masterminds/squirrel" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/internal/pgtype" ) @@ -18,6 +21,9 @@ type Repository interface { GetUserByUsername(ctx context.Context, username string) (*user.User, error) GetUsernameAndAvatarByID(ctx context.Context, userID int) (username string, avatar string, err error) GetUserIdByUsername(ctx context.Context, username string) (int, error) + GetUserData(ctx context.Context, userID, currUserID int) (user_ *user.User, isSubscribed bool, subsCount int, err error) + GetProfileData(ctx context.Context, userID int) (user_ *user.User, subsCount int, err error) + CheckUserExistence(ctx context.Context, userID int) error EditUserAvatar(ctx context.Context, userID int, avatar string) error GetAllUserData(ctx context.Context, userID int) (*user.User, error) EditUserInfo(ctx context.Context, userID int, updateFields S) error @@ -33,6 +39,34 @@ func NewUserRepoPG(db pgtype.PgxPoolIface) *userRepoPG { return &userRepoPG{db} } +func convertErrorPostgres(err error) error { + + switch err { + case pgx.ErrNoRows: + return &ErrNonExistingUser{} + case context.DeadlineExceeded: + return &errPkg.ErrTimeoutExceeded{} + } + + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + switch pgErr.SQLState() { + // add SQL states if necessary + } + } + return &errPkg.InternalError{Message: err.Error(), Layer: string(errPkg.Repo)} +} + +func (u *userRepoPG) CheckUserExistence(ctx context.Context, userID int) error { + row := u.db.QueryRow(ctx, CheckUserExistence, userID) + var dummy string + if err := row.Scan(&dummy); err != nil { + return convertErrorPostgres(err) + } + + return nil +} + func (u *userRepoPG) AddNewUser(ctx context.Context, user *user.User) error { _, err := u.db.Exec(ctx, InsertNewUser, user.Username, user.Password, user.Email) if err != nil { @@ -60,6 +94,27 @@ func (u *userRepoPG) GetUsernameAndAvatarByID(ctx context.Context, userID int) ( return } +func (u *userRepoPG) GetUserData(ctx context.Context, userID, currUserID int) (user_ *user.User, isSubscribed bool, subsCount int, err error) { + user_ = &user.User{} + if err := u.db.QueryRow(ctx, GetUserInfo, currUserID, userID).Scan( + &user_.ID, &user_.Username, &user_.Avatar, &user_.Name, &user_.Surname, + &user_.AboutMe, &isSubscribed, &subsCount, + ); err != nil { + return nil, false, 0, convertErrorPostgres(err) + } + return user_, isSubscribed, subsCount, nil +} + +func (u *userRepoPG) GetProfileData(ctx context.Context, userID int) (user_ *user.User, subsCount int, err error) { + user_ = &user.User{} + if err := u.db.QueryRow(ctx, GetProfileInfo, userID).Scan( + &user_.ID, &user_.Username, &user_.Avatar, &subsCount, + ); err != nil { + return nil, 0, convertErrorPostgres(err) + } + return user_, subsCount, nil +} + func (u *userRepoPG) EditUserAvatar(ctx context.Context, userID int, avatar string) error { _, err := u.db.Exec(ctx, UpdateAvatarProfile, avatar, userID) if err != nil { diff --git a/internal/pkg/service/auth.go b/internal/pkg/service/auth.go deleted file mode 100644 index e146083..0000000 --- a/internal/pkg/service/auth.go +++ /dev/null @@ -1,226 +0,0 @@ -package service - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" - usecase "github.com/go-park-mail-ru/2023_2_OND_team/internal/usecases/user" - log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" -) - -// Login godoc -// -// @Description User login, check authentication, get user info -// @Tags Auth -// @Produce json -// @Param session_key header string false "Auth session id" example(senjs7rvdnrgkjdr) -// @Success 200 {object} JsonResponse{body=user.User} -// @Failure 400 {object} JsonErrResponse -// @Failure 404 {object} JsonErrResponse -// @Failure 500 {object} JsonErrResponse -// @Router /api/v1/auth/login [get] -func (s *Service) CheckLogin(w http.ResponseWriter, r *http.Request) { - s.log.Info("request on check login", log.F{"method", r.Method}, log.F{"path", r.URL.Path}) - SetContentTypeJSON(w) - - cookie, err := r.Cookie("session_key") - if err != nil { - s.log.Info("no cookie", log.F{"error", err.Error()}) - err = responseError(w, "no_auth", "the user is not logged in") - if err != nil { - s.log.Error(err.Error()) - } - return - } - - userID, err := s.sm.GetUserIDBySessionKey(r.Context(), cookie.Value) - if err != nil { - err = responseError(w, "no_auth", "no user session found") - if err != nil { - s.log.Error(err.Error()) - } - return - } - - username, avatar, err := s.userCase.FindOutUsernameAndAvatar(r.Context(), userID) - if err != nil { - s.log.Error(err.Error()) - err = responseError(w, "no_auth", "no user was found for this session") - } else { - err = responseOk(w, "user found", map[string]string{"username": username, "avatar": avatar}) - } - if err != nil { - s.log.Error(err.Error()) - } -} - -// Login godoc -// -// @Description User login, creating new session -// @Tags Auth -// @Accept json -// @Produce json -// @Param username body string true "Username" example(clicker123) -// @Param password body string true "Password" example(safe_pass) -// @Success 200 {object} JsonResponse -// @Failure 400 {object} JsonErrResponse -// @Failure 404 {object} JsonErrResponse -// @Failure 500 {object} JsonErrResponse -// @Header 200 {string} session_key "Auth cookie with new valid session id" -// @Router /api/v1/auth/login [post] -func (s *Service) Login(w http.ResponseWriter, r *http.Request) { - s.log.Info("request on signup", log.F{"method", r.Method}, log.F{"path", r.URL.Path}) - SetContentTypeJSON(w) - - defer r.Body.Close() - params := usecase.NewCredentials() - err := json.NewDecoder(r.Body).Decode(¶ms) - if err != nil { - s.log.Info("failed to parse parameters", log.F{"error", err.Error()}) - err = responseError(w, "parse_body", "the correct username and password are expected to be received in JSON format") - if err != nil { - s.log.Error(err.Error()) - } - return - } - - if !isValidPassword(params.Password) || !isValidUsername(params.Username) { - s.log.Info("invalid credentials") - err = responseError(w, "invalid_credentials", "invalid user credentials") - if err != nil { - s.log.Error(err.Error()) - } - return - } - - user, err := s.userCase.Authentication(r.Context(), params) - if err != nil { - s.log.Warn(err.Error()) - err = responseError(w, "bad_credentials", "incorrect user credentials") - if err != nil { - s.log.Error(err.Error()) - } - return - } - - session, err := s.sm.CreateNewSessionForUser(r.Context(), user.ID) - if err != nil { - s.log.Error(err.Error()) - err = responseError(w, "session", "failed to create a session for the user") - if err != nil { - s.log.Error(err.Error()) - } - return - } - - cookie := &http.Cookie{ - Name: "session_key", - Value: session.Key, - HttpOnly: true, - Secure: true, - Path: "/", - Expires: session.Expire, - SameSite: http.SameSiteStrictMode, - } - http.SetCookie(w, cookie) - - err = responseOk(w, "a new session has been created for the user", nil) - if err != nil { - s.log.Error(err.Error()) - } -} - -// SignUp godoc -// -// @Description User registration -// @Tags Auth -// @Accept json -// @Produce json -// @Param username body string true "Username" example(clicker123) -// @Param email body string true "Email" example(clickkk@gmail.com) -// @Param password body string true "Password" example(safe_pass) -// @Success 200 {object} JsonResponse -// @Failure 400 {object} JsonErrResponse -// @Failure 404 {object} JsonErrResponse -// @Failure 500 {object} JsonErrResponse -// @Router /api/v1/auth/signup [post] -func (s *Service) Signup(w http.ResponseWriter, r *http.Request) { - s.log.Info("request on signup", log.F{"method", r.Method}, log.F{"path", r.URL.Path}) - SetContentTypeJSON(w) - - defer r.Body.Close() - user := &user.User{} - err := json.NewDecoder(r.Body).Decode(user) - if err != nil { - s.log.Info("failed to parse parameters", log.F{"error", err.Error()}) - err = responseError(w, "parse_body", "the correct username, email and password are expected to be received in JSON format") - if err != nil { - s.log.Error(err.Error()) - } - return - } - - if err := IsValidUserForRegistration(user); err != nil { - s.log.Info("invalid user registration data") - err = responseError(w, "invalid_params", err.Error()) - if err != nil { - s.log.Error(err.Error()) - } - return - } - - err = s.userCase.Register(r.Context(), user) - if err != nil { - s.log.Warn(err.Error()) - err = responseError(w, "uniq_fields", "there is already an account with this username or password") - } else { - err = responseOk(w, "the user has been successfully registered", nil) - } - if err != nil { - s.log.Error(err.Error()) - } -} - -// Logout godoc -// -// @Description User logout, session deletion -// @Tags Auth -// @Produce json -// @Param session_key header string false "Auth session id" example(senjs7rvdnrgkjdr) -// -// @Success 200 {object} JsonResponse -// @Failure 400 {object} JsonErrResponse -// @Failure 404 {object} JsonErrResponse -// @Failure 500 {object} JsonErrResponse -// @Header 200 {string} Session-id "Auth cookie with expired session id" -// @Router /api/v1/auth/logout [delete] -func (s *Service) Logout(w http.ResponseWriter, r *http.Request) { - s.log.Info("request on logout", log.F{"method", r.Method}, log.F{"path", r.URL.Path}) - SetContentTypeJSON(w) - - cookie, err := r.Cookie("session_key") - if err != nil { - s.log.Info("no cookie", log.F{"error", err.Error()}) - err = responseError(w, "no_auth", "to log out, you must first log in") - if err != nil { - s.log.Error(err.Error()) - } - return - } - - cookie.Expires = time.Now().UTC().AddDate(0, -1, 0) - http.SetCookie(w, cookie) - - err = s.sm.DeleteUserSession(r.Context(), cookie.Value) - if err != nil { - s.log.Error(err.Error()) - err = responseError(w, "session", "the user logged out, but his session did not end") - } else { - err = responseOk(w, "the user has successfully logged out", nil) - } - if err != nil { - s.log.Error(err.Error()) - } -} diff --git a/internal/pkg/service/auth_test.go b/internal/pkg/service/auth_test.go deleted file mode 100644 index 384296e..0000000 --- a/internal/pkg/service/auth_test.go +++ /dev/null @@ -1,467 +0,0 @@ -package service - -import ( - "encoding/json" - "io" - "math/rand" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/ramrepo" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/usecases/session" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/usecases/user" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" - "github.com/stretchr/testify/require" -) - -func checkAuthCookie(cookies []*http.Cookie) bool { - if cookies == nil { - return false - } - for _, cookie := range cookies { - if cookie.Name == "session_key" { - return true - } - } - return false -} - -func TestCheckLogin(t *testing.T) { - log, _ := logger.New(logger.RFC3339FormatTime()) - defer log.Sync() - - db, _ := ramrepo.OpenDB(strconv.FormatInt(int64(rand.Int()), 10)) - defer db.Close() - - sm := session.New(log, ramrepo.NewRamSessionRepo(db)) - userCase := user.New(log, ramrepo.NewRamUserRepo(db)) - service := New(log, sm, userCase, nil) - - url := "https://domain.test:8080/api/v1/login" - goodCases := []struct { - name string - cookie *http.Cookie - expResp JsonResponse - }{ - { - "sending valid session_key", - &http.Cookie{ - Name: "session_key", - Value: "461afabf38b3147c", - }, - JsonResponse{ - Status: "ok", - Message: "user found", - Body: map[string]interface{}{"username": "dogsLover", "avatar": "https://pinspire.online:8081/upload/avatars/default-avatar.png"}, - }, - }, - } - - for _, tCase := range goodCases { - t.Run(tCase.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, url, nil) - req.AddCookie(tCase.cookie) - w := httptest.NewRecorder() - - service.CheckLogin(w, req) - - var actualResp JsonResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - actualResp.Body = actualResp.Body.(map[string]interface{}) - require.Equal(t, tCase.expResp, actualResp) - }) - } - - badCases := []struct { - name string - cookie *http.Cookie - expResp JsonErrResponse - }{ - { - "sending empty cookie", - &http.Cookie{ - Name: "", - Value: "", - }, - JsonErrResponse{ - Status: "error", - Message: "the user is not logged in", - Code: "no_auth", - }, - }, - { - "sending invalid cookie", - &http.Cookie{ - Name: "session_key", - Value: "doesn't exist", - }, - JsonErrResponse{ - Status: "error", - Message: "no user session found", - Code: "no_auth", - }, - }, - { - "sending cookie with invald user", - &http.Cookie{ - Name: "session_key", - Value: "f4280a941b664d02", - }, - JsonErrResponse{ - Status: "error", - Message: "no user was found for this session", - Code: "no_auth", - }, - }, - } - - for _, tCase := range badCases { - t.Run(tCase.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, url, nil) - req.AddCookie(tCase.cookie) - w := httptest.NewRecorder() - - service.CheckLogin(w, req) - - var actualResp JsonErrResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - require.Equal(t, tCase.expResp, actualResp) - }) - } - -} - -func TestLogin(t *testing.T) { - url := "https://domain.test:8080/api/v1/login" - log, _ := logger.New(logger.RFC3339FormatTime()) - defer log.Sync() - - db, _ := ramrepo.OpenDB(strconv.FormatInt(int64(rand.Int()), 10)) - defer db.Close() - - sm := session.New(log, ramrepo.NewRamSessionRepo(db)) - userCase := user.New(log, ramrepo.NewRamUserRepo(db)) - service := New(log, sm, userCase, nil) - - goodCases := []struct { - name string - rawBody string - expResp JsonResponse - }{ - { - "providing correct and valid user credentials", - `{"username":"dogsLover", "password":"big_string"}`, - JsonResponse{ - Status: "ok", - Message: "a new session has been created for the user", - Body: nil, - }, - }, - } - - for _, tCase := range goodCases { - t.Run(tCase.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, url, io.NopCloser(strings.NewReader(tCase.rawBody))) - w := httptest.NewRecorder() - - service.Login(w, req) - - var actualResp JsonResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - require.Equal(t, tCase.expResp, actualResp) - require.True(t, checkAuthCookie(w.Result().Cookies())) - }) - } - - badCases := []struct { - name string - rawBody string - expResp JsonErrResponse - }{ - { - "providing invalid credentials - broken body", - "{'username': 'dogsLover', 'password': 'big_string'", - JsonErrResponse{ - Status: "error", - Message: "the correct username and password are expected to be received in JSON format", - Code: "parse_body", - }, - }, - { - "providing invalid credentials - no username", - `{"password":"big_string"}`, - JsonErrResponse{ - Status: "error", - Message: "invalid user credentials", - Code: "invalid_credentials", - }, - }, - { - "providing invalid credentials - no password", - `{"username":"dogsLover"}`, - JsonErrResponse{ - Status: "error", - Message: "invalid user credentials", - Code: "invalid_credentials", - }, - }, - { - "providing invalid credentials - short username", - `{"username":"do", "password":"big_string"}`, - JsonErrResponse{ - Status: "error", - Message: "invalid user credentials", - Code: "invalid_credentials", - }, - }, - { - "providing invalid credentials - long username", - `{"username":"dojsbrjfbdrjhbhjldrbgbdrhjgbdjrbgjdhbgjhdbrghbdhj,gbdhjrbgjhdbvkvghkevfghjdvrfhvdhrvbjdfgdrgdr","password":"big_string"}`, - JsonErrResponse{ - Status: "error", - Message: "invalid user credentials", - Code: "invalid_credentials", - }, - }, - { - "providing invalid credentials - short password", - `{"username":"dogsLover","password":"bi"}`, - JsonErrResponse{ - Status: "error", - Message: "invalid user credentials", - Code: "invalid_credentials", - }, - }, - { - "providing invalid credentials - long password", - `{"username":"dogsLover","password":"biyugsgrusgubskhvfhkdgvfgvdvrjgbsjhgjkshzkljfskfwjkhkfjisuidgoquakflsjuzeofiow3i"}`, - JsonErrResponse{ - Status: "error", - Message: "invalid user credentials", - Code: "invalid_credentials", - }, - }, - { - "providing incorrect credentials - no user with such credentials", - `{"username":"dogsLover", "password":"doesn't_exist"}`, - JsonErrResponse{ - Status: "error", - Message: "incorrect user credentials", - Code: "bad_credentials", - }, - }, - } - - for _, tCase := range badCases { - t.Run(tCase.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, url, io.NopCloser(strings.NewReader(tCase.rawBody))) - w := httptest.NewRecorder() - - service.Login(w, req) - - var actualResp JsonErrResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - require.Equal(t, tCase.expResp, actualResp) - require.False(t, checkAuthCookie(w.Result().Cookies())) - }) - } -} - -func TestSignUp(t *testing.T) { - url := "https://domain.test:8080/api/v1/signup" - log, _ := logger.New(logger.RFC3339FormatTime()) - defer log.Sync() - - db, _ := ramrepo.OpenDB(strconv.FormatInt(int64(rand.Int()), 10)) - defer db.Close() - - sm := session.New(log, ramrepo.NewRamSessionRepo(db)) - userCase := user.New(log, ramrepo.NewRamUserRepo(db)) - service := New(log, sm, userCase, nil) - - goodCases := []struct { - name string - rawBody string - expResp JsonResponse - }{ - { - "providing correct and valid data for signup", - `{"username":"newbie", "password":"getHigh123", "email":"world@uandex.ru"}`, - JsonResponse{ - Status: "ok", - Message: "the user has been successfully registered", - Body: nil, - }, - }, - } - - for _, tCase := range goodCases { - t.Run(tCase.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, url, io.NopCloser(strings.NewReader(tCase.rawBody))) - w := httptest.NewRecorder() - - service.Signup(w, req) - - var actualResp JsonResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - require.Equal(t, tCase.expResp, actualResp) - }) - } - - badCases := []struct { - name string - rawBody string - expResp JsonErrResponse - }{ - { - "user with such data already exists", - `{"username":"dogsLover", "password":"big_string", "email":"dogslove@gmail.com"}`, - JsonErrResponse{ - Status: "error", - Message: "there is already an account with this username or password", - Code: "uniq_fields", - }, - }, - { - "invalid data - broken body", - `{"username":"dogsLover", "password":"big_string", "email":"dogslove@gmail.com"`, - JsonErrResponse{ - Status: "error", - Message: "the correct username, email and password are expected to be received in JSON format", - Code: "parse_body", - }, - }, - { - "invalid data - no username", - `{"password":"big_string", "email":"dogslove@gmail.com"}`, - JsonErrResponse{ - Status: "error", - Message: "username", - Code: "invalid_params", - }, - }, - { - "invalid data - no username, password", - `{"email":"dogslove@gmail.com"}`, - JsonErrResponse{ - Status: "error", - Message: "password,username", - Code: "invalid_params", - }, - }, - { - "invalid data - short username", - `{"username":"sh", "password":"big_string", "email":"dogslove@gmail.com"}`, - JsonErrResponse{ - Status: "error", - Message: "username", - Code: "invalid_params", - }, - }, - { - "invalid data - incorrect email", - `{"username":"sh", "password":"big_string", "email":"dog"}`, - JsonErrResponse{ - Status: "error", - Message: "email,username", - Code: "invalid_params", - }, - }, - } - - for _, tCase := range badCases { - t.Run(tCase.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, url, io.NopCloser(strings.NewReader(tCase.rawBody))) - w := httptest.NewRecorder() - - service.Signup(w, req) - - var actualResp JsonErrResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - require.Equal(t, tCase.expResp, actualResp) - }) - } -} - -func TestLogout(t *testing.T) { - url := "https://domain.test:8080/api/v1/logout" - log, _ := logger.New(logger.RFC3339FormatTime()) - defer log.Sync() - - db, _ := ramrepo.OpenDB(strconv.FormatInt(int64(rand.Int()), 10)) - defer db.Close() - - sm := session.New(log, ramrepo.NewRamSessionRepo(db)) - userCase := user.New(log, ramrepo.NewRamUserRepo(db)) - service := New(log, sm, userCase, nil) - - goodCases := []struct { - name string - cookie *http.Cookie - expResp JsonResponse - }{ - { - "user is logged in - providing valid cookie", - &http.Cookie{ - Name: "session_key", - Value: "461afabf38b3147c", - }, - JsonResponse{ - Status: "ok", - Message: "the user has successfully logged out", - Body: nil, - }, - }, - } - - for _, tCase := range goodCases { - t.Run(tCase.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, url, nil) - req.AddCookie(tCase.cookie) - w := httptest.NewRecorder() - - service.Logout(w, req) - - var actualResp JsonResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - require.Equal(t, tCase.expResp, actualResp) - }) - } - - badCases := []struct { - name string - cookie *http.Cookie - expResp JsonErrResponse - }{ - { - "user isn't logged in - providing invalid cookie", - &http.Cookie{ - Name: "not_auth_cookie", - Value: "blablalba", - }, - JsonErrResponse{ - Status: "error", - Message: "to log out, you must first log in", - Code: "no_auth", - }, - }, - } - - for _, tCase := range badCases { - t.Run(tCase.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, url, nil) - req.AddCookie(tCase.cookie) - w := httptest.NewRecorder() - - service.Logout(w, req) - - var actualResp JsonErrResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - require.Equal(t, tCase.expResp, actualResp) - }) - } - -} diff --git a/internal/pkg/service/pin.go b/internal/pkg/service/pin.go deleted file mode 100644 index 41332f2..0000000 --- a/internal/pkg/service/pin.go +++ /dev/null @@ -1,49 +0,0 @@ -package service - -import ( - "errors" - "net/http" - - _ "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" - - log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" -) - -var ErrCountParameterMissing = errors.New("the count parameter is missing") -var ErrBadParams = errors.New("bad params") - -// GetPins godoc -// -// @Description Get pin collection -// @Tags Pin -// @Accept json -// @Produce json -// @Param lastID path string false "ID of the pin that will be just before the first pin in the requested collection, 0 by default" example(2) -// -// @Param count path string true "Pins quantity after last pin specified in lastID" example(5) -// @Success 200 {object} JsonResponse{body=[]Pin} -// @Failure 400 {object} JsonErrResponse -// @Failure 404 {object} JsonErrResponse -// @Failure 500 {object} JsonErrResponse -// @Router /api/v1/pin [get] -func (s *Service) GetPins(w http.ResponseWriter, r *http.Request) { - s.log.Info("request on get pins", log.F{"method", r.Method}, log.F{"path", r.URL.Path}) - SetContentTypeJSON(w) - - count, lastID, err := FetchValidParamForLoadTape(r.URL) - if err != nil { - s.log.Info("parse url query params", log.F{"error", err.Error()}) - err = responseError(w, "bad_params", - "expected parameters: count(positive integer: [1; 1000]), lastID(positive integer, the absence of this parameter is equal to the value 0)") - } else { - s.log.Sugar().Infof("param: count=%d, lastID=%d", count, lastID) - pins, last := s.pinCase.SelectNewPins(r.Context(), count, lastID) - err = responseOk(w, "pins received are sorted by id", map[string]any{ - "pins": pins, - "lastID": last, - }) - } - if err != nil { - s.log.Error(err.Error()) - } -} diff --git a/internal/pkg/service/pin_test.go b/internal/pkg/service/pin_test.go deleted file mode 100644 index 2dd7998..0000000 --- a/internal/pkg/service/pin_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "io" - "math/rand" - "net/http/httptest" - "strconv" - "testing" - - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/ramrepo" - pinCase "github.com/go-park-mail-ru/2023_2_OND_team/internal/usecases/pin" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" - "github.com/stretchr/testify/require" -) - -func TestGetPins(t *testing.T) { - - log, _ := logger.New(logger.RFC3339FormatTime()) - defer log.Sync() - - db, _ := ramrepo.OpenDB(strconv.FormatInt(int64(rand.Int()), 10)) - defer db.Close() - - pinCase := pinCase.New(log, ramrepo.NewRamPinRepo(db)) - service := New(log, nil, nil, pinCase) - - rawUrl := "https://domain.test:8080/api/v1/pin" - goodCases := []struct { - rawURL string - expResp JsonResponse - }{ - { - rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, 1, 2), - expResp: JsonResponse{ - Status: "ok", - Message: "pins received are sorted by id", - Body: map[string]interface{}{ - "lastID": 3, - "pins": []interface{}{ - map[string]interface{}{"id": 3}, - }, - }, - }, - }, - { - rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, 2, 3), - expResp: JsonResponse{ - Status: "ok", - Message: "pins received are sorted by id", - Body: map[string]interface{}{ - "lastID": 5, - "pins": []interface{}{ - map[string]interface{}{"id": 4}, - map[string]interface{}{"id": 5}, - }, - }, - }, - }, - } - - for _, tCase := range goodCases { - t.Run(fmt.Sprintf("TestGetPins good: %s", tCase.rawURL), func(t *testing.T) { - req := httptest.NewRequest("GET", tCase.rawURL, nil) - w := httptest.NewRecorder() - service.GetPins(w, req) - - var actualResp JsonResponse - json.NewDecoder(w.Result().Body).Decode(&actualResp) - - require.Equal(t, tCase.expResp.Status, actualResp.Status) - require.Equal(t, tCase.expResp.Message, actualResp.Message) - expLastID := tCase.expResp.Body.(map[string]interface{})["lastID"].(int) - actualLastID := actualResp.Body.(map[string]interface{})["lastID"].(float64) - - expIDs, actualIDs := make([]int, 0), make([]int, 0) - for _, pin := range tCase.expResp.Body.(map[string]interface{})["pins"].([]interface{}) { - expIDs = append(expIDs, pin.(map[string]interface{})["id"].(int)) - } - for _, pin := range actualResp.Body.(map[string]interface{})["pins"].([]interface{}) { - actualIDs = append(actualIDs, int(pin.(map[string]interface{})["id"].(float64))) - } - - require.Equal(t, expLastID, int(actualLastID)) - require.Equal(t, expIDs, actualIDs) - }) - } - - badCases := []struct { - rawURL string - expResp JsonErrResponse - }{ - { - rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, 0, 3), - expResp: JsonErrResponse{ - Status: "error", - Message: "expected parameters: count(positive integer: [1; 1000]), lastID(positive integer, the absence of this parameter is equal to the value 0)", - Code: "bad_params", - }, - }, - { - rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, -2, 3), - expResp: JsonErrResponse{ - Status: "error", - Message: "expected parameters: count(positive integer: [1; 1000]), lastID(positive integer, the absence of this parameter is equal to the value 0)", - Code: "bad_params", - }, - }, - { - rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, 213123, 3), - expResp: JsonErrResponse{ - Status: "error", - Message: "expected parameters: count(positive integer: [1; 1000]), lastID(positive integer, the absence of this parameter is equal to the value 0)", - Code: "bad_params", - }, - }, - { - rawURL: fmt.Sprintf("%s?count=%d&lastID=%d", rawUrl, 0, -1), - expResp: JsonErrResponse{ - Status: "error", - Message: "expected parameters: count(positive integer: [1; 1000]), lastID(positive integer, the absence of this parameter is equal to the value 0)", - Code: "bad_params", - }, - }, - { - rawURL: fmt.Sprintf("%s?count=&lastID=%d", rawUrl, 3), - expResp: JsonErrResponse{ - Status: "error", - Message: "expected parameters: count(positive integer: [1; 1000]), lastID(positive integer, the absence of this parameter is equal to the value 0)", - Code: "bad_params", - }, - }, - { - rawURL: fmt.Sprintf("%s?lastID=%d", rawUrl, 3), - expResp: JsonErrResponse{ - Status: "error", - Message: "expected parameters: count(positive integer: [1; 1000]), lastID(positive integer, the absence of this parameter is equal to the value 0)", - Code: "bad_params", - }, - }, - } - - for _, tCase := range badCases { - t.Run(fmt.Sprintf("TestGetPins bad: %s", tCase.rawURL), func(t *testing.T) { - req := httptest.NewRequest("GET", tCase.rawURL, nil) - w := httptest.NewRecorder() - service.GetPins(w, req) - - resp := w.Result() - body, _ := io.ReadAll(resp.Body) - var actualResp JsonErrResponse - - json.Unmarshal(body, &actualResp) - require.Equal(t, tCase.expResp, actualResp) - }) - } -} diff --git a/internal/pkg/service/response.go b/internal/pkg/service/response.go deleted file mode 100644 index df3121d..0000000 --- a/internal/pkg/service/response.go +++ /dev/null @@ -1,57 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "net/http" -) - -// type JsonResponseNoBody struct { -// Status string `json:"status" example:"ok"` -// Message string `json:"message" example:"Response message"` -// } - -type JsonResponse struct { - Status string `json:"status" example:"ok"` - Message string `json:"message" example:"Response message"` - Body interface{} `json:"body" extensions:"x-omitempty"` -} // @name JsonResponse - -type JsonErrResponse struct { - Status string `json:"status" example:"error"` - Message string `json:"message" example:"Error description"` - Code string `json:"code"` -} // @name JsonErrResponse - -func SetContentTypeJSON(w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json") -} - -func responseOk(w http.ResponseWriter, message string, body any) error { - res := JsonResponse{ - Status: "ok", - Message: message, - Body: body, - } - resBytes, err := json.Marshal(res) - if err != nil { - return fmt.Errorf("responseOk: %w", err) - } - w.WriteHeader(http.StatusOK) - _, err = w.Write(resBytes) - return err -} - -func responseError(w http.ResponseWriter, code, message string) error { - res := JsonErrResponse{ - Status: "error", - Message: message, - Code: code, - } - resBytes, err := json.Marshal(res) - if err != nil { - return fmt.Errorf("responseError: %w", err) - } - _, err = w.Write(resBytes) - return err -} diff --git a/internal/pkg/service/service.go b/internal/pkg/service/service.go deleted file mode 100644 index 05bb916..0000000 --- a/internal/pkg/service/service.go +++ /dev/null @@ -1,24 +0,0 @@ -package service - -import ( - "github.com/go-park-mail-ru/2023_2_OND_team/internal/usecases/pin" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/usecases/session" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/usecases/user" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" -) - -type Service struct { - log *logger.Logger - userCase *user.Usecase - pinCase *pin.Usecase - sm *session.SessionManager -} - -func New(log *logger.Logger, sm *session.SessionManager, user *user.Usecase, pin *pin.Usecase) *Service { - return &Service{ - log: log, - userCase: user, - pinCase: pin, - sm: sm, - } -} diff --git a/internal/pkg/service/validation.go b/internal/pkg/service/validation.go deleted file mode 100644 index 9356faa..0000000 --- a/internal/pkg/service/validation.go +++ /dev/null @@ -1,96 +0,0 @@ -package service - -import ( - "fmt" - "net/url" - "strconv" - "strings" - "unicode" - - valid "github.com/asaskevich/govalidator" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" -) - -type errorFields []string - -func (b *errorFields) Error() string { - return strings.Join(*b, ",") -} - -func (b *errorFields) addInvalidField(fieldName string) { - *b = append(*b, fieldName) -} - -func (b *errorFields) Err() error { - if len(*b) == 0 { - return nil - } - return b -} - -func FetchValidParamForLoadTape(u *url.URL) (count int, lastID int, err error) { - if param := u.Query().Get("count"); len(param) > 0 { - c, err := strconv.ParseInt(param, 10, 64) - if err != nil { - return 0, 0, fmt.Errorf("fetch count param for load tape: %w", err) - } - count = int(c) - } else { - return 0, 0, ErrCountParameterMissing - } - if param := u.Query().Get("lastID"); len(param) > 0 { - last, err := strconv.ParseInt(param, 10, 64) - if err != nil { - return 0, 0, fmt.Errorf("fetch lastID param for load tape: %w", err) - } - lastID = int(last) - } - if count <= 0 || count > 1000 || lastID < 0 { - return 0, 0, ErrBadParams - } - return -} - -func IsValidUserForRegistration(user *user.User) error { - invalidFields := new(errorFields) - - if !isValidPassword(user.Password) { - invalidFields.addInvalidField("password") - } - if !isValidEmail(user.Email) { - invalidFields.addInvalidField("email") - } - if !isValidUsername(user.Username) { - invalidFields.addInvalidField("username") - } - - return invalidFields.Err() -} - -func isValidUsername(username string) bool { - if len(username) < 4 || len(username) > 50 { - return false - } - for _, r := range username { - if !(unicode.IsNumber(r) || unicode.IsSymbol(r) || unicode.IsPunct(r) || unicode.IsLetter(r)) { - return false - } - } - return true -} - -func isValidEmail(email string) bool { - return valid.IsEmail(email) && len(email) <= 50 -} - -func isValidPassword(password string) bool { - if len(password) < 8 || len(password) > 50 { - return false - } - for _, r := range password { - if !(unicode.IsNumber(r) || unicode.IsSymbol(r) || unicode.IsPunct(r) || unicode.IsLetter(r)) { - return false - } - } - return true -} diff --git a/internal/pkg/service/validation_test.go b/internal/pkg/service/validation_test.go deleted file mode 100644 index f2b094c..0000000 --- a/internal/pkg/service/validation_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package service - -import ( - "net/url" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestFetchValidParams(t *testing.T) { - rawUrl := "https://domain.test:8080/api/v1/pin" - - tests := []struct { - name string - queryRow string - wantCount, wantLastID int - }{ - {"both parameters were passed correctly", "?count=6&lastID=12", 6, 12}, - {"both parameters were passed correctly in a different order", "?lastID=1&count=3", 3, 1}, - {"repeating parameters", "?count=14&lastID=1&count=3&lastID=55&lastID=65", 14, 1}, - {"repeating parameters", "?count=14&lastID=1&count=3&lastID=55&lastID=65", 14, 1}, - {"empty parameter lastID", "?count=7", 7, 0}, - {"the parameter lastID is registered but not specified", "?lastID=&count=17", 17, 0}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - URL, err := url.Parse(rawUrl + test.queryRow) - if err != nil { - t.Fatalf("error when parsing into the url.URL structure: %v", err) - } - actualCount, actualLastID, err := FetchValidParamForLoadTape(URL) - require.NoError(t, err) - require.Equal(t, test.wantCount, actualCount) - require.Equal(t, test.wantLastID, actualLastID) - }) - } -} - -func TestErrorFetchValidParams(t *testing.T) { - rawUrl := "https://domain.test:8080/api/v1/pin" - - tests := []struct { - name string - queryRow string - wantErr error - }{ - {"empty query row", "", ErrCountParameterMissing}, - {"count equal zero", "?count=0", ErrBadParams}, - {"negative count", "?count=-5&lastID=12", ErrBadParams}, - {"negative lastID", "?count=5&lastID=-6", ErrBadParams}, - {"requested count is more than a thousand", "?count=1001", ErrBadParams}, - {"count param empty", "?count=&lastID=6", ErrCountParameterMissing}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - URL, err := url.Parse(rawUrl + test.queryRow) - if err != nil { - t.Fatalf("error when parsing into the url.URL structure: %v", err) - } - actualCount, actualLastID, err := FetchValidParamForLoadTape(URL) - require.ErrorIs(t, err, test.wantErr) - require.Equal(t, 0, actualCount) - require.Equal(t, 0, actualLastID) - }) - } -} diff --git a/internal/pkg/usecase/auth/mock/auth_mock.go b/internal/pkg/usecase/auth/mock/auth_mock.go new file mode 100644 index 0000000..813ae3b --- /dev/null +++ b/internal/pkg/usecase/auth/mock/auth_mock.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + session "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/session" + user "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + gomock "github.com/golang/mock/gomock" +) + +// MockUsecase is a mock of Usecase interface. +type MockUsecase struct { + ctrl *gomock.Controller + recorder *MockUsecaseMockRecorder +} + +// MockUsecaseMockRecorder is the mock recorder for MockUsecase. +type MockUsecaseMockRecorder struct { + mock *MockUsecase +} + +// NewMockUsecase creates a new mock instance. +func NewMockUsecase(ctrl *gomock.Controller) *MockUsecase { + mock := &MockUsecase{ctrl: ctrl} + mock.recorder = &MockUsecaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { + return m.recorder +} + +// GetUserIDBySession mocks base method. +func (m *MockUsecase) GetUserIDBySession(ctx context.Context, sess *session.Session) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserIDBySession", ctx, sess) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserIDBySession indicates an expected call of GetUserIDBySession. +func (mr *MockUsecaseMockRecorder) GetUserIDBySession(ctx, sess interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserIDBySession", reflect.TypeOf((*MockUsecase)(nil).GetUserIDBySession), ctx, sess) +} + +// Login mocks base method. +func (m *MockUsecase) Login(ctx context.Context, username, password string) (*session.Session, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Login", ctx, username, password) + ret0, _ := ret[0].(*session.Session) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Login indicates an expected call of Login. +func (mr *MockUsecaseMockRecorder) Login(ctx, username, password interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockUsecase)(nil).Login), ctx, username, password) +} + +// Logout mocks base method. +func (m *MockUsecase) Logout(ctx context.Context, sess *session.Session) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Logout", ctx, sess) + ret0, _ := ret[0].(error) + return ret0 +} + +// Logout indicates an expected call of Logout. +func (mr *MockUsecaseMockRecorder) Logout(ctx, sess interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logout", reflect.TypeOf((*MockUsecase)(nil).Logout), ctx, sess) +} + +// Register mocks base method. +func (m *MockUsecase) Register(ctx context.Context, user *user.User) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Register", ctx, user) + ret0, _ := ret[0].(error) + return ret0 +} + +// Register indicates an expected call of Register. +func (mr *MockUsecaseMockRecorder) Register(ctx, user interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockUsecase)(nil).Register), ctx, user) +} diff --git a/internal/pkg/usecase/auth/usecase.go b/internal/pkg/usecase/auth/usecase.go new file mode 100644 index 0000000..49ee9bf --- /dev/null +++ b/internal/pkg/usecase/auth/usecase.go @@ -0,0 +1,80 @@ +package auth + +import ( + "context" + "fmt" + + authProto "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/auth" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/session" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "google.golang.org/protobuf/types/known/timestamppb" +) + +//go:generate mockgen -destination=./mock/auth_mock.go -package=mock -source=usecase.go Usecase +type Usecase interface { + Register(ctx context.Context, user *entity.User) error + Login(ctx context.Context, username, password string) (*session.Session, error) + GetUserIDBySession(ctx context.Context, sess *session.Session) (int, error) + Logout(ctx context.Context, sess *session.Session) error +} + +type authCase struct { + client authProto.AuthClient +} + +func New(client authProto.AuthClient) *authCase { + return &authCase{client} +} + +func (ac *authCase) Register(ctx context.Context, user *entity.User) error { + _, err := ac.client.Register(ctx, &authProto.RegisterData{ + Cred: &authProto.Credentials{ + Username: user.Username, + Password: user.Password, + }, + Email: user.Email, + }) + if err != nil { + return fmt.Errorf("register: %w", err) + } + return nil +} + +func (ac *authCase) Logout(ctx context.Context, sess *session.Session) error { + _, err := ac.client.Logout(ctx, &authProto.Session{ + Key: sess.Key, + UserID: int64(sess.UserID), + Expire: timestamppb.New(sess.Expire), + }) + if err != nil { + return fmt.Errorf("logout: %w", err) + } + return nil +} + +func (ac *authCase) Login(ctx context.Context, username, password string) (*session.Session, error) { + sess, err := ac.client.Login(ctx, &authProto.Credentials{ + Username: username, + Password: password, + }) + if err != nil { + return nil, fmt.Errorf("login: %w", err) + } + return &session.Session{ + Key: sess.Key, + UserID: int(sess.UserID), + Expire: sess.Expire.AsTime(), + }, nil +} + +func (ac *authCase) GetUserIDBySession(ctx context.Context, sess *session.Session) (int, error) { + userID, err := ac.client.GetUserID(ctx, &authProto.Session{ + Key: sess.Key, + UserID: int64(sess.UserID), + Expire: timestamppb.New(sess.Expire), + }) + if err != nil { + return 0, fmt.Errorf("get user id by session: %w", err) + } + return int(userID.Id), nil +} diff --git a/internal/pkg/usecase/board/delete.go b/internal/pkg/usecase/board/delete.go index da4fd8b..cab1a87 100644 --- a/internal/pkg/usecase/board/delete.go +++ b/internal/pkg/usecase/board/delete.go @@ -31,3 +31,31 @@ func (bCase *boardUsecase) DeleteCertainBoard(ctx context.Context, boardID int) return nil } + +func (bCase *boardUsecase) DeletePinFromBoard(ctx context.Context, boardID, pinID int) error { + boardAuthorID, err := bCase.boardRepo.GetBoardAuthorByBoardID(ctx, boardID) + if err != nil { + switch err { + case repository.ErrNoData: + return ErrNoSuchBoard + default: + return fmt.Errorf("delete certain board: %w", err) + } + } + + currUserID, loggedIn := ctx.Value(auth.KeyCurrentUserID).(int) + if !(loggedIn && currUserID == boardAuthorID) { + return ErrNoAccess + } + + err = bCase.boardRepo.DeletePinFromBoard(ctx, boardID, pinID) + if err != nil { + switch err { + case repository.ErrNoDataAffected: + return ErrNoPinOnBoard + } + return fmt.Errorf("delete certain board: %w", err) + } + + return nil +} diff --git a/internal/pkg/usecase/board/errors.go b/internal/pkg/usecase/board/errors.go index b241f3c..da4ef61 100644 --- a/internal/pkg/usecase/board/errors.go +++ b/internal/pkg/usecase/board/errors.go @@ -5,6 +5,7 @@ import "errors" var ( ErrInvalidUsername = errors.New("username doesn't exist") ErrNoSuchBoard = errors.New("board is not accessable or doesn't exist") + ErrNoPinOnBoard = errors.New("no such pin on board") ErrInvalidUserID = errors.New("invalid user id has been provided") ErrNoAccess = errors.New("no access for this action") ) diff --git a/internal/pkg/usecase/board/get.go b/internal/pkg/usecase/board/get.go index 9e02946..a56467f 100644 --- a/internal/pkg/usecase/board/get.go +++ b/internal/pkg/usecase/board/get.go @@ -36,26 +36,23 @@ func (bCase *boardUsecase) GetBoardsByUsername(ctx context.Context, username str return nil, fmt.Errorf("get boards by user id usecase: %w", err) } - for _, board := range boards { - board.Sanitize(bCase.sanitizer) - } return boards, nil } -func (bCase *boardUsecase) GetCertainBoard(ctx context.Context, boardID int) (entity.BoardWithContent, error) { +func (bCase *boardUsecase) GetCertainBoard(ctx context.Context, boardID int) (entity.BoardWithContent, string, error) { boardAuthorID, err := bCase.boardRepo.GetBoardAuthorByBoardID(ctx, boardID) if err != nil { switch err { case repository.ErrNoData: - return entity.BoardWithContent{}, ErrNoSuchBoard + return entity.BoardWithContent{}, "", ErrNoSuchBoard default: - return entity.BoardWithContent{}, fmt.Errorf("get certain board: %w", err) + return entity.BoardWithContent{}, "", fmt.Errorf("get certain board: %w", err) } } boardContributors, err := bCase.boardRepo.GetContributorsByBoardID(ctx, boardID) if err != nil { - return entity.BoardWithContent{}, fmt.Errorf("get certain board: %w", err) + return entity.BoardWithContent{}, "", fmt.Errorf("get certain board: %w", err) } boardContributorsIDs := make([]int, 0, len(boardContributors)) @@ -70,18 +67,16 @@ func (bCase *boardUsecase) GetCertainBoard(ctx context.Context, boardID int) (en hasAccess = true } - board, err := bCase.boardRepo.GetBoardByID(ctx, boardID, hasAccess) + board, username, err := bCase.boardRepo.GetBoardByID(ctx, boardID, hasAccess) if err != nil { switch err { case repository.ErrNoData: - return entity.BoardWithContent{}, ErrNoSuchBoard + return entity.BoardWithContent{}, "", ErrNoSuchBoard default: - return entity.BoardWithContent{}, fmt.Errorf("get certain board: %w", err) + return entity.BoardWithContent{}, "", fmt.Errorf("get certain board: %w", err) } } - - board.Sanitize(bCase.sanitizer) - return board, nil + return board, username, nil } func isContributor(contributorsIDs []int, userID int) bool { diff --git a/internal/pkg/usecase/board/mock/board_mock.go b/internal/pkg/usecase/board/mock/board_mock.go index 39845c3..1b14189 100644 --- a/internal/pkg/usecase/board/mock/board_mock.go +++ b/internal/pkg/usecase/board/mock/board_mock.go @@ -79,6 +79,20 @@ func (mr *MockUsecaseMockRecorder) DeleteCertainBoard(ctx, boardID interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCertainBoard", reflect.TypeOf((*MockUsecase)(nil).DeleteCertainBoard), ctx, boardID) } +// DeletePinFromBoard mocks base method. +func (m *MockUsecase) DeletePinFromBoard(ctx context.Context, boardID, pinID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePinFromBoard", ctx, boardID, pinID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePinFromBoard indicates an expected call of DeletePinFromBoard. +func (mr *MockUsecaseMockRecorder) DeletePinFromBoard(ctx, boardID, pinID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePinFromBoard", reflect.TypeOf((*MockUsecase)(nil).DeletePinFromBoard), ctx, boardID, pinID) +} + // FixPinsOnBoard mocks base method. func (m *MockUsecase) FixPinsOnBoard(ctx context.Context, boardID int, pinIds []int, userID int) error { m.ctrl.T.Helper() @@ -125,12 +139,13 @@ func (mr *MockUsecaseMockRecorder) GetBoardsByUsername(ctx, username interface{} } // GetCertainBoard mocks base method. -func (m *MockUsecase) GetCertainBoard(ctx context.Context, boardID int) (board.BoardWithContent, error) { +func (m *MockUsecase) GetCertainBoard(ctx context.Context, boardID int) (board.BoardWithContent, string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCertainBoard", ctx, boardID) ret0, _ := ret[0].(board.BoardWithContent) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetCertainBoard indicates an expected call of GetCertainBoard. diff --git a/internal/pkg/usecase/board/update.go b/internal/pkg/usecase/board/update.go index c30180c..686f44e 100644 --- a/internal/pkg/usecase/board/update.go +++ b/internal/pkg/usecase/board/update.go @@ -49,10 +49,6 @@ func (bCase *boardUsecase) GetBoardInfoForUpdate(ctx context.Context, boardID in } } - board.Sanitize(bCase.sanitizer) - for id, title := range tagTitles { - tagTitles[id] = bCase.sanitizer.Sanitize(title) - } return board, tagTitles, nil } diff --git a/internal/pkg/usecase/board/usecase.go b/internal/pkg/usecase/board/usecase.go index 641945c..730e042 100644 --- a/internal/pkg/usecase/board/usecase.go +++ b/internal/pkg/usecase/board/usecase.go @@ -8,18 +8,18 @@ import ( boardRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/board" userRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/user" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" - "github.com/microcosm-cc/bluemonday" ) //go:generate mockgen -destination=./mock/board_mock.go -package=mock -source=usecase.go Usecase type Usecase interface { CreateNewBoard(ctx context.Context, newBoard entity.Board, tagTitles []string) (int, error) GetBoardsByUsername(ctx context.Context, username string) ([]entity.BoardWithContent, error) - GetCertainBoard(ctx context.Context, boardID int) (entity.BoardWithContent, error) + GetCertainBoard(ctx context.Context, boardID int) (entity.BoardWithContent, string, error) GetBoardInfoForUpdate(ctx context.Context, boardID int) (entity.Board, []string, error) UpdateBoardInfo(ctx context.Context, updatedBoard entity.Board, tagTitles []string) error DeleteCertainBoard(ctx context.Context, boardID int) error FixPinsOnBoard(ctx context.Context, boardID int, pinIds []int, userID int) error + DeletePinFromBoard(ctx context.Context, boardID, pinID int) error CheckAvailabilityFeedPinCfgOnBoard(ctx context.Context, cfg pin.FeedPinConfig, userID int, isAuth bool) error } @@ -27,9 +27,8 @@ type boardUsecase struct { log *logger.Logger boardRepo boardRepo.Repository userRepo userRepo.Repository - sanitizer *bluemonday.Policy } -func New(logger *logger.Logger, boardRepo boardRepo.Repository, userRepo userRepo.Repository, sanitizer *bluemonday.Policy) *boardUsecase { - return &boardUsecase{log: logger, boardRepo: boardRepo, userRepo: userRepo, sanitizer: sanitizer} +func New(logger *logger.Logger, boardRepo boardRepo.Repository, userRepo userRepo.Repository) *boardUsecase { + return &boardUsecase{log: logger, boardRepo: boardRepo, userRepo: userRepo} } diff --git a/internal/pkg/usecase/board/usecase_test.go b/internal/pkg/usecase/board/usecase_test.go index f3c20dd..251d70c 100644 --- a/internal/pkg/usecase/board/usecase_test.go +++ b/internal/pkg/usecase/board/usecase_test.go @@ -339,6 +339,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { GetBoardByID GetBoardByID hasAccess bool expBoard entity.BoardWithContent + expUsername string wantErr bool expErr error }{ @@ -363,7 +364,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { PinsNumber: 1, Pins: []string{"/pic1"}, TagTitles: []string{"good", "bad"}, - }, nil).Times(1) + }, "user", nil).Times(1) }, hasAccess: true, expBoard: entity.BoardWithContent{ @@ -377,6 +378,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { Pins: []string{"/pic1"}, TagTitles: []string{"good", "bad"}, }, + expUsername: "user", }, { name: "private board, valid board id, request from contributor", @@ -399,7 +401,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { PinsNumber: 1, Pins: []string{"/pic1"}, TagTitles: []string{"good", "bad"}, - }, nil).Times(1) + }, "user", nil).Times(1) }, hasAccess: true, expBoard: entity.BoardWithContent{ @@ -413,6 +415,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { Pins: []string{"/pic1"}, TagTitles: []string{"good", "bad"}, }, + expUsername: "user", }, { name: "private board, valid board id, request from not author, not contributor", @@ -425,7 +428,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { mockRepo.EXPECT().GetContributorsByBoardID(ctx, boardID).Return([]uEntity.User{{ID: 123}}, nil).Times(1) }, GetBoardByID: func(mockRepo *mock_board.MockRepository, ctx context.Context, boardID int, hasAccess bool) { - mockRepo.EXPECT().GetBoardByID(ctx, boardID, hasAccess).Return(entity.BoardWithContent{}, repository.ErrNoData).Times(1) + mockRepo.EXPECT().GetBoardByID(ctx, boardID, hasAccess).Return(entity.BoardWithContent{}, "", repository.ErrNoData).Times(1) }, hasAccess: false, expBoard: entity.BoardWithContent{}, @@ -443,7 +446,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { mockRepo.EXPECT().GetContributorsByBoardID(ctx, boardID).Return([]uEntity.User{{ID: 123}}, nil).Times(1) }, GetBoardByID: func(mockRepo *mock_board.MockRepository, ctx context.Context, boardID int, hasAccess bool) { - mockRepo.EXPECT().GetBoardByID(ctx, boardID, hasAccess).Return(entity.BoardWithContent{}, repository.ErrNoData).Times(1) + mockRepo.EXPECT().GetBoardByID(ctx, boardID, hasAccess).Return(entity.BoardWithContent{}, "", repository.ErrNoData).Times(1) }, hasAccess: false, expBoard: entity.BoardWithContent{}, @@ -471,7 +474,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { PinsNumber: 1, Pins: []string{"/pic1"}, TagTitles: []string{"good", "bad"}, - }, nil).Times(1) + }, "user", nil).Times(1) }, hasAccess: false, expBoard: entity.BoardWithContent{ @@ -485,6 +488,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { Pins: []string{"/pic1"}, TagTitles: []string{"good", "bad"}, }, + expUsername: "user", }, { name: "invalid board id", @@ -514,7 +518,7 @@ func TestBoardUsecase_GetCertainBoard(t *testing.T) { test.GetBoardByID(mockBoardRepo, test.inCtx, test.boardID, test.hasAccess) boardUsecase := New(log, mockBoardRepo, nil, sanitizer) - board, err := boardUsecase.GetCertainBoard(test.inCtx, test.boardID) + board, _, err := boardUsecase.GetCertainBoard(test.inCtx, test.boardID) if test.wantErr { require.EqualError(t, err, test.expErr.Error()) diff --git a/internal/pkg/usecase/comment/check.go b/internal/pkg/usecase/comment/check.go new file mode 100644 index 0000000..a278b68 --- /dev/null +++ b/internal/pkg/usecase/comment/check.go @@ -0,0 +1,29 @@ +package comment + +import ( + "context" + "errors" + "fmt" +) + +var ErrNotAvailableAction = errors.New("action not available for user") + +func (c *commentCase) isAvailableCommentForDelete(ctx context.Context, userID, commentID int) error { + comment, err := c.repo.GetCommentByID(ctx, commentID) + if err != nil { + return fmt.Errorf("get comment for check available comment for delete: %w", err) + } + + if comment.Author.ID == userID { + return nil + } + + authorPinID, err := c.GetAuthorIdOfThePin(ctx, comment.PinID) + if err != nil { + return fmt.Errorf("get author pin for check availabel comment: %w", err) + } + if authorPinID != userID { + return ErrNotAvailableAction + } + return nil +} diff --git a/internal/pkg/usecase/comment/mock/comment_mock.go b/internal/pkg/usecase/comment/mock/comment_mock.go new file mode 100644 index 0000000..92bec48 --- /dev/null +++ b/internal/pkg/usecase/comment/mock/comment_mock.go @@ -0,0 +1,133 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + comment "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + gomock "github.com/golang/mock/gomock" +) + +// MockUsecase is a mock of Usecase interface. +type MockUsecase struct { + ctrl *gomock.Controller + recorder *MockUsecaseMockRecorder +} + +// MockUsecaseMockRecorder is the mock recorder for MockUsecase. +type MockUsecaseMockRecorder struct { + mock *MockUsecase +} + +// NewMockUsecase creates a new mock instance. +func NewMockUsecase(ctrl *gomock.Controller) *MockUsecase { + mock := &MockUsecase{ctrl: ctrl} + mock.recorder = &MockUsecaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { + return m.recorder +} + +// DeleteComment mocks base method. +func (m *MockUsecase) DeleteComment(ctx context.Context, userID, commentID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteComment", ctx, userID, commentID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteComment indicates an expected call of DeleteComment. +func (mr *MockUsecaseMockRecorder) DeleteComment(ctx, userID, commentID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteComment", reflect.TypeOf((*MockUsecase)(nil).DeleteComment), ctx, userID, commentID) +} + +// GetFeedCommentOnPin mocks base method. +func (m *MockUsecase) GetFeedCommentOnPin(ctx context.Context, userID, pinID, count, lastID int) ([]comment.Comment, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFeedCommentOnPin", ctx, userID, pinID, count, lastID) + ret0, _ := ret[0].([]comment.Comment) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetFeedCommentOnPin indicates an expected call of GetFeedCommentOnPin. +func (mr *MockUsecaseMockRecorder) GetFeedCommentOnPin(ctx, userID, pinID, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeedCommentOnPin", reflect.TypeOf((*MockUsecase)(nil).GetFeedCommentOnPin), ctx, userID, pinID, count, lastID) +} + +// PutCommentOnPin mocks base method. +func (m *MockUsecase) PutCommentOnPin(ctx context.Context, userID int, comment *comment.Comment) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutCommentOnPin", ctx, userID, comment) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutCommentOnPin indicates an expected call of PutCommentOnPin. +func (mr *MockUsecaseMockRecorder) PutCommentOnPin(ctx, userID, comment interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutCommentOnPin", reflect.TypeOf((*MockUsecase)(nil).PutCommentOnPin), ctx, userID, comment) +} + +// MockavailablePinChecker is a mock of availablePinChecker interface. +type MockavailablePinChecker struct { + ctrl *gomock.Controller + recorder *MockavailablePinCheckerMockRecorder +} + +// MockavailablePinCheckerMockRecorder is the mock recorder for MockavailablePinChecker. +type MockavailablePinCheckerMockRecorder struct { + mock *MockavailablePinChecker +} + +// NewMockavailablePinChecker creates a new mock instance. +func NewMockavailablePinChecker(ctrl *gomock.Controller) *MockavailablePinChecker { + mock := &MockavailablePinChecker{ctrl: ctrl} + mock.recorder = &MockavailablePinCheckerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockavailablePinChecker) EXPECT() *MockavailablePinCheckerMockRecorder { + return m.recorder +} + +// GetAuthorIdOfThePin mocks base method. +func (m *MockavailablePinChecker) GetAuthorIdOfThePin(ctx context.Context, pinID int) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthorIdOfThePin", ctx, pinID) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthorIdOfThePin indicates an expected call of GetAuthorIdOfThePin. +func (mr *MockavailablePinCheckerMockRecorder) GetAuthorIdOfThePin(ctx, pinID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorIdOfThePin", reflect.TypeOf((*MockavailablePinChecker)(nil).GetAuthorIdOfThePin), ctx, pinID) +} + +// IsAvailablePinForViewingUser mocks base method. +func (m *MockavailablePinChecker) IsAvailablePinForViewingUser(ctx context.Context, userID, pinID int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsAvailablePinForViewingUser", ctx, userID, pinID) + ret0, _ := ret[0].(error) + return ret0 +} + +// IsAvailablePinForViewingUser indicates an expected call of IsAvailablePinForViewingUser. +func (mr *MockavailablePinCheckerMockRecorder) IsAvailablePinForViewingUser(ctx, userID, pinID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAvailablePinForViewingUser", reflect.TypeOf((*MockavailablePinChecker)(nil).IsAvailablePinForViewingUser), ctx, userID, pinID) +} diff --git a/internal/pkg/usecase/comment/usecase.go b/internal/pkg/usecase/comment/usecase.go new file mode 100644 index 0000000..b7f4272 --- /dev/null +++ b/internal/pkg/usecase/comment/usecase.go @@ -0,0 +1,110 @@ +package comment + +import ( + "context" + "fmt" + "time" + + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + commentRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/comment" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime/notification" +) + +//go:generate mockgen -destination=./mock/comment_mock.go -package=mock -source=usecase.go Usecase +type Usecase interface { + PutCommentOnPin(ctx context.Context, userID int, comment *entity.Comment) (int, error) + GetFeedCommentOnPin(ctx context.Context, userID, pinID, count, lastID int) ([]entity.Comment, int, error) + DeleteComment(ctx context.Context, userID, commentID int) error + GetCommentWithAuthor(ctx context.Context, commentID int) (*entity.Comment, error) +} + +type availablePinChecker interface { + IsAvailablePinForViewingUser(ctx context.Context, userID, pinID int) error + GetAuthorIdOfThePin(ctx context.Context, pinID int) (int, error) +} + +const _timeoutNotification = 5 * time.Minute + +type commentCase struct { + availablePinChecker + + notifyCase notification.Usecase + repo commentRepo.Repository + + notifyIsEnable bool +} + +func New(repo commentRepo.Repository, checker availablePinChecker, notifyCase notification.Usecase) *commentCase { + comCase := &commentCase{ + availablePinChecker: checker, + repo: repo, + notifyCase: notifyCase, + } + + if notifyCase != nil { + comCase.notifyIsEnable = true + } + return comCase +} + +func (c *commentCase) PutCommentOnPin(ctx context.Context, userID int, comment *entity.Comment) (int, error) { + err := c.IsAvailablePinForViewingUser(ctx, userID, comment.PinID) + if err != nil { + return 0, fmt.Errorf("put comment on not available pin: %w", err) + } + + comment.Author = &user.User{ID: userID} + + id, err := c.repo.AddComment(ctx, comment) + if err != nil { + return 0, fmt.Errorf("put comment on available pin: %w", err) + } + + if c.notifyIsEnable { + ctx, _ = context.WithTimeout(context.Background(), _timeoutNotification) + go c.notifyCase.NotifyCommentLeftOnPin(ctx, id) + } + + return id, nil +} + +func (c *commentCase) GetFeedCommentOnPin(ctx context.Context, userID, pinID, count, lastID int) ([]entity.Comment, int, error) { + err := c.IsAvailablePinForViewingUser(ctx, userID, pinID) + if err != nil { + return nil, 0, fmt.Errorf("put comment on not available pin: %w", err) + } + + feed, err := c.repo.GetCommensToPin(ctx, pinID, lastID, count) + if err != nil { + err = fmt.Errorf("get feed comment on pin: %w", err) + } + + var newLastID int + if len(feed) > 0 { + newLastID = feed[len(feed)-1].ID + } + return feed, newLastID, err +} + +func (c *commentCase) DeleteComment(ctx context.Context, userID, commentID int) error { + err := c.isAvailableCommentForDelete(ctx, userID, commentID) + if err != nil { + return fmt.Errorf("check available delete comment: %w", err) + } + + err = c.repo.EditStatusCommentOnDeletedByID(ctx, commentID) + if err != nil { + return fmt.Errorf("delete comment: %w", err) + } + return nil +} + +func (c *commentCase) GetCommentWithAuthor(ctx context.Context, commentID int) (*entity.Comment, error) { + comment, err := c.repo.GetCommentByID(ctx, commentID) + if err != nil { + return nil, fmt.Errorf("get comment with author: %w", err) + } + + return comment, nil +} diff --git a/internal/pkg/usecase/image/filtration.go b/internal/pkg/usecase/image/filtration.go new file mode 100644 index 0000000..457a1af --- /dev/null +++ b/internal/pkg/usecase/image/filtration.go @@ -0,0 +1,101 @@ +package image + +import ( + "context" + "errors" + "strings" + + vision "cloud.google.com/go/vision/v2/apiv1" + pb "cloud.google.com/go/vision/v2/apiv1/visionpb" + validate "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/validation" +) + +var ( + maxAnnotationsNumber int32 = 15 + explicitLabels = []string{"goose", "duck"} + ErrExplicitImage = errors.New("image content doesn't comply with service policy") +) + +type ImageFilter interface { + Filter(ctx context.Context, imgBytes []byte, explicitLabels []string) error +} + +type googleVision struct { + visionClient *vision.ImageAnnotatorClient + censor validate.ProfanityCensor +} + +func NewFilter(client *vision.ImageAnnotatorClient, censor validate.ProfanityCensor) *googleVision { + return &googleVision{client, censor} +} + +func CheckAnnotations(annotation *pb.SafeSearchAnnotation) bool { + return annotation.GetAdult() >= pb.Likelihood_LIKELY || + annotation.GetMedical() >= pb.Likelihood_LIKELY || + annotation.GetRacy() >= pb.Likelihood_LIKELY || + annotation.GetViolence() >= pb.Likelihood_LIKELY || + annotation.GetSpoof() >= pb.Likelihood_LIKELY +} + +func GetImageLabels(annotations []*pb.EntityAnnotation) []string { + imgLabels := make([]string, 0, len(annotations)) + for _, label := range annotations { + imgLabels = append(imgLabels, label.GetDescription()) + } + return imgLabels +} + +func CheckCertainLabels(explicitLabels, imgLabels []string) bool { + for _, label := range explicitLabels { + if HasExplicitLabel(label, imgLabels) { + return true + } + } + return false +} + +func HasExplicitLabel(explicitLabel string, imgLabels []string) bool { + for _, label := range imgLabels { + if strings.Contains(strings.ToLower(label), strings.ToLower(explicitLabel)) { + return true + } + } + return false +} + +func getTextDescription(resp *pb.AnnotateImageResponse) string { + annotations := resp.GetTextAnnotations() + if len(annotations) == 0 { + return "" + } + return annotations[0].GetDescription() +} + +func CheckExplicit(resp *pb.AnnotateImageResponse, explicitLabels []string, censor validate.ProfanityCensor) error { + if CheckCertainLabels(explicitLabels, GetImageLabels(resp.GetLabelAnnotations())) || + CheckAnnotations(resp.GetSafeSearchAnnotation()) || + censor.IsProfane(getTextDescription(resp)) { + return ErrExplicitImage + } + return nil +} + +func (filter *googleVision) Filter(ctx context.Context, imgBytes []byte, explicitLabels []string) error { + req := &pb.BatchAnnotateImagesRequest{ + Requests: []*pb.AnnotateImageRequest{ + { + Image: &pb.Image{Content: imgBytes}, + Features: []*pb.Feature{ + {Type: pb.Feature_LABEL_DETECTION, MaxResults: maxAnnotationsNumber}, + {Type: pb.Feature_SAFE_SEARCH_DETECTION, MaxResults: maxAnnotationsNumber}, + {Type: pb.Feature_TEXT_DETECTION}, + }, + }, + }, + } + resp, err := filter.visionClient.BatchAnnotateImages(ctx, req) + if err != nil { + return err + } + return CheckExplicit(resp.GetResponses()[0], explicitLabels, filter.censor) +} diff --git a/internal/pkg/usecase/image/usecase.go b/internal/pkg/usecase/image/usecase.go index 799ed62..8798040 100644 --- a/internal/pkg/usecase/image/usecase.go +++ b/internal/pkg/usecase/image/usecase.go @@ -2,6 +2,7 @@ package image import ( "bytes" + "context" "errors" "fmt" "io" @@ -12,35 +13,45 @@ import ( "github.com/go-park-mail-ru/2023_2_OND_team/pkg/validator/image/check" ) -const PrefixURLImage = "https://pinspire.online:8081/" +const PrefixURLImage = "https://pinspire.site/" -var ErrInvalidImage = errors.New("invalid images") -var ErrUploadFile = errors.New("file upload failed") +var ( + ErrInvalidImage = errors.New("invalid images") + ErrUploadFile = errors.New("file upload failed") +) //go:generate mockgen -destination=./mock/image_mock.go -package=mock -source=usecase.go Usecase type Usecase interface { - UploadImage(path string, mimeType string, size int64, image io.Reader, check check.CheckSize) (string, error) + UploadImage(ctx context.Context, path string, mimeType string, size int64, image io.Reader, check check.CheckSize) (string, error) } type imageCase struct { - log *log.Logger - repo repo.Repository + log *log.Logger + repo repo.Repository + filter ImageFilter } -func New(log *log.Logger, repo repo.Repository) *imageCase { - return &imageCase{log, repo} +func New(log *log.Logger, repo repo.Repository, filter ImageFilter) *imageCase { + return &imageCase{log, repo, filter} } -func (img *imageCase) UploadImage(path string, mimeType string, size int64, image io.Reader, check check.CheckSize) (string, error) { +func (img *imageCase) UploadImage(ctx context.Context, path string, mimeType string, size int64, image io.Reader, check check.CheckSize) (string, error) { buf := bytes.NewBuffer(nil) extension, ok := valid.IsValidImage(io.TeeReader(image, buf), mimeType, check) if !ok { return "", ErrInvalidImage } - io.Copy(buf, image) + err := img.filter.Filter(ctx, buf.Bytes(), explicitLabels) + if err != nil { + if err == ErrExplicitImage { + return "", err + } + return "", fmt.Errorf("upload image: %w", err) + } + filename, written, err := img.repo.SaveImage(path, extension, buf) if err != nil { return "", fmt.Errorf("upload image: %w", err) diff --git a/internal/pkg/usecase/message/convert.go b/internal/pkg/usecase/message/convert.go new file mode 100644 index 0000000..847065e --- /dev/null +++ b/internal/pkg/usecase/message/convert.go @@ -0,0 +1,41 @@ +package message + +import ( + "github.com/jackc/pgx/v5/pgtype" + + mess "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" +) + +func convertFeedMessage(feed *mess.FeedMessage) []entity.Message { + res := make([]entity.Message, len(feed.Messages)) + for ind := range feed.Messages { + res[ind] = entity.Message{ + ID: int(feed.Messages[ind].GetId().Id), + From: int(feed.Messages[ind].UserFrom), + To: int(feed.Messages[ind].UserTo), + Content: pgtype.Text{ + String: feed.Messages[ind].GetContent(), + Valid: true, + }, + } + } + return res +} + +func convertFeedChat(feed *mess.FeedChat) entity.FeedUserChats { + res := make(entity.FeedUserChats, len(feed.Chats)) + + for ind := range feed.Chats { + res[ind] = entity.ChatWithUser{ + MessageLastID: int(feed.Chats[ind].GetLastMessageID()), + WichWhomChat: user.User{ + ID: int(feed.Chats[ind].GetChat().GetUserID()), + Username: feed.Chats[ind].GetChat().GetUsername(), + Avatar: feed.Chats[ind].GetChat().GetAvatar(), + }, + } + } + return res +} diff --git a/internal/pkg/usecase/message/mock/message_mock.go b/internal/pkg/usecase/message/mock/message_mock.go new file mode 100644 index 0000000..3b6d816 --- /dev/null +++ b/internal/pkg/usecase/message/mock/message_mock.go @@ -0,0 +1,142 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + message "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + message0 "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/message" + gomock "github.com/golang/mock/gomock" +) + +// MockUsecase is a mock of Usecase interface. +type MockUsecase struct { + ctrl *gomock.Controller + recorder *MockUsecaseMockRecorder +} + +// MockUsecaseMockRecorder is the mock recorder for MockUsecase. +type MockUsecaseMockRecorder struct { + mock *MockUsecase +} + +// NewMockUsecase creates a new mock instance. +func NewMockUsecase(ctrl *gomock.Controller) *MockUsecase { + mock := &MockUsecase{ctrl: ctrl} + mock.recorder = &MockUsecaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { + return m.recorder +} + +// DeleteMessage mocks base method. +func (m *MockUsecase) DeleteMessage(ctx context.Context, userID int, mes *message.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteMessage", ctx, userID, mes) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteMessage indicates an expected call of DeleteMessage. +func (mr *MockUsecaseMockRecorder) DeleteMessage(ctx, userID, mes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockUsecase)(nil).DeleteMessage), ctx, userID, mes) +} + +// GetMessage mocks base method. +func (m *MockUsecase) GetMessage(ctx context.Context, userID, messageID int) (*message.Message, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMessage", ctx, userID, messageID) + ret0, _ := ret[0].(*message.Message) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMessage indicates an expected call of GetMessage. +func (mr *MockUsecaseMockRecorder) GetMessage(ctx, userID, messageID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockUsecase)(nil).GetMessage), ctx, userID, messageID) +} + +// GetMessagesFromChat mocks base method. +func (m *MockUsecase) GetMessagesFromChat(ctx context.Context, userID int, chat message.Chat, count, lastID int) ([]message.Message, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMessagesFromChat", ctx, userID, chat, count, lastID) + ret0, _ := ret[0].([]message.Message) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetMessagesFromChat indicates an expected call of GetMessagesFromChat. +func (mr *MockUsecaseMockRecorder) GetMessagesFromChat(ctx, userID, chat, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessagesFromChat", reflect.TypeOf((*MockUsecase)(nil).GetMessagesFromChat), ctx, userID, chat, count, lastID) +} + +// GetUserChatsWithOtherUsers mocks base method. +func (m *MockUsecase) GetUserChatsWithOtherUsers(ctx context.Context, userID, count, lastID int) (message.FeedUserChats, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserChatsWithOtherUsers", ctx, userID, count, lastID) + ret0, _ := ret[0].(message.FeedUserChats) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetUserChatsWithOtherUsers indicates an expected call of GetUserChatsWithOtherUsers. +func (mr *MockUsecaseMockRecorder) GetUserChatsWithOtherUsers(ctx, userID, count, lastID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserChatsWithOtherUsers", reflect.TypeOf((*MockUsecase)(nil).GetUserChatsWithOtherUsers), ctx, userID, count, lastID) +} + +// SendMessage mocks base method. +func (m *MockUsecase) SendMessage(ctx context.Context, userID int, mes *message.Message) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendMessage", ctx, userID, mes) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SendMessage indicates an expected call of SendMessage. +func (mr *MockUsecaseMockRecorder) SendMessage(ctx, userID, mes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMessage", reflect.TypeOf((*MockUsecase)(nil).SendMessage), ctx, userID, mes) +} + +// SubscribeUserToAllChats mocks base method. +func (m *MockUsecase) SubscribeUserToAllChats(ctx context.Context, userID int) (<-chan message0.EventMessage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeUserToAllChats", ctx, userID) + ret0, _ := ret[0].(<-chan message0.EventMessage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SubscribeUserToAllChats indicates an expected call of SubscribeUserToAllChats. +func (mr *MockUsecaseMockRecorder) SubscribeUserToAllChats(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeUserToAllChats", reflect.TypeOf((*MockUsecase)(nil).SubscribeUserToAllChats), ctx, userID) +} + +// UpdateContentMessage mocks base method. +func (m *MockUsecase) UpdateContentMessage(ctx context.Context, userID int, mes *message.Message) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateContentMessage", ctx, userID, mes) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateContentMessage indicates an expected call of UpdateContentMessage. +func (mr *MockUsecaseMockRecorder) UpdateContentMessage(ctx, userID, mes interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContentMessage", reflect.TypeOf((*MockUsecase)(nil).UpdateContentMessage), ctx, userID, mes) +} diff --git a/internal/pkg/usecase/message/usecase.go b/internal/pkg/usecase/message/usecase.go new file mode 100644 index 0000000..a48369c --- /dev/null +++ b/internal/pkg/usecase/message/usecase.go @@ -0,0 +1,207 @@ +package message + +import ( + "context" + "errors" + "fmt" + "strconv" + + "github.com/jackc/pgx/v5/pgtype" + "google.golang.org/grpc/metadata" + + mess "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/messenger" + messMS "github.com/go-park-mail-ru/2023_2_OND_team/internal/microservices/messenger/delivery/grpc" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime/chat" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +var ErrNoAccess = errors.New("there is no access to perform this action") +var ErrRealTimeDisable = errors.New("realtime disable") +var ErrUnknowObj = errors.New("unknow object") + +//go:generate mockgen -destination=./mock/message_mock.go -package=mock -source=usecase.go Usecase +type Usecase interface { + GetUserChatsWithOtherUsers(ctx context.Context, userID, count, lastID int) (entity.FeedUserChats, int, error) + SendMessage(ctx context.Context, userID int, mes *entity.Message) (int, error) + GetMessagesFromChat(ctx context.Context, userID int, chat entity.Chat, count, lastID int) (feed []entity.Message, newLastID int, err error) + UpdateContentMessage(ctx context.Context, userID int, mes *entity.Message) error + DeleteMessage(ctx context.Context, userID int, mes *entity.Message) error + GetMessage(ctx context.Context, userID int, messageID int) (*entity.Message, error) + SubscribeUserToAllChats(ctx context.Context, userID int) (<-chan EventMessage, error) +} + +type EventMessage struct { + Type string + Message *entity.Message + Err error +} + +func makeErrEventMessage(err error) EventMessage { + return EventMessage{Err: err} +} + +type messageCase struct { + client mess.MessengerClient + realtimeChatCase chat.Usecase + log *logger.Logger + realtimeIsEnable bool +} + +func New(log *logger.Logger, cl mess.MessengerClient, rtChatCase chat.Usecase) *messageCase { + m := &messageCase{ + client: cl, + log: log, + } + + if rtChatCase != nil { + m.realtimeChatCase = rtChatCase + m.realtimeIsEnable = true + } + + return m +} + +func (m *messageCase) SendMessage(ctx context.Context, userID int, mes *entity.Message) (int, error) { + msgID, err := m.client.SendMessage(setAuthenticatedMetadataCtx(ctx, userID), &mess.Message{ + UserFrom: int64(userID), + UserTo: int64(mes.To), + Content: mes.Content.String, + }) + if err != nil { + return 0, fmt.Errorf("send message by grpc client") + } + + if m.realtimeIsEnable { + go m.realtimeChatCase.PublishNewMessage(ctx, mes.To, int(msgID.GetId())) + } + + return int(msgID.GetId()), nil +} + +func (m *messageCase) GetMessagesFromChat(ctx context.Context, userID int, chat entity.Chat, count, lastID int) (feed []entity.Message, newLastID int, err error) { + feedMsg, err := m.client.MessageFromChat(setAuthenticatedMetadataCtx(ctx, userID), &mess.FeedMessageRequest{ + Chat: &mess.Chat{ + UserID1: int64(chat[0]), + UserID2: int64(chat[1]), + }, + Count: int64(count), + LastID: int64(lastID), + }) + if err != nil { + err = fmt.Errorf("get message by : %w", err) + } + if feedMsg == nil { + return nil, 0, err + } + + return convertFeedMessage(feedMsg), int(feedMsg.LastID), nil +} + +func (m *messageCase) UpdateContentMessage(ctx context.Context, userID int, mes *entity.Message) error { + if _, err := m.client.UpdateMessage(setAuthenticatedMetadataCtx(ctx, userID), &mess.Message{ + Id: &mess.MsgID{ + Id: int64(mes.ID), + }, + Content: mes.Content.String, + }); err != nil { + return fmt.Errorf("update messege by grpc client") + } + + if m.realtimeIsEnable { + go m.realtimeChatCase.PublishUpdateMessage(ctx, mes.To, mes.ID) + } + + return nil +} + +func (m *messageCase) DeleteMessage(ctx context.Context, userID int, mes *entity.Message) error { + if _, err := m.client.DeleteMessage(setAuthenticatedMetadataCtx(ctx, userID), &mess.MsgID{Id: int64(mes.ID)}); err != nil { + return fmt.Errorf("delete messege by grpc client") + } + + if m.realtimeIsEnable { + go m.realtimeChatCase.PublishDeleteMessage(ctx, mes.To, mes.ID) + } + + return nil +} + +func (m *messageCase) GetMessage(ctx context.Context, userID int, messageID int) (*entity.Message, error) { + mes, err := m.client.GetMessage(setAuthenticatedMetadataCtx(ctx, userID), &mess.MsgID{Id: int64(messageID)}) + if err != nil { + return nil, fmt.Errorf("get message by grpc client") + } + return &entity.Message{ + ID: int(mes.GetId().Id), + From: int(mes.GetUserFrom()), + To: int(mes.GetUserTo()), + Content: pgtype.Text{ + String: mes.Content, + Valid: true, + }, + }, nil +} + +func (m *messageCase) GetUserChatsWithOtherUsers(ctx context.Context, userID, count, lastID int) (entity.FeedUserChats, int, error) { + feed, err := m.client.UserChatsWithOtherUsers(setAuthenticatedMetadataCtx(ctx, userID), &mess.FeedChatRequest{ + Count: int64(count), + LastID: int64(lastID), + }) + var errRes error + if err != nil { + errRes = fmt.Errorf("get user chats by grpc client: %w", err) + } + if feed == nil { + return nil, 0, errRes + } + return convertFeedChat(feed), int(feed.GetLastID()), errRes +} + +func (m *messageCase) SubscribeUserToAllChats(ctx context.Context, userID int) (<-chan EventMessage, error) { + if !m.realtimeIsEnable { + return nil, ErrRealTimeDisable + } + + subClient, err := m.realtimeChatCase.SubscribeUserToAllChats(ctx, userID) + if err != nil { + return nil, fmt.Errorf("subscribe: %w", err) + } + + chanEvMsg := make(chan EventMessage) + go m.receiveFromSubClient(ctx, userID, subClient, chanEvMsg) + return chanEvMsg, nil +} + +func (m *messageCase) receiveFromSubClient(ctx context.Context, userID int, subClient <-chan chat.EventMessageObjectID, chanEvMsg chan<- EventMessage) { + defer close(chanEvMsg) + + var ( + evMsg EventMessage + err error + ) + for msgObjID := range subClient { + if msgObjID.Err != nil { + chanEvMsg <- makeErrEventMessage(fmt.Errorf("receive from subcribtion client: %w", msgObjID.Err)) + return + } + + evMsg = EventMessage{ + Type: msgObjID.Type, + } + + evMsg.Message, err = m.GetMessage(ctx, userID, msgObjID.MessageID) + if err != nil { + m.log.Error(err.Error()) + evMsg.Err = err + } else if evMsg.Type == "delete" { + evMsg.Message.Content.String = "" + } + + chanEvMsg <- evMsg + } +} + +func setAuthenticatedMetadataCtx(ctx context.Context, userID int) context.Context { + return metadata.AppendToOutgoingContext(ctx, messMS.AuthenticatedMetadataKey, strconv.FormatInt(int64(userID), 10)) +} diff --git a/internal/pkg/usecase/pin/check.go b/internal/pkg/usecase/pin/check.go index 396a45d..4d0a751 100644 --- a/internal/pkg/usecase/pin/check.go +++ b/internal/pkg/usecase/pin/check.go @@ -6,6 +6,7 @@ import ( "fmt" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" ) var ( @@ -18,8 +19,6 @@ var ( const MaxSizeBatchPin = 100 -const UserUnknown = -1 - func (p *pinCase) IsAvailablePinForFixOnBoard(ctx context.Context, pinID, userID int) error { pin, err := p.repo.GetPinByID(ctx, pinID, false) if err != nil { @@ -55,7 +54,7 @@ func (p *pinCase) isAvailablePinForViewingUser(ctx context.Context, pin *entity. if pin.Public || pin.Author.ID == userID { return nil } - if userID == UserUnknown { + if userID == user.UserUnknown { return ErrPinNotAccess } @@ -71,9 +70,13 @@ func (p *pinCase) isAvailablePinForViewingUser(ctx context.Context, pin *entity. } func (p *pinCase) isAvailablePinForSetLike(ctx context.Context, pinID, userID int) error { + return p.IsAvailablePinForViewingUser(ctx, userID, pinID) +} + +func (p *pinCase) IsAvailablePinForViewingUser(ctx context.Context, userID, pinID int) error { pin, err := p.repo.GetPinByID(ctx, pinID, false) if err != nil { - return fmt.Errorf("get a pin to check for the availability of a like: %w", err) + return fmt.Errorf("get a pin to check for the availability: %w", err) } return p.isAvailablePinForViewingUser(ctx, pin, userID) diff --git a/internal/pkg/usecase/pin/update.go b/internal/pkg/usecase/pin/update.go index fc9281c..790e9e6 100644 --- a/internal/pkg/usecase/pin/update.go +++ b/internal/pkg/usecase/pin/update.go @@ -7,11 +7,14 @@ import ( "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/pin" ) +//go:generate easyjson update.go + +//easyjson:json type PinUpdateData struct { - Title *string - Description *string - Public *bool - Tags []string + Title *string `json:"title"` + Description *string `json:"description"` + Public *bool `json:"public"` + Tags []string `json:"tags"` } func (p *pinCase) EditPinByID(ctx context.Context, pinID, userID int, updateData *PinUpdateData) error { diff --git a/internal/pkg/usecase/pin/update_easyjson.go b/internal/pkg/usecase/pin/update_easyjson.go new file mode 100644 index 0000000..c3930bc --- /dev/null +++ b/internal/pkg/usecase/pin/update_easyjson.go @@ -0,0 +1,174 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package pin + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson2d86586fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecasePin(in *jlexer.Lexer, out *PinUpdateData) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "title": + if in.IsNull() { + in.Skip() + out.Title = nil + } else { + if out.Title == nil { + out.Title = new(string) + } + *out.Title = string(in.String()) + } + case "description": + if in.IsNull() { + in.Skip() + out.Description = nil + } else { + if out.Description == nil { + out.Description = new(string) + } + *out.Description = string(in.String()) + } + case "public": + if in.IsNull() { + in.Skip() + out.Public = nil + } else { + if out.Public == nil { + out.Public = new(bool) + } + *out.Public = bool(in.Bool()) + } + case "tags": + if in.IsNull() { + in.Skip() + out.Tags = nil + } else { + in.Delim('[') + if out.Tags == nil { + if !in.IsDelim(']') { + out.Tags = make([]string, 0, 4) + } else { + out.Tags = []string{} + } + } else { + out.Tags = (out.Tags)[:0] + } + for !in.IsDelim(']') { + var v1 string + v1 = string(in.String()) + out.Tags = append(out.Tags, v1) + in.WantComma() + } + in.Delim(']') + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson2d86586fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecasePin(out *jwriter.Writer, in PinUpdateData) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"title\":" + out.RawString(prefix[1:]) + if in.Title == nil { + out.RawString("null") + } else { + out.String(string(*in.Title)) + } + } + { + const prefix string = ",\"description\":" + out.RawString(prefix) + if in.Description == nil { + out.RawString("null") + } else { + out.String(string(*in.Description)) + } + } + { + const prefix string = ",\"public\":" + out.RawString(prefix) + if in.Public == nil { + out.RawString("null") + } else { + out.Bool(bool(*in.Public)) + } + } + { + const prefix string = ",\"tags\":" + out.RawString(prefix) + if in.Tags == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v2, v3 := range in.Tags { + if v2 > 0 { + out.RawByte(',') + } + out.String(string(v3)) + } + out.RawByte(']') + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v PinUpdateData) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson2d86586fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecasePin(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v PinUpdateData) MarshalEasyJSON(w *jwriter.Writer) { + easyjson2d86586fEncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecasePin(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *PinUpdateData) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson2d86586fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecasePin(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *PinUpdateData) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson2d86586fDecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecasePin(l, v) +} diff --git a/internal/pkg/usecase/pin/usecase.go b/internal/pkg/usecase/pin/usecase.go index 6385475..f288769 100644 --- a/internal/pkg/usecase/pin/usecase.go +++ b/internal/pkg/usecase/pin/usecase.go @@ -8,6 +8,7 @@ import ( "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" + userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" repo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/pin" "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/image" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" @@ -45,8 +46,11 @@ func New(log *log.Logger, imgCase image.Usecase, repo repo.Repository) *pinCase } func (p *pinCase) CreateNewPin(ctx context.Context, pin *entity.Pin, mimeTypePicture string, sizePicture int64, picture io.Reader) error { - picturePin, err := p.UploadImage("pins/", mimeTypePicture, sizePicture, picture, check.BothSidesFallIntoRange(100, 6000)) + picturePin, err := p.UploadImage(ctx, "pins/", mimeTypePicture, sizePicture, picture, check.BothSidesFallIntoRange(100, 6000)) if err != nil { + if err == image.ErrExplicitImage { + return err + } return fmt.Errorf("uploading an avatar when creating pin: %w", err) } pin.Picture = picturePin @@ -97,9 +101,26 @@ func (p *pinCase) ViewFeedPin(ctx context.Context, userID int, cfg pin.FeedPinCo return pin.FeedPin{}, ErrForbiddenAction } - if !hasBoard && (userID == UserUnknown || !hasUser || userID != user) && cfg.Protection != pin.FeedProtectionPublic { + if !hasBoard && (userID == userEntity.UserUnknown || !hasUser || userID != user) && cfg.Protection != pin.FeedProtectionPublic { return pin.FeedPin{}, ErrForbiddenAction } return p.repo.GetFeedPins(ctx, cfg) } + +func (p *pinCase) GetAuthorIdOfThePin(ctx context.Context, pinID int) (int, error) { + user, err := p.repo.GetAuthorPin(ctx, pinID) + if err != nil { + return 0, fmt.Errorf("get author id of the pin: %w", err) + } + return user.ID, nil +} + +func (p *pinCase) GetPinWithAuthor(ctx context.Context, pinID int) (*pin.Pin, error) { + pin, err := p.repo.GetPinByID(ctx, pinID, true) + if err != nil { + return nil, fmt.Errorf("get a pin with author: %w", err) + } + + return pin, nil +} diff --git a/internal/pkg/usecase/realtime/chat/chat.go b/internal/pkg/usecase/realtime/chat/chat.go new file mode 100644 index 0000000..7514d2a --- /dev/null +++ b/internal/pkg/usecase/realtime/chat/chat.go @@ -0,0 +1,114 @@ +package chat + +import ( + "context" + "fmt" + "strconv" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +type EventMessageObjectID struct { + Type string + MessageID int + Err error +} + +func makeErrEventMessageObjectID(err error) EventMessageObjectID { + return EventMessageObjectID{Err: err} +} + +type Usecase interface { + PublishNewMessage(ctx context.Context, userToWhom, msgID int) error + PublishUpdateMessage(ctx context.Context, userToWhom, msgID int) error + PublishDeleteMessage(ctx context.Context, userToWhom, msgID int) error + SubscribeUserToAllChats(ctx context.Context, userID int) (<-chan EventMessageObjectID, error) +} + +type realtimeCase struct { + client realtime.RealTimeClient + log *logger.Logger +} + +func New(client realtime.RealTimeClient, log *logger.Logger) *realtimeCase { + return &realtimeCase{client, log} +} + +func (r *realtimeCase) PublishNewMessage(ctx context.Context, userToWhom, msgID int) error { + err := r.publishMessage(ctx, userToWhom, msgID, rt.EventType_EV_CREATE) + if err != nil { + r.log.Error(err.Error()) + return fmt.Errorf("publish new message: %w", err) + } + return nil +} + +func (r *realtimeCase) PublishUpdateMessage(ctx context.Context, userToWhom, msgID int) error { + err := r.publishMessage(ctx, userToWhom, msgID, rt.EventType_EV_UPDATE) + if err != nil { + r.log.Error(err.Error()) + return fmt.Errorf("publish update message: %w", err) + } + return nil +} + +func (r *realtimeCase) PublishDeleteMessage(ctx context.Context, userToWhom, msgID int) error { + err := r.publishMessage(ctx, userToWhom, msgID, rt.EventType_EV_DELETE) + if err != nil { + r.log.Error(err.Error()) + return fmt.Errorf("publish delete message: %w", err) + } + return nil +} + +func (r *realtimeCase) SubscribeUserToAllChats(ctx context.Context, userToWhom int) (<-chan EventMessageObjectID, error) { + chPack, err := r.client.Subscribe(ctx, []string{strconv.Itoa(userToWhom)}) + if err != nil { + return nil, fmt.Errorf("subscribe user to all chats: %w", err) + } + + chanEvMsg := make(chan EventMessageObjectID) + go r.receiveFromSubClient(ctx, chPack, chanEvMsg) + + return chanEvMsg, nil +} + +func (r *realtimeCase) receiveFromSubClient(ctx context.Context, subClient <-chan realtime.Pack, chanEvMsg chan<- EventMessageObjectID) { + defer close(chanEvMsg) + + for pack := range subClient { + if pack.Err != nil { + chanEvMsg <- makeErrEventMessageObjectID(pack.Err) + return + } + + msg, ok := pack.Body.(*rt.Message_Object) + if !ok { + chanEvMsg <- makeErrEventMessageObjectID(realtime.ErrUnknownTypeObject) + return + } + + evMsgID := EventMessageObjectID{MessageID: int(msg.Object.GetId())} + switch msg.Object.GetType() { + case rt.EventType_EV_CREATE: + evMsgID.Type = "create" + case rt.EventType_EV_UPDATE: + evMsgID.Type = "update" + case rt.EventType_EV_DELETE: + evMsgID.Type = "delete" + } + + chanEvMsg <- evMsgID + } +} + +func (r *realtimeCase) publishMessage(ctx context.Context, userID, msgID int, t rt.EventType) error { + return r.client.Publish(ctx, strconv.Itoa(userID), &rt.Message_Object{ + Object: &rt.EventObject{ + Type: t, + Id: int64(msgID), + }, + }) +} diff --git a/internal/pkg/usecase/realtime/notification/comment.go b/internal/pkg/usecase/realtime/notification/comment.go new file mode 100644 index 0000000..0208aad --- /dev/null +++ b/internal/pkg/usecase/realtime/notification/comment.go @@ -0,0 +1,36 @@ +package notification + +import ( + "context" + "fmt" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" +) + +func (n *notificationClient) NotifyCommentLeftOnPin(ctx context.Context, commentID int) error { + notifier, ok := n.notifiers[entity.NotifyComment] + if !ok { + n.log.Error(ErrNotifierNotRegistered.Error()) + return ErrNotifierNotRegistered + } + + chanName, data, err := notifier.ChannelNameForPublishWithData(ctx, commentID) + if err != nil { + n.log.Error(err.Error()) + return fmt.Errorf("notify comment left on pin: %w", err) + } + + err = n.client.Publish(ctx, chanName, &rt.Message_Content{ + Content: &rt.EventMap{ + Type: int64(entity.NotifyComment), + M: data, + }, + }) + if err != nil { + n.log.Error(err.Error()) + return fmt.Errorf("publish to client: %w", err) + } + + return nil +} diff --git a/internal/pkg/usecase/realtime/notification/notification.go b/internal/pkg/usecase/realtime/notification/notification.go new file mode 100644 index 0000000..57d5ae2 --- /dev/null +++ b/internal/pkg/usecase/realtime/notification/notification.go @@ -0,0 +1,101 @@ +package notification + +import ( + "context" + "errors" + "fmt" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" + entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/notification" + notify "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/notification" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/usecase/realtime" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +var ErrNotifierNotRegistered = errors.New("notifier with this type not registered") + +type Usecase interface { + NotifyCommentLeftOnPin(ctx context.Context, commentID int) error +} + +type notificationClient struct { + client realtime.RealTimeClient + log *logger.Logger + notifiers map[entity.NotifyType]notify.Notifier +} + +func New(cl realtime.RealTimeClient, log *logger.Logger, opts ...Option) *notificationClient { + client := ¬ificationClient{ + client: cl, + log: log, + notifiers: make(map[entity.NotifyType]notify.Notifier), + } + + for _, opt := range opts { + opt.apply(client) + } + + return client +} + +func (n *notificationClient) SubscribeOnAllNotifications(ctx context.Context, userID int) (<-chan *entity.NotifyMessage, error) { + setChans := make(map[string]struct{}) + for t, notifier := range n.notifiers { + nameChans, err := notifier.ChannelsNameForSubscribe(ctx, userID) + if err != nil { + return nil, fmt.Errorf("receiving name channels for subscribe on %s notifier: %w", entity.TypeString(t), err) + } + + for _, name := range nameChans { + setChans[name] = struct{}{} + } + } + + uniqChans := make([]string, 0, len(setChans)) + + for nameChan := range setChans { + uniqChans = append(uniqChans, nameChan) + } + + chanPack, err := n.client.Subscribe(ctx, uniqChans) + if err != nil { + return nil, fmt.Errorf("subscribe on all notifications: %w", err) + } + + chanNotifyMsg := make(chan *entity.NotifyMessage) + + go n.pipelineNotify(chanPack, chanNotifyMsg) + + return chanNotifyMsg, nil +} + +func (n *notificationClient) pipelineNotify(chRecv <-chan realtime.Pack, chSend chan<- *entity.NotifyMessage) { + defer close(chSend) + + for pack := range chRecv { + if pack.Err != nil { + chSend <- entity.NewNotifyMessageWithError(pack.Err) + return + } + + notifyData, ok := pack.Body.(*rt.Message_Content) + if !ok { + chSend <- entity.NewNotifyMessageWithError(realtime.ErrUnknownTypeObject) + return + } + + notifier, ok := n.notifiers[entity.NotifyType(notifyData.Content.GetType())] + if !ok { + chSend <- entity.NewNotifyMessageWithError(ErrNotifierNotRegistered) + return + } + + msg, err := notifier.MessageNotify(notifyData.Content.GetM()) + if err != nil { + chSend <- entity.NewNotifyMessageWithError(err) + return + } + + chSend <- msg + } +} diff --git a/internal/pkg/usecase/realtime/notification/option.go b/internal/pkg/usecase/realtime/notification/option.go new file mode 100644 index 0000000..0978634 --- /dev/null +++ b/internal/pkg/usecase/realtime/notification/option.go @@ -0,0 +1,19 @@ +package notification + +import notify "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/notification" + +type Option interface { + apply(*notificationClient) +} + +type funcOption func(*notificationClient) + +func (f funcOption) apply(cl *notificationClient) { + f(cl) +} + +func Register(notifier notify.Notifier) Option { + return funcOption(func(cl *notificationClient) { + cl.notifiers[notifier.Type()] = notifier + }) +} diff --git a/internal/pkg/usecase/realtime/realtime.go b/internal/pkg/usecase/realtime/realtime.go new file mode 100644 index 0000000..e5af26f --- /dev/null +++ b/internal/pkg/usecase/realtime/realtime.go @@ -0,0 +1,109 @@ +package realtime + +import ( + "context" + "errors" + "fmt" + + rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" +) + +var ErrUnknownTypeObject = errors.New("unknown type") + +const ( + _topicChat = "chat" + _topicNotification = "notification" +) + +type RealTimeClient interface { + Subscribe(ctx context.Context, nameChans []string) (<-chan Pack, error) + Publish(ctx context.Context, chanName string, object any) error +} + +type Pack struct { + Body any + Err error +} + +type realtimeClient struct { + client rt.RealTimeClient + topic string +} + +func NewRealTimeChatClient(client rt.RealTimeClient) realtimeClient { + return realtimeClient{ + client: client, + topic: _topicChat, + } +} + +func NewRealTimeNotificationClient(client rt.RealTimeClient) realtimeClient { + return realtimeClient{ + client: client, + topic: _topicNotification, + } +} + +func (r realtimeClient) Publish(ctx context.Context, chanName string, object any) error { + pubMsg := &rt.PublishMessage{ + Channel: &rt.Channel{ + Topic: r.topic, + Name: chanName, + }, + Message: &rt.Message{}, + } + + switch body := object.(type) { + case *rt.Message_Object: + pubMsg.Message.Body = body + case *rt.Message_Content: + pubMsg.Message.Body = body + default: + return ErrUnknownTypeObject + } + + _, err := r.client.Publish(ctx, pubMsg) + if err != nil { + return fmt.Errorf("publish as a realtime client: %w", err) + } + return nil +} + +func (r realtimeClient) Subscribe(ctx context.Context, nameChans []string) (<-chan Pack, error) { + chans := &rt.Channels{ + Chans: make([]*rt.Channel, len(nameChans)), + } + + for _, name := range nameChans { + chans.Chans = append(chans.Chans, &rt.Channel{Topic: r.topic, Name: name}) + } + + subClient, err := r.client.Subscribe(ctx, chans) + if err != nil { + return nil, fmt.Errorf("subscribe as a realtime client: %w", err) + } + + ch := make(chan Pack) + go runServeSubscribeClient(subClient, ch) + + return ch, nil +} + +func runServeSubscribeClient(client rt.RealTime_SubscribeClient, ch chan<- Pack) { + defer close(ch) + + var ( + mes *rt.Message + err error + ) + + for { + mes, err = client.Recv() + if err != nil { + ch <- Pack{Err: err} + return + } + + ch <- Pack{Body: mes.GetBody()} + } +} diff --git a/internal/pkg/usecase/search/mock/search_mock.go b/internal/pkg/usecase/search/mock/search_mock.go new file mode 100644 index 0000000..dd303a6 --- /dev/null +++ b/internal/pkg/usecase/search/mock/search_mock.go @@ -0,0 +1,81 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + search "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" + gomock "github.com/golang/mock/gomock" +) + +// MockUsecase is a mock of Usecase interface. +type MockUsecase struct { + ctrl *gomock.Controller + recorder *MockUsecaseMockRecorder +} + +// MockUsecaseMockRecorder is the mock recorder for MockUsecase. +type MockUsecaseMockRecorder struct { + mock *MockUsecase +} + +// NewMockUsecase creates a new mock instance. +func NewMockUsecase(ctrl *gomock.Controller) *MockUsecase { + mock := &MockUsecase{ctrl: ctrl} + mock.recorder = &MockUsecaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { + return m.recorder +} + +// GetBoards mocks base method. +func (m *MockUsecase) GetBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBoards", ctx, opts) + ret0, _ := ret[0].([]search.BoardForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBoards indicates an expected call of GetBoards. +func (mr *MockUsecaseMockRecorder) GetBoards(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoards", reflect.TypeOf((*MockUsecase)(nil).GetBoards), ctx, opts) +} + +// GetPins mocks base method. +func (m *MockUsecase) GetPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPins", ctx, opts) + ret0, _ := ret[0].([]search.PinForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPins indicates an expected call of GetPins. +func (mr *MockUsecaseMockRecorder) GetPins(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPins", reflect.TypeOf((*MockUsecase)(nil).GetPins), ctx, opts) +} + +// GetUsers mocks base method. +func (m *MockUsecase) GetUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUsers", ctx, opts) + ret0, _ := ret[0].([]search.UserForSearch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUsers indicates an expected call of GetUsers. +func (mr *MockUsecaseMockRecorder) GetUsers(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsers", reflect.TypeOf((*MockUsecase)(nil).GetUsers), ctx, opts) +} diff --git a/internal/pkg/usecase/search/usecase.go b/internal/pkg/usecase/search/usecase.go new file mode 100644 index 0000000..4cfbea8 --- /dev/null +++ b/internal/pkg/usecase/search/usecase.go @@ -0,0 +1,49 @@ +package search + +import ( + "context" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/search" + sRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/search" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +//go:generate mockgen -destination=./mock/search_mock.go -package=mock -source=usecase.go Usecase +type Usecase interface { + GetUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) + GetBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) + GetPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) +} + +type searchUsecase struct { + log *logger.Logger + searchRepo sRepo.Repository +} + +func New(log *logger.Logger, searchRepo sRepo.Repository) Usecase { + return &searchUsecase{log: log, searchRepo: searchRepo} +} + +func (u *searchUsecase) GetUsers(ctx context.Context, opts *search.SearchOpts) ([]search.UserForSearch, error) { + users, err := u.searchRepo.GetFilteredUsers(ctx, opts) + if err != nil { + return nil, err + } + return users, nil +} + +func (u *searchUsecase) GetBoards(ctx context.Context, opts *search.SearchOpts) ([]search.BoardForSearch, error) { + boards, err := u.searchRepo.GetFilteredBoards(ctx, opts) + if err != nil { + return nil, err + } + return boards, nil +} + +func (u *searchUsecase) GetPins(ctx context.Context, opts *search.SearchOpts) ([]search.PinForSearch, error) { + pins, err := u.searchRepo.GetFilteredPins(ctx, opts) + if err != nil { + return nil, err + } + return pins, nil +} diff --git a/internal/pkg/usecase/subscription/create.go b/internal/pkg/usecase/subscription/create.go new file mode 100644 index 0000000..dba99a0 --- /dev/null +++ b/internal/pkg/usecase/subscription/create.go @@ -0,0 +1,15 @@ +package subscription + +import "context" + +func (u *subscriptionUsecase) SubscribeToUser(ctx context.Context, from, to int) error { + if from == to { + return &ErrSelfSubscription{} + } + + if err := u.userRepo.CheckUserExistence(ctx, to); err != nil { + return err + } + + return u.subRepo.CreateSubscriptionUser(ctx, from, to) +} diff --git a/internal/pkg/usecase/subscription/delete.go b/internal/pkg/usecase/subscription/delete.go new file mode 100644 index 0000000..c8c95f2 --- /dev/null +++ b/internal/pkg/usecase/subscription/delete.go @@ -0,0 +1,15 @@ +package subscription + +import "context" + +func (u *subscriptionUsecase) UnsubscribeFromUser(ctx context.Context, from, to int) error { + if from == to { + return &ErrSelfUnsubscription{} + } + + if err := u.userRepo.CheckUserExistence(ctx, to); err != nil { + return err + } + + return u.subRepo.DeleteSubscriptionUser(ctx, from, to) +} diff --git a/internal/pkg/usecase/subscription/errors.go b/internal/pkg/usecase/subscription/errors.go new file mode 100644 index 0000000..4b2e376 --- /dev/null +++ b/internal/pkg/usecase/subscription/errors.go @@ -0,0 +1,39 @@ +package subscription + +import ( + "fmt" + + errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" +) + +type ErrSelfSubscription struct{} + +func (e *ErrSelfSubscription) Error() string { + return "can't subscribe on yourself" +} + +func (e *ErrSelfSubscription) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrSelfUnsubscription struct{} + +func (e *ErrSelfUnsubscription) Error() string { + return "can't unsubscribe from yourself" +} + +func (e *ErrSelfUnsubscription) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} + +type ErrInvalidFilter struct { + filter string +} + +func (e *ErrInvalidFilter) Error() string { + return fmt.Sprintf("invalid filter: %s", e.filter) +} + +func (e *ErrInvalidFilter) Type() errPkg.Type { + return errPkg.ErrInvalidInput +} diff --git a/internal/pkg/usecase/subscription/get.go b/internal/pkg/usecase/subscription/get.go new file mode 100644 index 0000000..2217318 --- /dev/null +++ b/internal/pkg/usecase/subscription/get.go @@ -0,0 +1,33 @@ +package subscription + +import ( + "context" + + userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" +) + +func (u *subscriptionUsecase) GetSubscriptionInfoForUser(ctx context.Context, subOpts *userEntity.SubscriptionOpts) ([]userEntity.SubscriptionUser, error) { + if err := u.userRepo.CheckUserExistence(ctx, subOpts.UserID); err != nil { + return nil, err + } + + currUserID, _ := ctx.Value(auth.KeyCurrentUserID).(int) + var ( + users []userEntity.SubscriptionUser + err error + ) + switch subOpts.Filter { + case "subscriptions": + users, err = u.subRepo.GetUserSubscriptions(ctx, subOpts.UserID, subOpts.Count, subOpts.LastID, currUserID) + case "subscribers": + users, err = u.subRepo.GetUserSubscribers(ctx, subOpts.UserID, subOpts.Count, subOpts.LastID, currUserID) + default: + return nil, &ErrInvalidFilter{subOpts.Filter} + } + if err != nil { + return nil, err + } + + return users, nil +} diff --git a/internal/pkg/usecase/subscription/mock/subscription_mock.go b/internal/pkg/usecase/subscription/mock/subscription_mock.go new file mode 100644 index 0000000..6d3aab8 --- /dev/null +++ b/internal/pkg/usecase/subscription/mock/subscription_mock.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: usecase.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + user "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + gomock "github.com/golang/mock/gomock" +) + +// MockUsecase is a mock of Usecase interface. +type MockUsecase struct { + ctrl *gomock.Controller + recorder *MockUsecaseMockRecorder +} + +// MockUsecaseMockRecorder is the mock recorder for MockUsecase. +type MockUsecaseMockRecorder struct { + mock *MockUsecase +} + +// NewMockUsecase creates a new mock instance. +func NewMockUsecase(ctrl *gomock.Controller) *MockUsecase { + mock := &MockUsecase{ctrl: ctrl} + mock.recorder = &MockUsecaseMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { + return m.recorder +} + +// GetSubscriptionInfoForUser mocks base method. +func (m *MockUsecase) GetSubscriptionInfoForUser(ctx context.Context, subOpts *user.SubscriptionOpts) ([]user.SubscriptionUser, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubscriptionInfoForUser", ctx, subOpts) + ret0, _ := ret[0].([]user.SubscriptionUser) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubscriptionInfoForUser indicates an expected call of GetSubscriptionInfoForUser. +func (mr *MockUsecaseMockRecorder) GetSubscriptionInfoForUser(ctx, subOpts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptionInfoForUser", reflect.TypeOf((*MockUsecase)(nil).GetSubscriptionInfoForUser), ctx, subOpts) +} + +// SubscribeToUser mocks base method. +func (m *MockUsecase) SubscribeToUser(ctx context.Context, from, to int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SubscribeToUser", ctx, from, to) + ret0, _ := ret[0].(error) + return ret0 +} + +// SubscribeToUser indicates an expected call of SubscribeToUser. +func (mr *MockUsecaseMockRecorder) SubscribeToUser(ctx, from, to interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeToUser", reflect.TypeOf((*MockUsecase)(nil).SubscribeToUser), ctx, from, to) +} + +// UnsubscribeFromUser mocks base method. +func (m *MockUsecase) UnsubscribeFromUser(ctx context.Context, from, to int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnsubscribeFromUser", ctx, from, to) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnsubscribeFromUser indicates an expected call of UnsubscribeFromUser. +func (mr *MockUsecaseMockRecorder) UnsubscribeFromUser(ctx, from, to interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnsubscribeFromUser", reflect.TypeOf((*MockUsecase)(nil).UnsubscribeFromUser), ctx, from, to) +} diff --git a/internal/pkg/usecase/subscription/usecase.go b/internal/pkg/usecase/subscription/usecase.go new file mode 100644 index 0000000..d44b5bb --- /dev/null +++ b/internal/pkg/usecase/subscription/usecase.go @@ -0,0 +1,27 @@ +package subscription + +import ( + "context" + + userEntity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + subRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/subscription" + uRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/user" + "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" +) + +//go:generate mockgen -destination=./mock/subscription_mock.go -package=mock -source=usecase.go Usecase +type Usecase interface { + SubscribeToUser(ctx context.Context, from, to int) error + UnsubscribeFromUser(ctx context.Context, from, to int) error + GetSubscriptionInfoForUser(ctx context.Context, subOpts *userEntity.SubscriptionOpts) ([]userEntity.SubscriptionUser, error) +} + +type subscriptionUsecase struct { + subRepo subRepo.Repository + userRepo uRepo.Repository + log *logger.Logger +} + +func New(log *logger.Logger, subRepo subRepo.Repository, uRepo uRepo.Repository) Usecase { + return &subscriptionUsecase{subRepo: subRepo, userRepo: uRepo, log: log} +} diff --git a/internal/pkg/usecase/user/credentials.go b/internal/pkg/usecase/user/credentials.go index c8d95d6..9e3857c 100644 --- a/internal/pkg/usecase/user/credentials.go +++ b/internal/pkg/usecase/user/credentials.go @@ -1,6 +1,9 @@ package user +//go:generate easyjson credentials.go + +//easyjson:json type UserCredentials struct { - Username string - Password string + Username string `json:"username"` + Password string `json:"password"` } diff --git a/internal/pkg/usecase/user/credentials_easyjson.go b/internal/pkg/usecase/user/credentials_easyjson.go new file mode 100644 index 0000000..d303bee --- /dev/null +++ b/internal/pkg/usecase/user/credentials_easyjson.go @@ -0,0 +1,92 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package user + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjson5b679028DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(in *jlexer.Lexer, out *UserCredentials) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "username": + out.Username = string(in.String()) + case "password": + out.Password = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson5b679028EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(out *jwriter.Writer, in UserCredentials) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"username\":" + out.RawString(prefix[1:]) + out.String(string(in.Username)) + } + { + const prefix string = ",\"password\":" + out.RawString(prefix) + out.String(string(in.Password)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v UserCredentials) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson5b679028EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v UserCredentials) MarshalEasyJSON(w *jwriter.Writer) { + easyjson5b679028EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *UserCredentials) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson5b679028DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *UserCredentials) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson5b679028DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(l, v) +} diff --git a/internal/pkg/usecase/user/info.go b/internal/pkg/usecase/user/info.go index 1993261..b504f5f 100644 --- a/internal/pkg/usecase/user/info.go +++ b/internal/pkg/usecase/user/info.go @@ -1,10 +1,13 @@ package user +//go:generate easyjson info.go + +//easyjson:json type ProfileUpdateData struct { - Username *string - Email *string - Name *string - Surname *string + Username *string `json:"username"` + Email *string `json:"email"` + Name *string `json:"name"` + Surname *string `json:"surname"` AboutMe *string `json:"about_me"` - Password *string + Password *string `json:"password"` } diff --git a/internal/pkg/usecase/user/info_easyjson.go b/internal/pkg/usecase/user/info_easyjson.go new file mode 100644 index 0000000..af29bb5 --- /dev/null +++ b/internal/pkg/usecase/user/info_easyjson.go @@ -0,0 +1,192 @@ +// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. + +package user + +import ( + json "encoding/json" + easyjson "github.com/mailru/easyjson" + jlexer "github.com/mailru/easyjson/jlexer" + jwriter "github.com/mailru/easyjson/jwriter" +) + +// suppress unused package warning +var ( + _ *json.RawMessage + _ *jlexer.Lexer + _ *jwriter.Writer + _ easyjson.Marshaler +) + +func easyjsonDdc53814DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(in *jlexer.Lexer, out *ProfileUpdateData) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "username": + if in.IsNull() { + in.Skip() + out.Username = nil + } else { + if out.Username == nil { + out.Username = new(string) + } + *out.Username = string(in.String()) + } + case "email": + if in.IsNull() { + in.Skip() + out.Email = nil + } else { + if out.Email == nil { + out.Email = new(string) + } + *out.Email = string(in.String()) + } + case "name": + if in.IsNull() { + in.Skip() + out.Name = nil + } else { + if out.Name == nil { + out.Name = new(string) + } + *out.Name = string(in.String()) + } + case "surname": + if in.IsNull() { + in.Skip() + out.Surname = nil + } else { + if out.Surname == nil { + out.Surname = new(string) + } + *out.Surname = string(in.String()) + } + case "about_me": + if in.IsNull() { + in.Skip() + out.AboutMe = nil + } else { + if out.AboutMe == nil { + out.AboutMe = new(string) + } + *out.AboutMe = string(in.String()) + } + case "password": + if in.IsNull() { + in.Skip() + out.Password = nil + } else { + if out.Password == nil { + out.Password = new(string) + } + *out.Password = string(in.String()) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonDdc53814EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(out *jwriter.Writer, in ProfileUpdateData) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"username\":" + out.RawString(prefix[1:]) + if in.Username == nil { + out.RawString("null") + } else { + out.String(string(*in.Username)) + } + } + { + const prefix string = ",\"email\":" + out.RawString(prefix) + if in.Email == nil { + out.RawString("null") + } else { + out.String(string(*in.Email)) + } + } + { + const prefix string = ",\"name\":" + out.RawString(prefix) + if in.Name == nil { + out.RawString("null") + } else { + out.String(string(*in.Name)) + } + } + { + const prefix string = ",\"surname\":" + out.RawString(prefix) + if in.Surname == nil { + out.RawString("null") + } else { + out.String(string(*in.Surname)) + } + } + { + const prefix string = ",\"about_me\":" + out.RawString(prefix) + if in.AboutMe == nil { + out.RawString("null") + } else { + out.String(string(*in.AboutMe)) + } + } + { + const prefix string = ",\"password\":" + out.RawString(prefix) + if in.Password == nil { + out.RawString("null") + } else { + out.String(string(*in.Password)) + } + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v ProfileUpdateData) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjsonDdc53814EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v ProfileUpdateData) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonDdc53814EncodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *ProfileUpdateData) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjsonDdc53814DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *ProfileUpdateData) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonDdc53814DecodeGithubComGoParkMailRu20232ONDTeamInternalPkgUsecaseUser(l, v) +} diff --git a/internal/pkg/usecase/user/mock/user_mock.go b/internal/pkg/usecase/user/mock/user_mock.go index 167e063..745c994 100644 --- a/internal/pkg/usecase/user/mock/user_mock.go +++ b/internal/pkg/usecase/user/mock/user_mock.go @@ -97,6 +97,39 @@ func (mr *MockUsecaseMockRecorder) GetAllProfileInfo(ctx, userID interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllProfileInfo", reflect.TypeOf((*MockUsecase)(nil).GetAllProfileInfo), ctx, userID) } +// GetProfileInfo mocks base method. +func (m *MockUsecase) GetProfileInfo(ctx context.Context) (*user.User, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProfileInfo", ctx) + ret0, _ := ret[0].(*user.User) + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetProfileInfo indicates an expected call of GetProfileInfo. +func (mr *MockUsecaseMockRecorder) GetProfileInfo(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProfileInfo", reflect.TypeOf((*MockUsecase)(nil).GetProfileInfo), ctx) +} + +// GetUserInfo mocks base method. +func (m *MockUsecase) GetUserInfo(ctx context.Context, userID int) (*user.User, bool, int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserInfo", ctx, userID) + ret0, _ := ret[0].(*user.User) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(int) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// GetUserInfo indicates an expected call of GetUserInfo. +func (mr *MockUsecaseMockRecorder) GetUserInfo(ctx, userID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInfo", reflect.TypeOf((*MockUsecase)(nil).GetUserInfo), ctx, userID) +} + // Register mocks base method. func (m *MockUsecase) Register(ctx context.Context, user *user.User) error { m.ctrl.T.Helper() diff --git a/internal/pkg/usecase/user/profile.go b/internal/pkg/usecase/user/profile.go index 5cb4f34..414c4a0 100644 --- a/internal/pkg/usecase/user/profile.go +++ b/internal/pkg/usecase/user/profile.go @@ -7,6 +7,7 @@ import ( "io" entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/middleware/auth" repository "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/user" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/crypto" "github.com/go-park-mail-ru/2023_2_OND_team/pkg/validator/image/check" @@ -15,7 +16,7 @@ import ( var ErrBadBody = errors.New("bad body avatar") func (u *userCase) UpdateUserAvatar(ctx context.Context, userID int, mimeTypeAvatar string, sizeAvatar int64, avatar io.Reader) error { - avatarProfile, err := u.UploadImage("avatars/", mimeTypeAvatar, sizeAvatar, avatar, check.BothSidesFallIntoRange(200, 1800)) + avatarProfile, err := u.UploadImage(ctx, "avatars/", mimeTypeAvatar, sizeAvatar, avatar, check.BothSidesFallIntoRange(200, 1800)) if err != nil { return fmt.Errorf("uploading an avatar when updating avatar profile: %w", err) } @@ -27,6 +28,15 @@ func (u *userCase) UpdateUserAvatar(ctx context.Context, userID int, mimeTypeAva return nil } +func (u *userCase) GetUserInfo(ctx context.Context, userID int) (user *entity.User, isSubscribed bool, subsCount int, err error) { + currUserID, _ := ctx.Value(auth.KeyCurrentUserID).(int) + return u.repo.GetUserData(ctx, userID, currUserID) +} + +func (u *userCase) GetProfileInfo(ctx context.Context) (user *entity.User, subsCount int, err error) { + return u.repo.GetProfileData(ctx, ctx.Value(auth.KeyCurrentUserID).(int)) +} + func (u *userCase) GetAllProfileInfo(ctx context.Context, userID int) (*entity.User, error) { return u.repo.GetAllUserData(ctx, userID) } diff --git a/internal/pkg/usecase/user/usecase.go b/internal/pkg/usecase/user/usecase.go index 167860c..c9a3eb9 100644 --- a/internal/pkg/usecase/user/usecase.go +++ b/internal/pkg/usecase/user/usecase.go @@ -25,6 +25,8 @@ type Usecase interface { FindOutUsernameAndAvatar(ctx context.Context, userID int) (username string, avatar string, err error) UpdateUserAvatar(ctx context.Context, userID int, mimeTypeAvatar string, sizeAvatar int64, avatar io.Reader) error GetAllProfileInfo(ctx context.Context, userID int) (*entity.User, error) + GetUserInfo(ctx context.Context, userID int) (user *entity.User, isSubscribed bool, subsCount int, err error) + GetProfileInfo(ctx context.Context) (user *entity.User, subsCount int, err error) EditProfileInfo(ctx context.Context, userID int, updateData *ProfileUpdateData) error } diff --git a/internal/pkg/validation/censor.go b/internal/pkg/validation/censor.go new file mode 100644 index 0000000..4180389 --- /dev/null +++ b/internal/pkg/validation/censor.go @@ -0,0 +1,107 @@ +package validation + +import ( + "encoding/base64" + "log" + "strings" + + goaway "github.com/TwiN/go-away" +) + +var additionalProfanity = []string{ + "0YXRgw==", + "0L/QuNC30LTQsA==", + "0YfQu9C10L0=", + "0LLQsNCz0LjQvQ==", + "0LPQvtCy0L3Qvg==", + "0L/QsNGA0LDRiNCw", + "0YHRg9C60LA=", + "0L/QuNGB0YzQug==", + "0YHQuNGB0YzQug==", + "0LLQu9Cw0LPQsNC70LjRiQ==", + "0L/QtdC90LjRgQ==", + "0LHQu9GP0LTRjA==", + "0YjQu9GO0YU=", + "0L/RgNC+0YHRgtC40YLRg9GC0Lo=", + "0L3QuNCz0LXRgA==", + "0L3QtdCz0YA=", + "0YPQt9C60L7Qs9C70LDQt9GL", + "0YXQtdGA", + "0LXQsdCw0YLRjA==", + "0YPQtdCx0LDQvQ==", + "0YPQtdCx0L7Qug==", + "0YLRgNCw0YXQsNGC0Yw=", + "0YLQstCw0YDRjA==", + "0L/QuNC00YA=", + "0YPRgNC+0LQ=", + "0L/QuNC30LTQtdGG", + "0YXRg9GP", + "0LfQsNC70YPQvw==", + "0L/QuNC00LDRgNCw0YE=", + "0LvQvtGF", + "0LPQsNC90LTQvtC9", + "0LTRgNC+0YfQuNGC0Yw=", + "0LDQvdCw0Ls=", + "0LbQvtC/0LA=", + "0LPQvdC40LTQsA==", + "0YPRiNC70LXQv9C+0Lo=", + "0YHRg9GH0LXQvdGL0Yg=", + "0YHQv9C10YDQvNCw", + "0LHQu9GP0YLRjA==", + "0L/QvtGA0L3Qvg==", + "0YHRgNCw0YLRjA==", + "0YfQvNC+", + "0LTQtdCx0LjQuw==", + "0LrRgNC10YLQuNC9", + "cGl6ZGE=", + "0LXQsdCw0Ls=", + "0LPQsNCy0L3Qvg==", + "0LPQvtC90LTQvtC9", + "0YXRg9C1", + "0LXQsdCw0L0=", + "0LXQsdC70LDQvQ==", + "0LXQsdGD0YfQuA==", + "0LXQsdC70LjQstGL", + "0L/QuNC00YDQuNC7", + "0L/QvtGA0L3Rg9GF0LA=", + "0LXQsdC70Y8=", + "0YPQtdCx0LjRidC90Ys=", +} + +func GetLabels() []string { + decodedLabels := make([]string, 0, len(additionalProfanity)) + for _, badEncoded := range additionalProfanity { + decoded, err := base64.StdEncoding.DecodeString(badEncoded) + if err != nil { + log.Println(err) + } + decodedLabels = append(decodedLabels, string(decoded)) + } + return decodedLabels +} + +type ProfanityCensor interface { + IsProfane(string) bool + Sanitize(string) string +} + +type defaultCensor struct { + censor *goaway.ProfanityDetector +} + +func (c *defaultCensor) IsProfane(s string) bool { + for _, word := range strings.Fields(s) { + if c.censor.IsProfane(strings.ToLower(word)) { + return true + } + } + return false +} + +func (c *defaultCensor) Sanitize(s string) string { + return c.censor.Censor(s) +} + +func NewCensor(censor *goaway.ProfanityDetector) ProfanityCensor { + return &defaultCensor{censor} +} diff --git a/internal/pkg/validation/xss.go b/internal/pkg/validation/xss.go new file mode 100644 index 0000000..5ecce98 --- /dev/null +++ b/internal/pkg/validation/xss.go @@ -0,0 +1,19 @@ +package validation + +import "github.com/microcosm-cc/bluemonday" + +type SanitizerXSS interface { + Sanitize(string) string +} + +type bluemondaySanitizer struct { + sanitizer *bluemonday.Policy +} + +func (san *bluemondaySanitizer) Sanitize(s string) string { + return san.sanitizer.Sanitize(s) +} + +func NewSanitizerXSS(sanitizer *bluemonday.Policy) SanitizerXSS { + return &bluemondaySanitizer{sanitizer} +} diff --git a/internal/usecases/pin/usecase.go b/internal/usecases/pin/usecase.go deleted file mode 100644 index 2dbec72..0000000 --- a/internal/usecases/pin/usecase.go +++ /dev/null @@ -1,29 +0,0 @@ -package pin - -import ( - "context" - - entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/pin" - repo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/pin" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" -) - -type Usecase struct { - log *logger.Logger - repo repo.Repository -} - -func New(log *logger.Logger, repo repo.Repository) *Usecase { - return &Usecase{log, repo} -} - -func (u *Usecase) SelectNewPins(ctx context.Context, count, lastID int) ([]entity.Pin, int) { - pins, err := u.repo.GetSortedNPinsAfterID(ctx, count, lastID) - if err != nil { - u.log.Error(err.Error()) - } - if len(pins) == 0 { - return []entity.Pin{}, lastID - } - return pins, pins[len(pins)-1].ID -} diff --git a/internal/usecases/pin/usecase_test.go b/internal/usecases/pin/usecase_test.go deleted file mode 100644 index fa58f07..0000000 --- a/internal/usecases/pin/usecase_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package pin - -import ( - "context" - "math/rand" - "strconv" - "testing" - - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/ramrepo" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" - "github.com/stretchr/testify/require" -) - -func TestSelectNewPins(t *testing.T) { - log, _ := logger.New(logger.RFC3339FormatTime()) - defer log.Sync() - - db, _ := ramrepo.OpenDB(strconv.FormatInt(int64(rand.Int()), 10)) - defer db.Close() - - pinCase := New(log, ramrepo.NewRamPinRepo(db)) - - testCases := []struct { - name string - count, lastID int - expNewLastID int - }{ - { - name: "provide correct count and lastID", - count: 2, - lastID: 1, - expNewLastID: 3, - }, - { - name: "provide incorrect count", - count: -2, - lastID: 1, - expNewLastID: 1, - }, - } - - for _, tCase := range testCases { - t.Run(tCase.name, func(t *testing.T) { - _, actualLastID := pinCase.SelectNewPins(context.Background(), tCase.count, tCase.lastID) - require.Equal(t, tCase.expNewLastID, actualLastID) - }) - } -} diff --git a/internal/usecases/session/manager.go b/internal/usecases/session/manager.go deleted file mode 100644 index a7a3b8e..0000000 --- a/internal/usecases/session/manager.go +++ /dev/null @@ -1,61 +0,0 @@ -package session - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/session" - repo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/session" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/crypto" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" -) - -const SessionLifeTime = 30 * 24 * time.Hour - -const lenSessionKey = 16 - -var ErrExpiredSession = errors.New("session lifetime expired") - -type SessionManager struct { - log *logger.Logger - repo repo.Repository -} - -func New(log *logger.Logger, repo repo.Repository) *SessionManager { - return &SessionManager{log, repo} -} - -func (sm *SessionManager) CreateNewSessionForUser(ctx context.Context, userID int) (*session.Session, error) { - sessionKey, err := crypto.NewRandomString(lenSessionKey) - if err != nil { - return nil, fmt.Errorf("session key generation for new session: %w", err) - } - - session := &session.Session{ - Key: sessionKey, - UserID: userID, - Expire: time.Now().UTC().Add(SessionLifeTime), - } - err = sm.repo.AddSession(ctx, session) - if err != nil { - return nil, fmt.Errorf("creating a new session by the session manager: %w", err) - } - return session, nil -} - -func (sm *SessionManager) GetUserIDBySessionKey(ctx context.Context, sessionKey string) (int, error) { - session, err := sm.repo.GetSessionByKey(ctx, sessionKey) - if err != nil { - return 0, fmt.Errorf("getting a session by a manager: %w", err) - } - if time.Now().UTC().After(session.Expire) { - return 0, ErrExpiredSession - } - return session.UserID, nil -} - -func (sm *SessionManager) DeleteUserSession(ctx context.Context, key string) error { - return sm.repo.DeleteSessionByKey(ctx, key) -} diff --git a/internal/usecases/session/manager_test.go b/internal/usecases/session/manager_test.go deleted file mode 100644 index 89a3633..0000000 --- a/internal/usecases/session/manager_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package session - -import ( - "context" - "fmt" - "math/rand" - "strconv" - "testing" - - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/ramrepo" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" - "github.com/stretchr/testify/require" -) - -func TestGetUserIDBySessionKey(t *testing.T) { - log, err := logger.New(logger.RFC3339FormatTime()) - if err != nil { - fmt.Println(err) - return - } - defer log.Sync() - - db, err := ramrepo.OpenDB(strconv.FormatInt(int64(rand.Int()), 10)) - if err != nil { - log.Error(err.Error()) - return - } - defer db.Close() - - sm := New(log, ramrepo.NewRamSessionRepo(db)) - - testCases := []struct { - name string - session_key string - expUserId int - expErr error - }{ - { - "providing valid session key", - "461afabf38b3147c", - 1, - nil, - }, - } - - for _, tCase := range testCases { - t.Run(tCase.name, func(t *testing.T) { - id, err := sm.GetUserIDBySessionKey(context.Background(), tCase.session_key) - require.Equal(t, tCase.expErr, err) - require.Equal(t, tCase.expUserId, id) - }) - } -} diff --git a/internal/usecases/user/credentials.go b/internal/usecases/user/credentials.go deleted file mode 100644 index 9b5646b..0000000 --- a/internal/usecases/user/credentials.go +++ /dev/null @@ -1,10 +0,0 @@ -package user - -type userCredentials struct { - Username string - Password string -} - -func NewCredentials() userCredentials { - return userCredentials{} -} diff --git a/internal/usecases/user/usecase.go b/internal/usecases/user/usecase.go deleted file mode 100644 index b9cd907..0000000 --- a/internal/usecases/user/usecase.go +++ /dev/null @@ -1,59 +0,0 @@ -package user - -import ( - "context" - "errors" - "fmt" - - entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" - repo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/user" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/crypto" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" -) - -var ErrUserAuthentication = errors.New("user authentication") - -const ( - lenSalt = 16 - lenPasswordHash = 64 -) - -type Usecase struct { - log *logger.Logger - repo repo.Repository -} - -func New(log *logger.Logger, repo repo.Repository) *Usecase { - return &Usecase{log, repo} -} - -func (u *Usecase) Register(ctx context.Context, user *entity.User) error { - salt, err := crypto.NewRandomString(lenSalt) - if err != nil { - return fmt.Errorf("generating salt for registration: %w", err) - } - - user.Password = salt + crypto.PasswordHash(user.Password, salt, lenPasswordHash) - err = u.repo.AddNewUser(ctx, user) - if err != nil { - return fmt.Errorf("user registration: %w", err) - } - return nil -} - -func (u *Usecase) Authentication(ctx context.Context, credentials userCredentials) (*entity.User, error) { - user, err := u.repo.GetUserByUsername(ctx, credentials.Username) - if err != nil { - return nil, fmt.Errorf("user authentication: %w", err) - } - salt := user.Password[:lenSalt] - if crypto.PasswordHash(credentials.Password, salt, lenPasswordHash) != user.Password[lenSalt:] { - return nil, ErrUserAuthentication - } - user.Password = "" - return user, nil -} - -func (u *Usecase) FindOutUsernameAndAvatar(ctx context.Context, userID int) (username string, avatar string, err error) { - return u.repo.GetUsernameAndAvatarByID(ctx, userID) -} diff --git a/internal/usecases/user/usecase_test.go b/internal/usecases/user/usecase_test.go deleted file mode 100644 index 9fabfad..0000000 --- a/internal/usecases/user/usecase_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package user - -import ( - "context" - "fmt" - "math/rand" - "strconv" - "testing" - - entity "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" - "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/ramrepo" - "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" - "github.com/stretchr/testify/require" -) - -func TestRegister(t *testing.T) { - log, err := logger.New(logger.RFC3339FormatTime()) - if err != nil { - fmt.Println(err) - return - } - defer log.Sync() - - db, _ := ramrepo.OpenDB(strconv.FormatInt(int64(rand.Int()), 10)) - defer db.Close() - - userCase := New(log, ramrepo.NewRamUserRepo(db)) - - testCases := []struct { - name string - user *entity.User - expErr error - }{ - { - "providing valid user", - &entity.User{ - Username: "valid_user", - Password: "helloworld", - Email: "gggg@yandex.ru", - }, - nil, - }, - } - - for _, tCase := range testCases { - t.Run(tCase.name, func(t *testing.T) { - err := userCase.Register(context.Background(), tCase.user) - require.Equal(t, tCase.expErr, err) - }) - } -} diff --git a/pkg/levenstein/levenstein.go b/pkg/levenstein/levenstein.go new file mode 100644 index 0000000..c03a020 --- /dev/null +++ b/pkg/levenstein/levenstein.go @@ -0,0 +1,37 @@ +package levenstein + +func Levenshtein(s1, s2 []rune) int { + col := make([]int, len(s1)+1) + + for y := 1; y <= len(s1); y++ { + col[y] = y + } + for x := 1; x <= len(s2); x++ { + col[0] = x + lastkey := x - 1 + for y := 1; y <= len(s1); y++ { + oldkey := col[y] + var incr int + if s1[y-1] != s2[x-1] { + incr = 1 + } + + col[y] = min(col[y]+1, col[y-1]+1, lastkey+incr) + lastkey = oldkey + } + } + return col[len(s1)] +} + +func min(a, b, c int) int { + if a < b { + if a < c { + return a + } + } else { + if b < c { + return b + } + } + return c +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 1220cd9..a6d4f1a 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -1,6 +1,7 @@ package logger import ( + "context" "fmt" "go.uber.org/zap" @@ -105,3 +106,10 @@ func (log *Logger) multiLevelSugarLog(logFn zapSugarLogFn, template string, args } logFn(log.Logger.Sugar().With(fieldsSugarLogger...), template, args...) } + +func GetLoggerFromCtx(ctx context.Context) *Logger { + if log, ok := ctx.Value(KeyLogger).(*Logger); ok { + return log + } + return &Logger{} +} diff --git a/pkg/logger/option.go b/pkg/logger/option.go index 96db06c..9639478 100644 --- a/pkg/logger/option.go +++ b/pkg/logger/option.go @@ -18,3 +18,15 @@ func SetFormatTime(layout string) ConfigOption { func RFC3339FormatTime() ConfigOption { return SetFormatTime(time.RFC3339) } + +func SetOutputPaths(files ...string) ConfigOption { + return func(cfg *zap.Config) { + cfg.OutputPaths = files + } +} + +func SetErrorOutputPaths(files ...string) ConfigOption { + return func(cfg *zap.Config) { + cfg.ErrorOutputPaths = files + } +}