From e94b753b7bb4468f4cf47ec6fb0a71636b17345b Mon Sep 17 00:00:00 2001 From: Gvidow <96253031+Gvidow@users.noreply.github.com> Date: Wed, 20 Dec 2023 19:54:23 +0300 Subject: [PATCH] Dev3 (#21) * update API documantation files * delete Empty struct * TP-933 update: results validation func * dev: rename function NewRandomString * dev: update: package crypto * dev add: default avatar for user * dev add: default avatar * dev add: ramrepo -- fillPinTableRows, fillSessionTableRows * TP-6d1 add: choose https/http in config/config.yml * TP-565 add: TestCheckLogin * TP-565 add: TeshLogin * dev del: block else * added dataSourceName as a new OpenDB argument * TP-a49 add: fileserver * TP-565 add,update: added TestSignUp, TestLogout, updated previous tests * TP-a49 add: log fileserver * minor update in test * dev update: passing the config from main and return from main when errors * dev update: moved the const variable higher * dev add: wrapping errors in func OpenDB and prealocation * add targets for testing * added tests for usecases * TP-565 add: error test * dev update: configs, Makefile and handlers auth * TP-aad add: er-diagram.png and docker-compose file * TP-aad add: migrations * TP-aad update: er-diagram.png * TP-aad update: rename users relation to auth * TP-ee7 update: change register router and start server * TP-ee7 update: divided into 3 layers: delivery, usecase, repository * TP-ee7 add: middleware auth * TP-46f add: repository based on posgtres * TP-aad add: description relations and description of functional dependencies * TP-aad update: relations.md * TP-aad update: relations.md * TP-5b0 add: handler upload and change avatar * TP-5b0 update: change upload avatar dir * TP-aad update: merge relations auth and profile in profile * TP-5b0 update: made requests to global variables * TP-5b0 add: get all data in repository * TP-aad update: rename migrations: add prefix 00n_*.sql * TP-5b0 update: get profile info * TP-5b0 update: entity user * TP-5b0 update: handler edit user info * dev2 add: search_path in all migrations * dev2 add: volumes in docker-compose.yml * TP-5b0 add: validation of new user data * TP-e3a add: pin creations * TP-e3a update: reducing the number of database requests when creating a pin * TP-e3a add: likes, delete pin * PT-e3a add: hander for edit pins * TP-aad_db update: move creating triggers after create tables * TP-c80 add: middleware with csrf token verification or installation * TP-c80 update: updated the check for the need to update the csrf token * TP-c80 update: updated the check for the need to update the csrf token * TP-2f3 add: repository with storage in redis * dev2 update: hashing the password when it is changed * dev2 add: method PUT in allowed * TP-add_db update: add ERD-description * dev2 update: cors * TP-fcc_handlers_board update: add board repository, usecase, moved general validation in /pkg, corrected old tests * TP-aad add: attribute amout_me for profile relation * TP-aad add: migrations 003_alter_relations.sql * TP-5b0 add: attribute AboutMe * TP-e3a update: select pins from tape * TP-e3a update: select pin for tape * TP-e3a add: chek avalability pins * TP-e3a add: check action fix pin on board * TP-e3a add: user info when get pin * TP-ffc_handlers_board update: add response codes/messages * TP-ffc_handlers_board update: add repo.go with general info about repository layer * TP-ffc_handlers_board update: add CreateBoard, GetUserBoards, GetCertainBoard handlers * TP-ffc_handlers_board update: add access for contributors in GetUserBoards handler * TP-e3a add: getting user pins * TP-ffc_handlers_board update: add UpdateBoard, DeleteBoard handlers, fixed previous changes * TP-a9b add: pkg/validator * TP-ffc_handlers_board update: fixed GetBoardsByUserID, GetBoardByID in board repository * dev2 add: returning count like when request on set like * TP-113 add: logger * TP-113 add: logger in context * TP-113 add: get logger in handlers * dev2 add: check set like to pin from user * TP-113 add: logger with formater * TP-errors update: usecase image * TP-erros update: both sides fall into range * TP-f21 update: responseOk * TP-f21 update: rename BoardUsecase to boardUsecase * TP-f21 update: repository board * TP-f21 update: logger in delivery board * TP-f21 add: fix pins on board * TP-28e add: mock for usecases and repositories * TP-28e_mockTests update: add tests on CreateNewBoard, UpdateBoardInfo * TP-2e4 update: old test * TP-28e_mockTests update: add GetBoardsByUsername test * TP-28e_mockTests update: modified test_with_coverage target * TP-28e_mockTests update: add GetCertainBoard, GetBoardByUsername tests * TP-28e_mockTests update: add DeleteCertainBoard test * TP-28e_mockTests update: moved return value checking into if clause * TP-28e_mockTests update: moved logger initialization to the outer scope * TP-28e_mockTests: replaced log.Fatal with t.Fatalf * TP-2e4 add testing cover * TP-28e_mockTests update: delete makefile target, delete old pin usecase_test * TP-28e_mockTests update: update makefile * dev2 update: deleted generated mock duplicates * TP-2e4 add: usecase test session and image * dev2 update: not view deleted pin in tape * dev2 add: test for usecase pin * dev2 add: returning count like with delete * dev2 del: status code 204 on path /api/v1/csrf * dev2 update: select pin users * dev2 update: fail test * TP-2e4 add: cover usecase/pin * TP-ffc_handlers_board_fix update: add types.go with common content types, defined general errors, add error-code mapping * TP-ffc_handlers_board_fix update: fixed tags assignment, add GetBoardInfoForUpdade method * TP-ffc_handlers_board_fix update: moved validation to the delivery layer, add Sanitize() method for entity, usecase structures, add GetBoardInfoForUpdate() method, regenerate mocks * TP-ffc_handlers_board_fix update: regenerate mocks * TP-ffc_handlers_board_fix update: add board validation, additional errors, err-code mapping,GetBoardInfoForUpdate() on the delivery layer * dev 2 update: minor board repo fix * TP-db2 update: test version of working with the tape * TP-db2 add: return user id on request login * TP-db2 update: feed condition change * dev2 update: fixed error display * dev2 update: add field in GetCertainBoard response * TP-db2 update: feed pins * TP-db2: update repository add filters * dev2 update: check valid photo for pin * TP-619 add: config for redis and .env file for postgres * TP-619 add: pkg config for parse *.conf files * TP-619 add: connect with config * TP-619 update: check author for edit pin * dev2 update: add insertBoard, CreateBoard, GetBoardByUserID tests * TP-fcc_board_dataflow_change: removed dto from usecase, changed entity structure, add structures on the delivery layer * dev2 update: fixed pins view on board(s) * dev2 update: changed null value processing in board repo methods * dev 2 update: fixed double delete on board * TP-d07 update: rename func to dirToSave * update: use PrefixURLImage * dev2 update: changed permissions on uploaded images, added :8081 in AllowedOrigins * TP-f07 add: entity Message * TP-f07 add write layers: delivery, usecase, repository for message * TP-f07 update sql queries * TP-f07 add: fake communication via websocket * TP-f07 add: origin patterns * TP-f07 add: implementation http.Flusher * TP-500 update: add global errors package with general application error types, errors * TP-500 update: add function to get logger from ctx * TP-500 update: add middleware with request timeout assignment * TP-500 update: add general rest api errors on the delivery layer, add getCodeStatusHttp for errors, responseErr for err convertion and response assignment * TP-500 update: add user subscription handlers, converter from postgresql error to application error in user and subscription repos * TP-409 add: description real time server as .proto * TP-500_subscriptions update: add handlers for getting user/profile info with subscriptions, made some optimizations * TP-500_subscriptions: add id field in profile header * TP-409 add: skeleton real time server * TP-409 add: partitions package internal/microservices/realtime * TP-409 update: websocket handler * TP-409 update func new websocket * TP-409 update: entity message json tag * dev3: changed pagination for subscriptions, minor errors update * dev3 minor: removed default case * TP-409 add: get feed user chats with other users * microservice auth * TP-34d_search: add user, board, pin search with pagination and sort options * TP-34d_search: changed pin likes aggregation * TP-4b9 update: auth * TP-0a2 add: metrics * TP-34d_search: add DeletePinFromBoard handler * dev3 minor: deleted redundant query.go from search repo * TP-0a2 update: move api/auth, api/realtime to internal/api/auth, internal/api/realtime * dev3 del defer close grpc connect * dev3 del: scheme https for prometheus target pinspire.online:9100 * dev3 update: change option for proto files * TP-cc2 add: proto description service messenger * TP-cc2 add: microservice messenger * Tp cc2 ms message (#19) * TP-cc2 update: ms messenger * TP-cc2 update: rename key metadata for auth * TP-cc2 add: metrics * dev3: add author_username field in GetCertainBoard * dev3 update: address for metrics servers * dev3 add: eventType in message response * TP-c01_ci-cd: add Dockerfiles for nginx and microservices, add service deps,healthchecks * TP-c01_ci-cd: changed prometheus targets, servers addresses from localhost: to :, replaced string servers params with env variables * TP-3a4 add: output files flag for logging * dev3 add: generate mocks * TP-c01_ci-cd: add linter configuration * TP-c01_ci-cd: add named volumes for postgres and redis, add compose.prod.yml for pulling images while deploying * TP-c01_ci-cd: add workflows description, add configuration file for ansible, add inventory in .gitignore * TP-c01_ci-cd: add kafka healthcheck * TP-c01_ci-cd: deleted nginx config from compose.yml, changed triggers, changed ports * TP-c01_ci-cd: deleted dev3 from triggers * TP-c01_ci-cd: changed triggers, add step * TP-59b add: comments * TP-6ec_easyjson: generated easyjson, changed serialization for user, board, search, subscription * TP-1c3 add: notification ws handler * TP-1c3 update: the subscription is carried out on the backend * TP-1c3 update: work with realtime for chat * TP-1c3 update: proto scheme for a reltime microservice * TP-1c3 update: chat * TP-1c3 update: rename request publish struct * TP-1c3 update: regenerate * TP-1c3 add: notification after commenting on the pin * TP-1c3 add: easyjson generation * TP-8da update: returned user from and to when deleting message * Update websocket.go: delete unused package "os" * TP-6ec_easyjson: add rest easyjson * dev4: removed pull request trigger for deploy workflow * dev4: replaced auth service host:port string with env variables, moved auth config to main.go * TP-87a_filtration: add filtration of pin content * TP-87a_filtration: add task with cloud api token provision, add '2023_2_OND_team' folder to dest * dev4: replaced google vision client with image filter interface * de4: changed deployment branch, add cloud api token to the .env file * dev4: add images volume for main service container * dev4: changed makefile variable, included all branches in CI --------- Co-authored-by: wonderf00l Co-authored-by: wonderf00l <105116952+wonderf00l@users.noreply.github.com> --- .github/workflows/ci.yml | 31 + .github/workflows/deployment.yml | 58 ++ .gitignore | 3 + Makefile | 17 +- api/proto/realtime.proto | 13 +- cmd/app/config.go | 8 - cmd/app/main.go | 20 +- cmd/auth/config.go | 2 +- cmd/realtime/main.go | 4 +- configs/.golangci.yml | 206 ++++++ configs/config.yml | 2 +- configs/playbook.yml | 18 + configs/prometheus.yml | 10 +- deployments/Dockerfile.auth | 20 + deployments/Dockerfile.main | 21 + deployments/Dockerfile.messenger | 20 + deployments/Dockerfile.realtime | 20 + deployments/compose.prod.yml | 14 + deployments/docker-compose.yml | 108 +++- go.mod | 33 +- go.sum | 142 +++- internal/api/realtime/realtime.pb.go | 297 ++++++--- internal/api/realtime/realtime_grpc.pb.go | 10 +- internal/api/server/router/router.go | 12 +- internal/app/app.go | 69 +- internal/app/auth/auth.go | 10 +- internal/app/config.go | 8 +- internal/app/messenger/messenger.go | 4 +- internal/app/redis_conn.go | 2 +- .../usecase/message/mock/message_mock.go | 31 + internal/microservices/realtime/node.go | 5 +- internal/microservices/realtime/server.go | 6 +- internal/pkg/delivery/http/v1/auth.go | 8 +- internal/pkg/delivery/http/v1/auth_test.go | 71 +- internal/pkg/delivery/http/v1/board.go | 167 ++--- .../pkg/delivery/http/v1/board_validation.go | 48 -- internal/pkg/delivery/http/v1/chat.go | 2 +- internal/pkg/delivery/http/v1/comment.go | 118 ++++ .../v1/{board_errors.go => errors/board.go} | 12 +- .../pkg/delivery/http/v1/errors/general.go | 111 ++++ .../pkg/delivery/http/v1/errors/search.go | 23 + internal/pkg/delivery/http/v1/feed.go | 4 +- internal/pkg/delivery/http/v1/handler.go | 4 + internal/pkg/delivery/http/v1/pin.go | 14 +- internal/pkg/delivery/http/v1/pin_test.go | 17 +- internal/pkg/delivery/http/v1/profile.go | 34 +- internal/pkg/delivery/http/v1/profile_test.go | 5 +- internal/pkg/delivery/http/v1/response.go | 134 +--- internal/pkg/delivery/http/v1/search.go | 5 +- .../pkg/delivery/http/v1/structs/board.go | 114 ++++ .../http/v1/structs/board_easyjson.go | 605 ++++++++++++++++++ .../pkg/delivery/http/v1/structs/response.go | 17 + .../http/v1/structs/response_easyjson.go | 191 ++++++ .../delivery/http/v1/structs/subscription.go | 17 + .../http/v1/structs/subscription_easyjson.go | 97 +++ internal/pkg/delivery/http/v1/structs/user.go | 23 + .../delivery/http/v1/structs/user_easyjson.go | 221 +++++++ internal/pkg/delivery/http/v1/subscription.go | 35 +- internal/pkg/delivery/websocket/chat.go | 114 ++++ .../pkg/delivery/websocket/notification.go | 54 ++ internal/pkg/delivery/websocket/socket.go | 37 ++ internal/pkg/delivery/websocket/types.go | 19 +- .../pkg/delivery/websocket/types_easyjson.go | 451 +++++++++++++ internal/pkg/delivery/websocket/websocket.go | 196 +----- internal/pkg/entity/board/board.go | 6 +- internal/pkg/entity/board/board_easyjson.go | 293 +++++++++ internal/pkg/entity/comment/comment.go | 16 + .../pkg/entity/comment/comment_easyjson.go | 121 ++++ internal/pkg/entity/message/message.go | 5 +- .../pkg/entity/message/message_easyjson.go | 114 ++++ internal/pkg/entity/notification/message.go | 26 + .../entity/notification/message_easyjson.go | 92 +++ .../pkg/entity/notification/notification.go | 86 +++ internal/pkg/entity/notification/template.go | 5 + internal/pkg/entity/notification/type.go | 20 + internal/pkg/entity/search/search.go | 5 + internal/pkg/entity/search/search_easyjson.go | 312 +++++++++ internal/pkg/entity/user/user.go | 6 + internal/pkg/entity/user/user_easyjson.go | 233 +++++++ internal/pkg/notification/comment/comment.go | 57 ++ internal/pkg/notification/notifier.go | 27 + .../pkg/repository/board/mock/board_mock.go | 21 +- .../pkg/repository/board/postgres/repo.go | 16 +- internal/pkg/repository/board/repo.go | 2 +- .../repository/comment/mock/comment_mock.go | 95 +++ internal/pkg/repository/comment/queries.go | 19 + internal/pkg/repository/comment/repo.go | 86 +++ .../repository/message/mock/message_mock.go | 15 + .../pkg/repository/search/mock/search_mock.go | 81 +++ internal/pkg/repository/search/repo.go | 1 + .../subscription/mock/subscription_mock.go | 94 +++ internal/pkg/repository/subscription/repo.go | 1 + internal/pkg/usecase/auth/mock/auth_mock.go | 95 +++ internal/pkg/usecase/auth/usecase.go | 1 + internal/pkg/usecase/board/get.go | 17 +- internal/pkg/usecase/board/mock/board_mock.go | 21 +- internal/pkg/usecase/board/usecase.go | 2 +- internal/pkg/usecase/board/usecase_test.go | 16 +- internal/pkg/usecase/comment/check.go | 29 + .../pkg/usecase/comment/mock/comment_mock.go | 148 +++++ internal/pkg/usecase/comment/usecase.go | 110 ++++ internal/pkg/usecase/image/filtration.go | 93 +++ internal/pkg/usecase/image/mock/image_mock.go | 9 +- internal/pkg/usecase/image/usecase.go | 29 +- .../pkg/usecase/message/mock/message_mock.go | 71 +- internal/pkg/usecase/message/usecase.go | 100 ++- internal/pkg/usecase/pin/check.go | 11 +- internal/pkg/usecase/pin/update.go | 11 +- internal/pkg/usecase/pin/update_easyjson.go | 174 +++++ internal/pkg/usecase/pin/usecase.go | 25 +- internal/pkg/usecase/realtime/chat/chat.go | 114 ++++ .../usecase/realtime/notification/comment.go | 36 ++ .../realtime/notification/notification.go | 101 +++ .../usecase/realtime/notification/option.go | 19 + internal/pkg/usecase/realtime/realtime.go | 109 ++++ .../pkg/usecase/search/mock/search_mock.go | 81 +++ internal/pkg/usecase/search/usecase.go | 1 + .../subscription/mock/subscription_mock.go | 79 +++ internal/pkg/usecase/subscription/usecase.go | 1 + internal/pkg/usecase/user/credentials.go | 7 +- .../pkg/usecase/user/credentials_easyjson.go | 92 +++ internal/pkg/usecase/user/info.go | 13 +- internal/pkg/usecase/user/info_easyjson.go | 192 ++++++ internal/pkg/usecase/user/profile.go | 2 +- pkg/logger/option.go | 12 + 125 files changed, 7019 insertions(+), 849 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deployment.yml delete mode 100644 cmd/app/config.go create mode 100644 configs/.golangci.yml create mode 100644 configs/playbook.yml create mode 100644 deployments/Dockerfile.auth create mode 100644 deployments/Dockerfile.main create mode 100644 deployments/Dockerfile.messenger create mode 100644 deployments/Dockerfile.realtime create mode 100644 deployments/compose.prod.yml delete mode 100644 internal/pkg/delivery/http/v1/board_validation.go create mode 100644 internal/pkg/delivery/http/v1/comment.go rename internal/pkg/delivery/http/v1/{board_errors.go => errors/board.go} (83%) create mode 100644 internal/pkg/delivery/http/v1/errors/general.go create mode 100644 internal/pkg/delivery/http/v1/errors/search.go create mode 100644 internal/pkg/delivery/http/v1/structs/board.go create mode 100644 internal/pkg/delivery/http/v1/structs/board_easyjson.go create mode 100644 internal/pkg/delivery/http/v1/structs/response.go create mode 100644 internal/pkg/delivery/http/v1/structs/response_easyjson.go create mode 100644 internal/pkg/delivery/http/v1/structs/subscription.go create mode 100644 internal/pkg/delivery/http/v1/structs/subscription_easyjson.go create mode 100644 internal/pkg/delivery/http/v1/structs/user.go create mode 100644 internal/pkg/delivery/http/v1/structs/user_easyjson.go create mode 100644 internal/pkg/delivery/websocket/chat.go create mode 100644 internal/pkg/delivery/websocket/notification.go create mode 100644 internal/pkg/delivery/websocket/socket.go create mode 100644 internal/pkg/delivery/websocket/types_easyjson.go create mode 100644 internal/pkg/entity/board/board_easyjson.go create mode 100644 internal/pkg/entity/comment/comment.go create mode 100644 internal/pkg/entity/comment/comment_easyjson.go create mode 100644 internal/pkg/entity/message/message_easyjson.go create mode 100644 internal/pkg/entity/notification/message.go create mode 100644 internal/pkg/entity/notification/message_easyjson.go create mode 100644 internal/pkg/entity/notification/notification.go create mode 100644 internal/pkg/entity/notification/template.go create mode 100644 internal/pkg/entity/notification/type.go create mode 100644 internal/pkg/entity/search/search_easyjson.go create mode 100644 internal/pkg/entity/user/user_easyjson.go create mode 100644 internal/pkg/notification/comment/comment.go create mode 100644 internal/pkg/notification/notifier.go create mode 100644 internal/pkg/repository/comment/mock/comment_mock.go create mode 100644 internal/pkg/repository/comment/queries.go create mode 100644 internal/pkg/repository/comment/repo.go create mode 100644 internal/pkg/repository/search/mock/search_mock.go create mode 100644 internal/pkg/repository/subscription/mock/subscription_mock.go create mode 100644 internal/pkg/usecase/auth/mock/auth_mock.go create mode 100644 internal/pkg/usecase/comment/check.go create mode 100644 internal/pkg/usecase/comment/mock/comment_mock.go create mode 100644 internal/pkg/usecase/comment/usecase.go create mode 100644 internal/pkg/usecase/image/filtration.go create mode 100644 internal/pkg/usecase/pin/update_easyjson.go create mode 100644 internal/pkg/usecase/realtime/chat/chat.go create mode 100644 internal/pkg/usecase/realtime/notification/comment.go create mode 100644 internal/pkg/usecase/realtime/notification/notification.go create mode 100644 internal/pkg/usecase/realtime/notification/option.go create mode 100644 internal/pkg/usecase/realtime/realtime.go create mode 100644 internal/pkg/usecase/search/mock/search_mock.go create mode 100644 internal/pkg/usecase/subscription/mock/subscription_mock.go create mode 100644 internal/pkg/usecase/user/credentials_easyjson.go create mode 100644 internal/pkg/usecase/user/info_easyjson.go 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..97709e8 --- /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.online + 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.online + 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 + \ No newline at end of file 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 7b0652c..f054d18 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ .PHONY: build run test test_with_coverage cleantest retest doc generate cover_all currcover -.PHONY: build_auth build_realtime build_messenger +.PHONY: build_auth build_realtime build_messenger build_all +.PHONY: .install-linter lint lint-fast ENTRYPOINT=cmd/app/main.go DOC_DIR=./docs @@ -7,6 +8,10 @@ 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 @@ -19,6 +24,8 @@ build_realtime: build_messenger: go build -o bin/messenger cmd/messenger/*.go +build_all: build build_auth build_realtime build_messenger + run: build ./bin/app @@ -51,3 +58,11 @@ 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/realtime.proto b/api/proto/realtime.proto index d04bc35..3fca899 100644 --- a/api/proto/realtime.proto +++ b/api/proto/realtime.proto @@ -8,7 +8,11 @@ package realtime; service RealTime { rpc Publish(PublishMessage) returns (google.protobuf.Empty) {} - rpc Subscribe(Channel) returns (stream Message) {} + rpc Subscribe(Channels) returns (stream Message) {} +} + +message Channels { + repeated Channel chans = 1; } message Channel { @@ -27,10 +31,15 @@ message EventObject { EventType type = 2; } +message EventMap { + int64 type = 1; + map m = 2; +} + message Message { oneof body { EventObject object = 1; - string content = 2; + EventMap content = 2; } } diff --git a/cmd/app/config.go b/cmd/app/config.go deleted file mode 100644 index 0cb9a84..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", - AddrAuthServer: "localhost:8085", -} 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 index bf1142d..2ffd53a 100644 --- a/cmd/auth/config.go +++ b/cmd/auth/config.go @@ -3,6 +3,6 @@ package main import "github.com/go-park-mail-ru/2023_2_OND_team/internal/app/auth" var configAuth = auth.Config{ - Addr: "localhost:8085", + Addr: "0.0.0.0:8085", RedisFileConfig: "redis.conf", } diff --git a/cmd/realtime/main.go b/cmd/realtime/main.go index 84c0ecb..636a3b6 100644 --- a/cmd/realtime/main.go +++ b/cmd/realtime/main.go @@ -4,6 +4,7 @@ import ( "fmt" "net" + "github.com/joho/godotenv" "google.golang.org/grpc" rt "github.com/go-park-mail-ru/2023_2_OND_team/internal/api/realtime" @@ -16,6 +17,7 @@ import ( const _address = "localhost:8090" func main() { + godotenv.Load() log, err := logger.New() if err != nil { fmt.Println(err) @@ -44,7 +46,7 @@ func RealTimeRun(log *logger.Logger, addr string) error { } serv := grpc.NewServer(grpc.ChainUnaryInterceptor( - interceptor.Monitoring(metrics, "localhost:8091"), + interceptor.Monitoring(metrics, "0.0.0.0:8091"), interceptor.Logger(log), )) rt.RegisterRealTimeServer(serv, realtime.NewServer(node)) diff --git a/configs/.golangci.yml b/configs/.golangci.yml new file mode 100644 index 0000000..cac2d04 --- /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 88ea0c1..99e2da9 100644 --- a/configs/config.yml +++ b/configs/config.yml @@ -2,6 +2,6 @@ app: server: host: 127.0.0.1 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 index 2fcfe33..a65d7b8 100644 --- a/configs/prometheus.yml +++ b/configs/prometheus.yml @@ -5,23 +5,23 @@ global: scrape_configs: - job_name: 'api' static_configs: - - targets: ['host.docker.internal:8080'] + - targets: ['main_service:8079'] - job_name: 'auth' static_configs: - - targets: ['host.docker.internal:8086'] + - targets: ['auth_service:8086'] - job_name: 'messenger' static_configs: - - targets: ['host.docker.internal:8096'] + - targets: ['messenger_service:8096'] - job_name: 'realtime' static_configs: - - targets: ['host.docker.internal:8091'] + - targets: ['realtime_service:8091'] - job_name: 'node_exporter' static_configs: - - targets: ['pinspire.online:9100'] + - targets: ['node_exporter:9100'] - job_name: 'pinspire' scheme: https 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 8ee69d6..74908d2 100644 --- a/deployments/docker-compose.yml +++ b/deployments/docker-compose.yml @@ -8,17 +8,110 @@ 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 + depends_on: + kafka: + condition: 'service_healthy' + ports: + - 8191:8091 + # - 8103:8090 zookeeper: image: bitnami/zookeeper:latest @@ -42,6 +135,14 @@ services: - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092 - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 + healthcheck: + test: | + curl localhost:9092 + [ $(echo $?) = '52' ] && exit 0 || exit -1 + interval: 5s + timeout: 5s + retries: 10 + start_period: 15s depends_on: - zookeeper @@ -56,6 +157,8 @@ services: grafana: image: grafana/grafana:latest container_name: pinspireGrafana + env_file: + - ../.env ports: - 3000:3000 volumes: @@ -76,8 +179,9 @@ services: ports: - "9100:9100" - volumes: + postgres_storage: {} + redis_storage: {} zookeeper_data: driver: local kafka_data: diff --git a/go.mod b/go.mod index f8b2c99..ea3acaa 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,17 @@ module github.com/go-park-mail-ru/2023_2_OND_team go 1.19 require ( + 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/golang/protobuf v1.5.3 - github.com/google/uuid v1.3.1 + github.com/google/uuid v1.4.0 github.com/jackc/pgx/v5 v5.4.3 github.com/joho/godotenv v1.5.1 - github.com/labstack/echo/v4 v4.11.3 + 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 @@ -27,12 +28,17 @@ 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.59.0 + google.golang.org/api v0.149.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/aymerick/douceur v0.2.0 // indirect @@ -52,9 +58,12 @@ require ( github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/go-text/typesetting v0.0.0-20231013144250-6cc35dbfae7d // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // 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 @@ -69,12 +78,8 @@ require ( 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/labstack/gommon v0.4.0 // 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/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // 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 @@ -86,17 +91,19 @@ require ( 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 - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // 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/time v0.3.0 // indirect golang.org/x/tools v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // 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 155b082..d50cb55 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,14 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +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/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/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/vision/v2 v2.7.5 h1:T/ujUghvEaTb+YnFY/jiYwVAkMbIC8EieK0CJo6B4vg= +cloud.google.com/go/vision/v2 v2.7.5/go.mod h1:GcviprJLFfK9OLf0z8Gm6lQb6ZFUulvpZws+mm6yPLM= git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v/9acCWA64Yz/HDLqjcRTvc0noQACi4MsJw= @@ -24,8 +35,11 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r 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/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/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= @@ -42,6 +56,10 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4A 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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= @@ -70,22 +88,48 @@ github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= github.com/go-text/typesetting v0.0.0-20231013144250-6cc35dbfae7d h1:HrdwTlHVMdi9nOW7ZnYiLmIT1hJHvipIwM0aX3rKn8I= github.com/go-text/typesetting v0.0.0-20231013144250-6cc35dbfae7d/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/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-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.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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +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.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/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.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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.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= @@ -133,10 +177,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= -github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -147,13 +187,6 @@ 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/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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= @@ -168,6 +201,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI 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= @@ -213,13 +247,10 @@ 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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 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.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 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= @@ -233,25 +264,34 @@ 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-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/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-20190313153728-d0100b6bd8b3/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-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/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/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-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-20190620200207-3b0461eec859/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-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -261,26 +301,28 @@ 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.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-20190423024810-112230192c58/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-20190412213103-97732733099d/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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.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= @@ -291,13 +333,15 @@ 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.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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -313,10 +357,37 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T 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/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +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.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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +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.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +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/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.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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= @@ -333,9 +404,10 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/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= nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/internal/api/realtime/realtime.pb.go b/internal/api/realtime/realtime.pb.go index 8e17fe5..3b3ed8f 100644 --- a/internal/api/realtime/realtime.pb.go +++ b/internal/api/realtime/realtime.pb.go @@ -70,6 +70,53 @@ 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 @@ -82,7 +129,7 @@ type Channel struct { func (x *Channel) Reset() { *x = Channel{} if protoimpl.UnsafeEnabled { - mi := &file_api_proto_realtime_proto_msgTypes[0] + mi := &file_api_proto_realtime_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -95,7 +142,7 @@ func (x *Channel) String() string { func (*Channel) ProtoMessage() {} func (x *Channel) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_realtime_proto_msgTypes[0] + mi := &file_api_proto_realtime_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -108,7 +155,7 @@ func (x *Channel) ProtoReflect() protoreflect.Message { // Deprecated: Use Channel.ProtoReflect.Descriptor instead. func (*Channel) Descriptor() ([]byte, []int) { - return file_api_proto_realtime_proto_rawDescGZIP(), []int{0} + return file_api_proto_realtime_proto_rawDescGZIP(), []int{1} } func (x *Channel) GetTopic() string { @@ -137,7 +184,7 @@ type EventObject struct { func (x *EventObject) Reset() { *x = EventObject{} if protoimpl.UnsafeEnabled { - mi := &file_api_proto_realtime_proto_msgTypes[1] + mi := &file_api_proto_realtime_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -150,7 +197,7 @@ func (x *EventObject) String() string { func (*EventObject) ProtoMessage() {} func (x *EventObject) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_realtime_proto_msgTypes[1] + mi := &file_api_proto_realtime_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -163,7 +210,7 @@ func (x *EventObject) ProtoReflect() protoreflect.Message { // Deprecated: Use EventObject.ProtoReflect.Descriptor instead. func (*EventObject) Descriptor() ([]byte, []int) { - return file_api_proto_realtime_proto_rawDescGZIP(), []int{1} + return file_api_proto_realtime_proto_rawDescGZIP(), []int{2} } func (x *EventObject) GetId() int64 { @@ -180,6 +227,61 @@ func (x *EventObject) GetType() EventType { 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 @@ -195,7 +297,7 @@ type Message struct { func (x *Message) Reset() { *x = Message{} if protoimpl.UnsafeEnabled { - mi := &file_api_proto_realtime_proto_msgTypes[2] + mi := &file_api_proto_realtime_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -208,7 +310,7 @@ func (x *Message) String() string { func (*Message) ProtoMessage() {} func (x *Message) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_realtime_proto_msgTypes[2] + mi := &file_api_proto_realtime_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -221,7 +323,7 @@ func (x *Message) ProtoReflect() protoreflect.Message { // Deprecated: Use Message.ProtoReflect.Descriptor instead. func (*Message) Descriptor() ([]byte, []int) { - return file_api_proto_realtime_proto_rawDescGZIP(), []int{2} + return file_api_proto_realtime_proto_rawDescGZIP(), []int{4} } func (m *Message) GetBody() isMessage_Body { @@ -238,11 +340,11 @@ func (x *Message) GetObject() *EventObject { return nil } -func (x *Message) GetContent() string { +func (x *Message) GetContent() *EventMap { if x, ok := x.GetBody().(*Message_Content); ok { return x.Content } - return "" + return nil } type isMessage_Body interface { @@ -254,7 +356,7 @@ type Message_Object struct { } type Message_Content struct { - Content string `protobuf:"bytes,2,opt,name=content,proto3,oneof"` + Content *EventMap `protobuf:"bytes,2,opt,name=content,proto3,oneof"` } func (*Message_Object) isMessage_Body() {} @@ -273,7 +375,7 @@ type PublishMessage struct { func (x *PublishMessage) Reset() { *x = PublishMessage{} if protoimpl.UnsafeEnabled { - mi := &file_api_proto_realtime_proto_msgTypes[3] + mi := &file_api_proto_realtime_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -286,7 +388,7 @@ func (x *PublishMessage) String() string { func (*PublishMessage) ProtoMessage() {} func (x *PublishMessage) ProtoReflect() protoreflect.Message { - mi := &file_api_proto_realtime_proto_msgTypes[3] + mi := &file_api_proto_realtime_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -299,7 +401,7 @@ func (x *PublishMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use PublishMessage.ProtoReflect.Descriptor instead. func (*PublishMessage) Descriptor() ([]byte, []int) { - return file_api_proto_realtime_proto_rawDescGZIP(), []int{3} + return file_api_proto_realtime_proto_rawDescGZIP(), []int{5} } func (x *PublishMessage) GetChannel() *Channel { @@ -323,43 +425,56 @@ var file_api_proto_realtime_proto_rawDesc = []byte{ 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, 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, 0x5e, - 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, 0x1a, 0x0a, 0x07, 0x63, 0x6f, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 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, 0x80, 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, 0x35, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x11, 0x2e, - 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x1a, 0x11, 0x2e, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, 0x39, 0x5a, 0x37, 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, 0x72, 0x65, 0x61, 0x6c, 0x74, 0x69, - 0x6d, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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 ( @@ -375,29 +490,35 @@ func file_api_proto_realtime_proto_rawDescGZIP() []byte { } var file_api_proto_realtime_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_api_proto_realtime_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_api_proto_realtime_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_api_proto_realtime_proto_goTypes = []interface{}{ (EventType)(0), // 0: realtime.EventType - (*Channel)(nil), // 1: realtime.Channel - (*EventObject)(nil), // 2: realtime.EventObject - (*Message)(nil), // 3: realtime.Message - (*PublishMessage)(nil), // 4: realtime.PublishMessage - (*empty.Empty)(nil), // 5: google.protobuf.Empty + (*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{ - 0, // 0: realtime.EventObject.type:type_name -> realtime.EventType - 2, // 1: realtime.Message.object:type_name -> realtime.EventObject - 1, // 2: realtime.PublishMessage.channel:type_name -> realtime.Channel - 3, // 3: realtime.PublishMessage.message:type_name -> realtime.Message - 4, // 4: realtime.RealTime.Publish:input_type -> realtime.PublishMessage - 1, // 5: realtime.RealTime.Subscribe:input_type -> realtime.Channel - 5, // 6: realtime.RealTime.Publish:output_type -> google.protobuf.Empty - 3, // 7: realtime.RealTime.Subscribe:output_type -> realtime.Message - 6, // [6:8] is the sub-list for method output_type - 4, // [4:6] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 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() } @@ -407,7 +528,7 @@ func file_api_proto_realtime_proto_init() { } if !protoimpl.UnsafeEnabled { file_api_proto_realtime_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Channel); i { + switch v := v.(*Channels); i { case 0: return &v.state case 1: @@ -419,7 +540,7 @@ func file_api_proto_realtime_proto_init() { } } file_api_proto_realtime_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EventObject); i { + switch v := v.(*Channel); i { case 0: return &v.state case 1: @@ -431,7 +552,7 @@ func file_api_proto_realtime_proto_init() { } } file_api_proto_realtime_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Message); i { + switch v := v.(*EventObject); i { case 0: return &v.state case 1: @@ -443,6 +564,30 @@ func file_api_proto_realtime_proto_init() { } } 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 @@ -455,7 +600,7 @@ func file_api_proto_realtime_proto_init() { } } } - file_api_proto_realtime_proto_msgTypes[2].OneofWrappers = []interface{}{ + file_api_proto_realtime_proto_msgTypes[4].OneofWrappers = []interface{}{ (*Message_Object)(nil), (*Message_Content)(nil), } @@ -465,7 +610,7 @@ func file_api_proto_realtime_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_api_proto_realtime_proto_rawDesc, NumEnums: 1, - NumMessages: 4, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/api/realtime/realtime_grpc.pb.go b/internal/api/realtime/realtime_grpc.pb.go index 759e3ff..420b61e 100644 --- a/internal/api/realtime/realtime_grpc.pb.go +++ b/internal/api/realtime/realtime_grpc.pb.go @@ -24,7 +24,7 @@ const _ = grpc.SupportPackageIsVersion7 // 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 *Channel, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) + Subscribe(ctx context.Context, in *Channels, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) } type realTimeClient struct { @@ -44,7 +44,7 @@ func (c *realTimeClient) Publish(ctx context.Context, in *PublishMessage, opts . return out, nil } -func (c *realTimeClient) Subscribe(ctx context.Context, in *Channel, opts ...grpc.CallOption) (RealTime_SubscribeClient, error) { +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 @@ -81,7 +81,7 @@ func (x *realTimeSubscribeClient) Recv() (*Message, error) { // for forward compatibility type RealTimeServer interface { Publish(context.Context, *PublishMessage) (*empty.Empty, error) - Subscribe(*Channel, RealTime_SubscribeServer) error + Subscribe(*Channels, RealTime_SubscribeServer) error mustEmbedUnimplementedRealTimeServer() } @@ -92,7 +92,7 @@ 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(*Channel, RealTime_SubscribeServer) error { +func (UnimplementedRealTimeServer) Subscribe(*Channels, RealTime_SubscribeServer) error { return status.Errorf(codes.Unimplemented, "method Subscribe not implemented") } func (UnimplementedRealTimeServer) mustEmbedUnimplementedRealTimeServer() {} @@ -127,7 +127,7 @@ func _RealTime_Publish_Handler(srv interface{}, ctx context.Context, dec func(in } func _RealTime_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(Channel) + m := new(Channels) if err := stream.RecvMsg(m); err != nil { return err } diff --git a/internal/api/server/router/router.go b/internal/api/server/router/router.go index 66b6f54..434ed62 100644 --- a/internal/api/server/router/router.go +++ b/internal/api/server/router/router.go @@ -101,6 +101,15 @@ func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, wsHandler *deli 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) { @@ -132,6 +141,7 @@ func (r Router) RegisterRoute(handler *deliveryHTTP.HandlerHTTP, wsHandler *deli }) r.Mux.With(auth.RequireAuth).Route("/websocket/connect", func(r chi.Router) { - r.Get("/chat", wsHandler.WebSocketConnect) + r.Get("/chat", wsHandler.Chat) + r.Get("/notification", wsHandler.Notification) }) } diff --git a/internal/app/app.go b/internal/app/app.go index 0437156..82b70ff 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,21 +2,29 @@ package app import ( "context" + "encoding/base64" + "fmt" + "os" "time" - "github.com/joho/godotenv" "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" searchRepo "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/repository/search/postgres" @@ -24,22 +32,27 @@ import ( 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/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" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) -var _timeoutForConnPG = 5 * time.Second +var ( + _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 { @@ -57,15 +70,52 @@ func Run(ctx context.Context, log *log.Logger, cfg ConfigFiles) { } defer pool.Close() - connMessMS, err := grpc.Dial("localhost:8095", grpc.WithTransportCredentials(insecure.NewCredentials())) + 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() - imgCase := image.New(log, imgRepo.NewImageRepoFS(uploadFiles)) - messageCase := message.New(messenger.NewMessengerClient(connMessMS)) + 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() + + 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")) + fmt.Println(string(token)) + if err != nil { + log.Error(err.Error()) + return + } + + visionClient, err := vision.NewImageAnnotatorClient(visionCtx, option.WithCredentialsJSON(token)) + if err != nil { + log.Error(err.Error()) + return + } + + imgCase := image.New(log, imgRepo.NewImageRepoFS(uploadFiles), image.NewFilter(visionClient)) + messageCase := message.New(log, messenger.NewMessengerClient(connMessMS), chat.New(realtime.NewRealTimeChatClient(rtClient), log)) + pinCase := pin.New(log, imgCase, pinRepo.NewPinRepoPG(pool)) + + 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 { @@ -78,14 +128,15 @@ func Run(ctx context.Context, log *log.Logger, cfg ConfigFiles) { handler := deliveryHTTP.New(log, deliveryHTTP.UsecaseHub{ AuhtCase: ac, UserCase: user.New(log, imgCase, userRepo.NewUserRepoPG(pool)), - PinCase: pin.New(log, imgCase, pinRepo.NewPinRepoPG(pool)), + PinCase: pinCase, BoardCase: board.New(log, boardRepo.NewBoardRepoPG(pool), userRepo.NewUserRepoPG(pool), bluemonday.UGCPolicy()), SubscriptionCase: subscription.New(log, subRepo.NewSubscriptionRepoPG(pool), userRepo.NewUserRepoPG(pool), bluemonday.UGCPolicy()), SearchCase: search.New(log, searchRepo.NewSearchRepoPG(pool), bluemonday.UGCPolicy()), MessageCase: messageCase, + CommentCase: comment.New(commentRepo.NewCommentRepoPG(pool), pinCase, notifyCase), }) - wsHandler := deliveryWS.New(log, messageCase, + wsHandler := deliveryWS.New(log, messageCase, notifyCase, deliveryWS.SetOriginPatterns([]string{"pinspire.online", "pinspire.online:*"})) cfgServ, err := server.NewConfig(cfg.ServerConfigFile) diff --git a/internal/app/auth/auth.go b/internal/app/auth/auth.go index e5a9aa7..d33c702 100644 --- a/internal/app/auth/auth.go +++ b/internal/app/auth/auth.go @@ -3,6 +3,7 @@ package auth import ( "context" "net" + "os" "time" "github.com/joho/godotenv" @@ -54,10 +55,9 @@ func Run(ctx context.Context, log *logger.Logger, cfg Config) { ctxRedis, cancelCtxRedis := context.WithTimeout(ctx, _timeoutForConnRedis) defer cancelCtxRedis() - redisCfg, err := app.NewConfig(cfg.RedisFileConfig) - if err != nil { - log.Error(err.Error()) - return + redisCfg := app.RedisConfig{ + Addr: os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"), + Password: os.Getenv("REDIS_PASSWORD"), } redisCl, err := app.NewRedisClient(ctxRedis, redisCfg) @@ -71,7 +71,7 @@ func Run(ctx context.Context, log *logger.Logger, cfg Config) { u := user.New(log, nil, userRepo.NewUserRepoPG(pool)) s := grpc.NewServer(grpc.ChainUnaryInterceptor( - interceptor.Monitoring(metrics, "localhost:8086"), + interceptor.Monitoring(metrics, "0.0.0.0:8086"), interceptor.Logger(log), )) authProto.RegisterAuthServer(s, authMS.New(log, sm, u)) diff --git a/internal/app/config.go b/internal/app/config.go index 7fa6c87..4fb3bb2 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -11,18 +11,18 @@ type ConfigFiles struct { 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 index a363334..27ca153 100644 --- a/internal/app/messenger/messenger.go +++ b/internal/app/messenger/messenger.go @@ -42,13 +42,13 @@ func Run(ctx context.Context, log *logger.Logger) { messageCase := message.New(mesRepo.NewMessageRepo(pool)) server := grpc.NewServer(grpc.ChainUnaryInterceptor( - interceptor.Monitoring(metrics, "localhost:8096"), + 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", "localhost:8095") + l, err := net.Listen("tcp", "0.0.0.0:8095") if 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/messenger/usecase/message/mock/message_mock.go b/internal/microservices/messenger/usecase/message/mock/message_mock.go index 449be90..65e877a 100644 --- a/internal/microservices/messenger/usecase/message/mock/message_mock.go +++ b/internal/microservices/messenger/usecase/message/mock/message_mock.go @@ -49,6 +49,21 @@ func (mr *MockUsecaseMockRecorder) DeleteMessage(ctx, userID, mesID interface{}) 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() @@ -65,6 +80,22 @@ func (mr *MockUsecaseMockRecorder) GetMessagesFromChat(ctx, chat, count, lastID 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() diff --git a/internal/microservices/realtime/node.go b/internal/microservices/realtime/node.go index db17bf6..53d64cc 100644 --- a/internal/microservices/realtime/node.go +++ b/internal/microservices/realtime/node.go @@ -2,6 +2,7 @@ package realtime import ( "fmt" + "os" "sync" "google.golang.org/protobuf/proto" @@ -18,9 +19,9 @@ type Node struct { func NewNode() (*Node, error) { node := &Node{} - broker, err := NewKafkaBroker(node, KafkaConfig{ - Addres: []string{"localhost:9092"}, + // Addres: []string{"localhost:9092"}, + Addres: []string{os.Getenv("KAFKA_BROKER_ADDRESS") + ":" + os.Getenv("KAFKA_BROKER_PORT")}, PartitionsOnTopic: _numWorkers, MaxNumTopic: 10, }) diff --git a/internal/microservices/realtime/server.go b/internal/microservices/realtime/server.go index c83d166..0574eef 100644 --- a/internal/microservices/realtime/server.go +++ b/internal/microservices/realtime/server.go @@ -42,7 +42,7 @@ func (s Server) Publish(ctx context.Context, pm *rt.PublishMessage) (*empty.Empt return &empty.Empty{}, nil } -func (s Server) Subscribe(c *rt.Channel, ss rt.RealTime_SubscribeServer) error { +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") @@ -52,7 +52,9 @@ func (s Server) Subscribe(c *rt.Channel, ss rt.RealTime_SubscribeServer) error { transport: ss, } - s.node.AddSubscriber(c, client) + for _, ch := range chans.GetChans() { + s.node.AddSubscriber(ch, client) + } <-ss.Context().Done() return nil diff --git a/internal/pkg/delivery/http/v1/auth.go b/internal/pkg/delivery/http/v1/auth.go index 6f536a7..0c0149a 100644 --- a/internal/pkg/delivery/http/v1/auth.go +++ b/internal/pkg/delivery/http/v1/auth.go @@ -1,7 +1,6 @@ package v1 import ( - "encoding/json" "net/http" "time" @@ -10,6 +9,7 @@ import ( "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 @@ -56,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()}) @@ -122,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()}) 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 317e1c4..2b02193 100644 --- a/internal/pkg/delivery/http/v1/board.go +++ b/internal/pkg/delivery/http/v1/board.go @@ -2,45 +2,25 @@ package v1 import ( "encoding/json" - "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" log "github.com/go-park-mail-ru/2023_2_OND_team/pkg/logger" ) var TimeFormat = "2006-01-02" -// 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']"` -} - -type DeletePinFromBoard struct { - PinID int `json:"pin_id" example:"22"` -} - -func ToCertainBoardFromService(board entity.BoardWithContent) CertainBoard { - return CertainBoard{ +func ToCertainBoardFromService(board entity.BoardWithContent) structs.CertainBoard { + return structs.CertainBoard{ ID: board.BoardInfo.ID, + AuthorID: board.BoardInfo.AuthorID, Title: board.BoardInfo.Title, Description: board.BoardInfo.Description, CreatedAt: board.BoardInfo.CreatedAt.Format(TimeFormat), @@ -50,40 +30,34 @@ func ToCertainBoardFromService(board entity.BoardWithContent) CertainBoard { } } -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 +func ToCertainBoardUsernameFromService(board entity.BoardWithContent, username string) structs.CertainBoardWithUsername { + return 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, } - if err := checkIsValidTagTitles(data.Tags); err != nil { - return fmt.Errorf("%s: %w", err.Error(), ErrInvalidTagTitles) - } - return nil } func (h *HandlerHTTP) CreateNewBoard(w http.ResponseWriter, r *http.Request) { logger := h.getRequestLogger(r) if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - code, message := getErrCodeMessage(ErrBadContentType) + 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 } @@ -91,7 +65,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 } @@ -112,7 +86,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 } @@ -121,7 +95,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())) } } @@ -130,8 +104,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 } @@ -139,12 +113,12 @@ 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)) } @@ -152,7 +126,7 @@ func (h *HandlerHTTP) GetUserBoards(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())) } } @@ -162,24 +136,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", 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())) } } @@ -189,15 +163,15 @@ 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 } @@ -206,14 +180,14 @@ func (h *HandlerHTTP) GetBoardInfoForUpdate(w http.ResponseWriter, r *http.Reque 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 := r.Header.Get("Content-Type"); contentType != ApplicationJson { - code, message := getErrCodeMessage(ErrBadContentType) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadContentType) responseError(w, code, message) return } @@ -221,17 +195,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 } @@ -239,7 +213,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 } @@ -258,7 +232,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 } @@ -267,7 +241,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())) } } @@ -277,7 +251,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 } @@ -285,7 +259,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 } @@ -294,7 +268,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())) } } @@ -362,30 +336,30 @@ func (h *HandlerHTTP) DeletePinFromBoard(w http.ResponseWriter, r *http.Request) 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 := getErrCodeMessage(ErrBadUrlParam) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadUrlParam) responseError(w, code, message) return } if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - code, message := getErrCodeMessage(ErrBadContentType) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadContentType) responseError(w, code, message) return } - delPinFromBoard := DeletePinFromBoard{} - err = json.NewDecoder(r.Body).Decode(&delPinFromBoard) + delPinFromBoard := structs.DeletePinFromBoard{} + err = easyjson.UnmarshalFromReader(r.Body, &delPinFromBoard) + defer r.Body.Close() if err != nil { - code, message := getErrCodeMessage(ErrBadBody) + code, message := errHTTP.GetErrCodeMessage(errHTTP.ErrBadBody) responseError(w, code, message) return } - defer r.Body.Close() 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 := getErrCodeMessage(err) + code, message := errHTTP.GetErrCodeMessage(err) responseError(w, code, message) return } @@ -394,33 +368,6 @@ func (h *HandlerHTTP) DeletePinFromBoard(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())) } } - -/* -logger := h.getRequestLogger(r) - - 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) - responseError(w, code, message) - return - } - - 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) - responseError(w, code, message) - return - } - - err = responseOk(http.StatusOK, w, "deleted board successfully", nil) - if err != nil { - logger.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(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 index ac6adf5..09e64cb 100644 --- a/internal/pkg/delivery/http/v1/chat.go +++ b/internal/pkg/delivery/http/v1/chat.go @@ -90,7 +90,7 @@ func (h *HandlerHTTP) DeleteMessage(w http.ResponseWriter, r *http.Request) { return } - err = h.messageCase.DeleteMessage(r.Context(), userID, messageID) + 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") diff --git a/internal/pkg/delivery/http/v1/comment.go b/internal/pkg/delivery/http/v1/comment.go new file mode 100644 index 0000000..a5dd834 --- /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": feed, "lastID": newLastID}) + if err != nil { + logger.Error(err.Error()) + } +} diff --git a/internal/pkg/delivery/http/v1/board_errors.go b/internal/pkg/delivery/http/v1/errors/board.go similarity index 83% rename from internal/pkg/delivery/http/v1/board_errors.go rename to internal/pkg/delivery/http/v1/errors/board.go index ab31833..0d2106c 100644 --- a/internal/pkg/delivery/http/v1/board_errors.go +++ b/internal/pkg/delivery/http/v1/errors/board.go @@ -1,4 +1,4 @@ -package v1 +package errors import ( "errors" @@ -16,8 +16,8 @@ var ( ) var ( - wrappedErrors = map[error]string{ErrInvalidTagTitles: "bad_Tagtitles"} - errCodeCompability = map[error]string{ + WrappedErrors = map[error]string{ErrInvalidTagTitles: "bad_Tagtitles"} + ErrCodeCompability = map[error]string{ ErrInvalidBoardTitle: "bad_boardTitle", ErrEmptyTitle: "empty_boardTitle", ErrEmptyPubOpt: "bad_pubOpt", @@ -29,7 +29,7 @@ var ( } ) -func getErrCodeMessage(err error) (string, string) { +func GetErrCodeMessage(err error) (string, string) { var ( code string general, specific bool @@ -40,9 +40,9 @@ func getErrCodeMessage(err error) (string, string) { return code, err.Error() } - code, specific = errCodeCompability[err] + code, specific = ErrCodeCompability[err] if !specific { - for wrappedErr, code_ := range wrappedErrors { + for wrappedErr, code_ := range WrappedErrors { if errors.Is(err, wrappedErr) { specific = true code = code_ 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 837e590..736654e 100644 --- a/internal/pkg/delivery/http/v1/handler.go +++ b/internal/pkg/delivery/http/v1/handler.go @@ -3,6 +3,7 @@ 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/search" @@ -20,6 +21,7 @@ type HandlerHTTP struct { subCase subscription.Usecase searchCase search.Usecase messageCase message.Usecase + commentCase comment.Usecase } func New(log *logger.Logger, hub UsecaseHub) *HandlerHTTP { @@ -32,6 +34,7 @@ func New(log *logger.Logger, hub UsecaseHub) *HandlerHTTP { subCase: hub.SubscriptionCase, searchCase: hub.SearchCase, messageCase: hub.MessageCase, + commentCase: hub.CommentCase, } } @@ -43,4 +46,5 @@ type UsecaseHub struct { SubscriptionCase subscription.Usecase SearchCase search.Usecase MessageCase message.Usecase + CommentCase comment.Usecase } diff --git a/internal/pkg/delivery/http/v1/pin.go b/internal/pkg/delivery/http/v1/pin.go index be7c2c9..a0868b3 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,7 +172,7 @@ 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 { 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 b03ed6f..be60ccb 100644 --- a/internal/pkg/delivery/http/v1/profile.go +++ b/internal/pkg/delivery/http/v1/profile.go @@ -1,38 +1,22 @@ 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/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/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" ) -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"` -} - -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 ToUserInfoFromService(user *userEntity.User, isSubscribed bool, subsCount int) UserInfo { - return UserInfo{ +func ToUserInfoFromService(user *userEntity.User, isSubscribed bool, subsCount int) structs.UserInfo { + return structs.UserInfo{ ID: user.ID, Username: user.Username, Avatar: user.Avatar, @@ -44,8 +28,8 @@ func ToUserInfoFromService(user *userEntity.User, isSubscribed bool, subsCount i } } -func ToProfileInfoFromService(user *userEntity.User, subsCount int) ProfileInfo { - return ProfileInfo{ +func ToProfileInfoFromService(user *userEntity.User, subsCount int) structs.ProfileInfo { + return structs.ProfileInfo{ ID: user.ID, Username: user.Username, Avatar: user.Avatar, @@ -57,7 +41,7 @@ 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, &ErrInvalidUrlParams{map[string]string{"userID": userIdParam}}) + h.responseErr(w, r, &errHTTP.ErrInvalidUrlParams{Params: map[string]string{"userID": userIdParam}}) return } @@ -82,7 +66,7 @@ func (h *HandlerHTTP) ProfileEditInfo(w http.ResponseWriter, r *http.Request) { 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()) 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 98ab544..7b77059 100644 --- a/internal/pkg/delivery/http/v1/response.go +++ b/internal/pkg/delivery/http/v1/response.go @@ -1,140 +1,26 @@ package v1 import ( - "encoding/json" - "errors" "fmt" "net/http" - errPkg "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/errors" + 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" ) -// 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 -} - -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) @@ -146,12 +32,12 @@ 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) } @@ -162,7 +48,7 @@ func responseError(w http.ResponseWriter, code, message string) error { func (h *HandlerHTTP) responseErr(w http.ResponseWriter, r *http.Request, err error) error { log := logger.GetLoggerFromCtx(r.Context()) - code, status := getCodeStatusHttp(err) + code, status := errHTTP.GetCodeStatusHttp(err) var msg string if status == http.StatusInternalServerError { log.Warnf("unexpected application error: %s", err.Error()) @@ -171,12 +57,12 @@ func (h *HandlerHTTP) responseErr(w http.ResponseWriter, r *http.Request, err er msg = err.Error() } - res := JsonErrResponse{ + res := structs.JsonErrResponse{ Status: "error", Message: msg, Code: code, } - resBytes, err := json.Marshal(res) + resBytes, err := easyjson.Marshal(res) if err != nil { return fmt.Errorf("responseError: %w", err) } diff --git a/internal/pkg/delivery/http/v1/search.go b/internal/pkg/delivery/http/v1/search.go index f798d36..4ec1e79 100644 --- a/internal/pkg/delivery/http/v1/search.go +++ b/internal/pkg/delivery/http/v1/search.go @@ -4,6 +4,7 @@ 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" ) @@ -89,7 +90,7 @@ func GetSearchOpts(r *http.Request, sortOpts []string, defaultSortOpt string) (* } if len(invalidParams) > 0 { - return nil, &ErrInvalidQueryParam{params: invalidParams} + return nil, &errHTTP.ErrInvalidQueryParam{Params: invalidParams} } return opts, nil @@ -114,7 +115,7 @@ func GetGeneralOpts(r *http.Request, invalidParams map[string]string) (*search.G opts.Template = template } } else { - return nil, &ErrNoData{} + return nil, &errHTTP.ErrNoData{} } if sortOrder := r.URL.Query().Get("order"); sortOrder != "" { 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..9e42ece --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/board.go @@ -0,0 +1,114 @@ +package structs + +import ( + "fmt" + "unicode" + + errHTTP "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/delivery/http/v1/errors" +) + +//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']"` +} + +//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..3afc2aa --- /dev/null +++ b/internal/pkg/delivery/http/v1/structs/user.go @@ -0,0 +1,23 @@ +package structs + +//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"` +} + +//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"` +} 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 index 4614476..7444bdf 100644 --- a/internal/pkg/delivery/http/v1/subscription.go +++ b/internal/pkg/delivery/http/v1/subscription.go @@ -1,12 +1,15 @@ package v1 import ( - "encoding/json" "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 ( @@ -17,26 +20,16 @@ var ( maxCount = 50 ) -type SubscriptionAction struct { - To *int `json:"to" example:"2"` -} - -func (s *SubscriptionAction) Validate() error { - if s.To == nil { - return &ErrMissingBodyParams{[]string{"to"}} - } - return nil -} - func (h *HandlerHTTP) Subscribe(w http.ResponseWriter, r *http.Request) { if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - h.responseErr(w, r, &ErrInvalidContentType{preferredType: ApplicationJson}) + h.responseErr(w, r, &errHTTP.ErrInvalidContentType{PreferredType: ApplicationJson}) return } - sub := SubscriptionAction{} - if err := json.NewDecoder(r.Body).Decode(&sub); err != nil { - h.responseErr(w, r, &ErrInvalidBody{}) + sub := structs.SubscriptionAction{} + + if err := easyjson.UnmarshalFromReader(r.Body, &sub); err != nil { + h.responseErr(w, r, &errHTTP.ErrInvalidBody{}) return } defer r.Body.Close() @@ -56,13 +49,13 @@ func (h *HandlerHTTP) Subscribe(w http.ResponseWriter, r *http.Request) { func (h *HandlerHTTP) Unsubscribe(w http.ResponseWriter, r *http.Request) { if contentType := r.Header.Get("Content-Type"); contentType != ApplicationJson { - h.responseErr(w, r, &ErrInvalidContentType{preferredType: ApplicationJson}) + h.responseErr(w, r, &errHTTP.ErrInvalidContentType{PreferredType: ApplicationJson}) return } - sub := SubscriptionAction{} - if err := json.NewDecoder(r.Body).Decode(&sub); err != nil { - h.responseErr(w, r, &ErrInvalidBody{}) + sub := structs.SubscriptionAction{} + if err := easyjson.UnmarshalFromReader(r.Body, &sub); err != nil { + h.responseErr(w, r, &errHTTP.ErrInvalidBody{}) return } defer r.Body.Close() @@ -146,7 +139,7 @@ func GetOpts(r *http.Request) (*userEntity.SubscriptionOpts, error) { opts.Count = maxCount } if len(invalidParams) > 0 { - return nil, &ErrInvalidQueryParam{invalidParams} + return nil, &errHTTP.ErrInvalidQueryParam{invalidParams} } return opts, nil } 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 index b0ff2c9..1271683 100644 --- a/internal/pkg/delivery/websocket/types.go +++ b/internal/pkg/delivery/websocket/types.go @@ -2,26 +2,20 @@ package websocket import "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/message" -type Channel struct { - Name string `json:"name"` - Topic string `json:"topic"` -} +//go:generate easyjson --all type Object struct { Type string `json:"eventType,omitempty"` Message message.Message `json:"message"` } -type Request struct { - ID int `json:"requestID"` - Action string - Channel Channel - Message Object +type PublishRequest struct { + ID int `json:"requestID"` + Message Object `json:"message"` } type MessageFromChannel struct { Type string `json:"type"` - Channel Channel `json:"channel"` Message ResponseMessage `json:"message"` } @@ -52,10 +46,9 @@ func newResponseOnRequest(id int, status, code, message string, body any) *Respo } } -func newMessageFromChannel(channel Channel, status, code string, v any) *MessageFromChannel { +func newMessageFromChannel(status, code string, v any) *MessageFromChannel { mes := &MessageFromChannel{ - Type: "event", - Channel: channel, + Type: "event", Message: ResponseMessage{ Status: status, Code: code, 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 index 6074730..bd7db73 100644 --- a/internal/pkg/delivery/websocket/websocket.go +++ b/internal/pkg/delivery/websocket/websocket.go @@ -6,23 +6,22 @@ import ( "net/http" "time" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" ws "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" - 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/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/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 - client rt.RealTimeClient + notifySub notifySubscriber } type Option func(h *HandlerWebSocket) @@ -35,14 +34,9 @@ func SetOriginPatterns(patterns []string) Option { } } -func New(log *log.Logger, mesCase usecase.Usecase, opts ...Option) *HandlerWebSocket { - gRPCConn, err := grpc.Dial("localhost:8090", grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - log.Error(fmt.Errorf("grpc dial: %w", err).Error()) - } +func New(log *log.Logger, mesCase usecase.Usecase, notify notifySubscriber, opts ...Option) *HandlerWebSocket { + handlerWS := &HandlerWebSocket{log: log, messageCase: mesCase, notifySub: notify} - client := rt.NewRealTimeClient(gRPCConn) - handlerWS := &HandlerWebSocket{log: log, messageCase: mesCase, client: client} for _, opt := range opts { opt(handlerWS) } @@ -50,178 +44,10 @@ func New(log *log.Logger, mesCase usecase.Usecase, opts ...Option) *HandlerWebSo return handlerWS } -func (h *HandlerWebSocket) WebSocketConnect(w http.ResponseWriter, r *http.Request) { +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 { - 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() - - err = h.serveWebSocketConn(ctx, conn, userID) - if err != nil { - h.log.Error(err.Error()) - } -} - -func (h *HandlerWebSocket) serveWebSocketConn(ctx context.Context, conn *ws.Conn, userID int) error { - request := &Request{} - var err error - for { - err = wsjson.Read(ctx, conn, request) - if err != nil { - h.log.Error(err.Error()) - return fmt.Errorf("read message: %w", err) - } - switch request.Action { - case "Publish": - switch request.Message.Type { - case "create": - mesCopy := &message.Message{} - *mesCopy = request.Message.Message - mesCopy.From = userID - id, err := h.messageCase.SendMessage(ctx, userID, mesCopy) - if err != nil { - h.log.Warn(err.Error()) - continue - } - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "ok", "", "publish success", map[string]int{"id": id})) - _, err = h.client.Publish(ctx, &rt.PublishMessage{ - Channel: &rt.Channel{ - Name: request.Channel.Name, - Topic: request.Channel.Topic, - }, - Message: &rt.Message{ - Body: &rt.Message_Object{ - Object: &rt.EventObject{ - Type: rt.EventType_EV_CREATE, - Id: int64(id), - }, - }, - }, - }) - if err != nil { - h.log.Error(err.Error()) - } - case "update": - mesCopy := &message.Message{} - *mesCopy = request.Message.Message - err = h.messageCase.UpdateContentMessage(ctx, userID, mesCopy) - if err != nil { - h.log.Warn(err.Error()) - continue - } - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "ok", "", "publish success", nil)) - _, err = h.client.Publish(ctx, &rt.PublishMessage{ - Channel: &rt.Channel{ - Name: request.Channel.Name, - Topic: request.Channel.Topic, - }, - Message: &rt.Message{ - Body: &rt.Message_Object{ - Object: &rt.EventObject{ - Type: rt.EventType_EV_UPDATE, - Id: int64(request.Message.Message.ID), - }, - }, - }, - }) - if err != nil { - h.log.Error(err.Error()) - } - - case "delete": - err = h.messageCase.DeleteMessage(ctx, userID, request.Message.Message.ID) - if err != nil { - h.log.Warn(err.Error()) - continue - } - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "ok", "", "publish success", nil)) - _, err = h.client.Publish(ctx, &rt.PublishMessage{ - Channel: &rt.Channel{ - Name: request.Channel.Name, - Topic: request.Channel.Topic, - }, - Message: &rt.Message{ - Body: &rt.Message_Object{ - Object: &rt.EventObject{ - Type: rt.EventType_EV_DELETE, - Id: int64(request.Message.Message.ID), - }, - }, - }, - }) - if err != nil { - h.log.Error(err.Error()) - } - default: - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "error", "unsupported", "unsupported eventType", nil)) - } - case "Subscribe": - err = h.subscribe(ctx, h.client, request, conn, userID) - if err != nil { - h.log.Warn(err.Error()) - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "error", "subscribe_fail", "failed to subscribe to the channel", nil)) - continue - } - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "ok", "", "you have successfully subscribed to the channel", nil)) - default: - wsjson.Write(ctx, conn, newResponseOnRequest(request.ID, "error", "unsupported", "unsupported action", nil)) - } - } -} - -func (h *HandlerWebSocket) subscribe(ctx context.Context, client rt.RealTimeClient, req *Request, conn *ws.Conn, userID int) error { - sc, err := client.Subscribe(ctx, &rt.Channel{ - Name: req.Channel.Name, - Topic: req.Channel.Topic, - }) - if err != nil { - return fmt.Errorf("subscribe: %w", err) + return nil, fmt.Errorf("upgrade to websocket connect: %w", err) } - go func() { - for { - obj, err := sc.Recv() - if err != nil { - return - } - mes, ok := obj.Body.(*rt.Message_Object) - if ok { - var msg *message.Message - if mes.Object.Type == rt.EventType_EV_DELETE { - msg = &message.Message{ID: int(mes.Object.Id)} - } else { - msg, err = h.messageCase.GetMessage(ctx, userID, int(mes.Object.Id)) - if err != nil { - h.log.Error(err.Error()) - return - } - } - objType := "" - switch mes.Object.Type { - case rt.EventType_EV_CREATE: - objType = "create" - case rt.EventType_EV_UPDATE: - objType = "update" - case rt.EventType_EV_DELETE: - objType = "delete" - } - err = wsjson.Write(ctx, conn, newMessageFromChannel(req.Channel, "ok", "", Object{ - Type: objType, - Message: *msg, - })) - if err != nil { - h.log.Error(err.Error()) - return - } - } - } - }() - return nil + return conn, nil } diff --git a/internal/pkg/entity/board/board.go b/internal/pkg/entity/board/board.go index 92c37b6..d695468 100644 --- a/internal/pkg/entity/board/board.go +++ b/internal/pkg/entity/board/board.go @@ -6,9 +6,12 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//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 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..493e153 --- /dev/null +++ b/internal/pkg/entity/comment/comment.go @@ -0,0 +1,16 @@ +package comment + +import ( + "github.com/jackc/pgx/v5/pgtype" + + "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" +) + +//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"` +} 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 index f6f893d..ba42c98 100644 --- a/internal/pkg/entity/message/message.go +++ b/internal/pkg/entity/message/message.go @@ -6,10 +6,13 @@ import ( "github.com/go-park-mail-ru/2023_2_OND_team/internal/pkg/entity/user" ) +//go:generate easyjson message.go + type Chat [2]int +//easyjson:json type Message struct { - ID int + ID int `json:"id,omitempty"` From int `json:"from"` To int `json:"to"` Content pgtype.Text `json:"content"` 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/search/search.go b/internal/pkg/entity/search/search.go index 76b0c6a..478aabe 100644 --- a/internal/pkg/entity/search/search.go +++ b/internal/pkg/entity/search/search.go @@ -8,6 +8,8 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//go:generate easyjson search.go + type Template string func (t *Template) Validate() bool { @@ -26,12 +28,14 @@ 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"` @@ -39,6 +43,7 @@ type PinForSearch struct { Likes int `json:"likes"` } +//easyjson:json type UserForSearch struct { ID int `json:"id"` Username string `json:"username"` 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 1b270be..5a636fc 100644 --- a/internal/pkg/entity/user/user.go +++ b/internal/pkg/entity/user/user.go @@ -5,6 +5,11 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//go:generate easyjson user.go + +const UserUnknown = -1 + +//easyjson:json type User struct { ID int `json:"id,omitempty" example:"123"` Username string `json:"username" example:"Green"` @@ -16,6 +21,7 @@ type User struct { Password string `json:"password,omitempty" example:"pass123"` } // @name User +//easyjson:json type SubscriptionUser struct { ID int `json:"id"` Username string `json:"username"` 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/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/repo.go b/internal/pkg/repository/board/postgres/repo.go index 1c92ad1..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) { diff --git a/internal/pkg/repository/board/repo.go b/internal/pkg/repository/board/repo.go index 7c6634f..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) 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..cfaf576 --- /dev/null +++ b/internal/pkg/repository/comment/repo.go @@ -0,0 +1,86 @@ +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() { + 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/message/mock/message_mock.go b/internal/pkg/repository/message/mock/message_mock.go index 275535a..b7b9a0e 100644 --- a/internal/pkg/repository/message/mock/message_mock.go +++ b/internal/pkg/repository/message/mock/message_mock.go @@ -94,6 +94,21 @@ func (mr *MockRepositoryMockRecorder) GetMessages(ctx, chat, count, lastID inter 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() 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/repo.go b/internal/pkg/repository/search/repo.go index 98ca7bf..b49c815 100644 --- a/internal/pkg/repository/search/repo.go +++ b/internal/pkg/repository/search/repo.go @@ -6,6 +6,7 @@ import ( "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) 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/repo.go b/internal/pkg/repository/subscription/repo.go index 7d23950..6c8c841 100644 --- a/internal/pkg/repository/subscription/repo.go +++ b/internal/pkg/repository/subscription/repo.go @@ -6,6 +6,7 @@ import ( 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 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 index 2c23f5a..49ee9bf 100644 --- a/internal/pkg/usecase/auth/usecase.go +++ b/internal/pkg/usecase/auth/usecase.go @@ -10,6 +10,7 @@ import ( "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) diff --git a/internal/pkg/usecase/board/get.go b/internal/pkg/usecase/board/get.go index 9e02946..e2555cd 100644 --- a/internal/pkg/usecase/board/get.go +++ b/internal/pkg/usecase/board/get.go @@ -42,20 +42,20 @@ func (bCase *boardUsecase) GetBoardsByUsername(ctx context.Context, username str 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 +70,17 @@ 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/usecase.go b/internal/pkg/usecase/board/usecase.go index 2fefc44..a00f980 100644 --- a/internal/pkg/usecase/board/usecase.go +++ b/internal/pkg/usecase/board/usecase.go @@ -15,7 +15,7 @@ import ( 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 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..459127c --- /dev/null +++ b/internal/pkg/usecase/comment/mock/comment_mock.go @@ -0,0 +1,148 @@ +// 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) +} + +// GetCommentWithAuthor mocks base method. +func (m *MockUsecase) GetCommentWithAuthor(ctx context.Context, commentID int) (*comment.Comment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCommentWithAuthor", ctx, commentID) + ret0, _ := ret[0].(*comment.Comment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCommentWithAuthor indicates an expected call of GetCommentWithAuthor. +func (mr *MockUsecaseMockRecorder) GetCommentWithAuthor(ctx, commentID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCommentWithAuthor", reflect.TypeOf((*MockUsecase)(nil).GetCommentWithAuthor), ctx, 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..8b73710 --- /dev/null +++ b/internal/pkg/usecase/image/filtration.go @@ -0,0 +1,93 @@ +package image + +import ( + "context" + "errors" + "strings" + + vision "cloud.google.com/go/vision/v2/apiv1" + pb "cloud.google.com/go/vision/v2/apiv1/visionpb" +) + +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 +} + +func NewFilter(client *vision.ImageAnnotatorClient) *googleVision { + return &googleVision{client} +} + +func CheckAnnotations(annotation *pb.SafeSearchAnnotation) bool { + if 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 { + return true + } + return false +} + +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 CheckExplicit(resp *pb.AnnotateImageResponse, explicitLabels []string) error { + if CheckCertainLabels(explicitLabels, GetImageLabels(resp.GetLabelAnnotations())) || + CheckAnnotations(resp.GetSafeSearchAnnotation()) { + return ErrExplicitImage + } + return nil +} + +func (vision *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}, + }, + }, + }, + } + resp, err := vision.visionClient.BatchAnnotateImages(ctx, req) + if err != nil { + return err + } + + return CheckExplicit(resp.GetResponses()[0], explicitLabels) +} diff --git a/internal/pkg/usecase/image/mock/image_mock.go b/internal/pkg/usecase/image/mock/image_mock.go index 4832c0e..4846c2c 100644 --- a/internal/pkg/usecase/image/mock/image_mock.go +++ b/internal/pkg/usecase/image/mock/image_mock.go @@ -5,6 +5,7 @@ package mock import ( + context "context" io "io" reflect "reflect" @@ -36,16 +37,16 @@ func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { } // UploadImage mocks base method. -func (m *MockUsecase) UploadImage(path, mimeType string, size int64, image io.Reader, check check.CheckSize) (string, error) { +func (m *MockUsecase) UploadImage(ctx context.Context, path, mimeType string, size int64, image io.Reader, check check.CheckSize) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UploadImage", path, mimeType, size, image, check) + ret := m.ctrl.Call(m, "UploadImage", ctx, path, mimeType, size, image, check) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // UploadImage indicates an expected call of UploadImage. -func (mr *MockUsecaseMockRecorder) UploadImage(path, mimeType, size, image, check interface{}) *gomock.Call { +func (mr *MockUsecaseMockRecorder) UploadImage(ctx, path, mimeType, size, image, check interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadImage", reflect.TypeOf((*MockUsecase)(nil).UploadImage), path, mimeType, size, image, check) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadImage", reflect.TypeOf((*MockUsecase)(nil).UploadImage), ctx, path, mimeType, size, image, check) } diff --git a/internal/pkg/usecase/image/usecase.go b/internal/pkg/usecase/image/usecase.go index 799ed62..5ace111 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" @@ -14,33 +15,43 @@ import ( const PrefixURLImage = "https://pinspire.online:8081/" -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/mock/message_mock.go b/internal/pkg/usecase/message/mock/message_mock.go index 449be90..3b6d816 100644 --- a/internal/pkg/usecase/message/mock/message_mock.go +++ b/internal/pkg/usecase/message/mock/message_mock.go @@ -9,6 +9,7 @@ import ( 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" ) @@ -36,23 +37,38 @@ func (m *MockUsecase) EXPECT() *MockUsecaseMockRecorder { } // DeleteMessage mocks base method. -func (m *MockUsecase) DeleteMessage(ctx context.Context, userID, mesID int) error { +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, mesID) + 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, mesID interface{}) *gomock.Call { +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, mesID) + 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, chat message.Chat, count, lastID int) ([]message.Message, int, error) { +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, chat, count, lastID) + ret := m.ctrl.Call(m, "GetMessagesFromChat", ctx, userID, chat, count, lastID) ret0, _ := ret[0].([]message.Message) ret1, _ := ret[1].(int) ret2, _ := ret[2].(error) @@ -60,24 +76,55 @@ func (m *MockUsecase) GetMessagesFromChat(ctx context.Context, chat message.Chat } // GetMessagesFromChat indicates an expected call of GetMessagesFromChat. -func (mr *MockUsecaseMockRecorder) GetMessagesFromChat(ctx, chat, count, lastID interface{}) *gomock.Call { +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, chat, count, lastID) + 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, mes *message.Message) (int, error) { +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, mes) + 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, mes interface{}) *gomock.Call { +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, "SendMessage", reflect.TypeOf((*MockUsecase)(nil).SendMessage), ctx, mes) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeUserToAllChats", reflect.TypeOf((*MockUsecase)(nil).SubscribeUserToAllChats), ctx, userID) } // UpdateContentMessage mocks base method. diff --git a/internal/pkg/usecase/message/usecase.go b/internal/pkg/usecase/message/usecase.go index 4cf8794..00bc0fc 100644 --- a/internal/pkg/usecase/message/usecase.go +++ b/internal/pkg/usecase/message/usecase.go @@ -12,9 +12,13 @@ import ( 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 { @@ -22,16 +26,40 @@ type Usecase interface { 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, mesID int) 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 + client mess.MessengerClient + realtimeChatCase chat.Usecase + log *logger.Logger + realtimeIsEnable bool } -func New(repo mess.MessengerClient) *messageCase { - return &messageCase{repo} +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) { @@ -43,6 +71,11 @@ func (m *messageCase) SendMessage(ctx context.Context, userID int, mes *entity.M 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 } @@ -74,13 +107,23 @@ func (m *messageCase) UpdateContentMessage(ctx context.Context, userID int, mes }); 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, mesID int) error { - if _, err := m.client.DeleteMessage(setAuthenticatedMetadataCtx(ctx, userID), &mess.MsgID{Id: int64(mesID)}); err != 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 } @@ -115,6 +158,51 @@ func (m *messageCase) GetUserChatsWithOtherUsers(ctx context.Context, userID, co 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()) + } + + 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 index 6b06e24..b6277ca 100644 --- a/internal/pkg/usecase/search/usecase.go +++ b/internal/pkg/usecase/search/usecase.go @@ -9,6 +9,7 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//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) 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 index 4a88d83..a00705f 100644 --- a/internal/pkg/usecase/subscription/usecase.go +++ b/internal/pkg/usecase/subscription/usecase.go @@ -10,6 +10,7 @@ import ( "github.com/microcosm-cc/bluemonday" ) +//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 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/profile.go b/internal/pkg/usecase/user/profile.go index 1a5d1e4..414c4a0 100644 --- a/internal/pkg/usecase/user/profile.go +++ b/internal/pkg/usecase/user/profile.go @@ -16,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) } 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 + } +}