From 8cc06988032754ba4bbdf26d3a148ed1060eccaf Mon Sep 17 00:00:00 2001 From: Myles <96409608+ice-myles@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:27:40 +0300 Subject: [PATCH] Task completion without required order. STAGING task prize information. (#50) --- application.yaml | 24 +++ cmd/santa-sleigh/api/docs.go | 39 +++++ cmd/santa-sleigh/api/swagger.json | 39 +++++ cmd/santa-sleigh/api/swagger.yaml | 28 ++++ cmd/santa-sleigh/santa.go | 5 +- cmd/santa-sleigh/tasks.go | 14 +- cmd/santa/api/docs.go | 32 ++++ cmd/santa/api/swagger.json | 32 ++++ cmd/santa/api/swagger.yaml | 23 +++ cmd/santa/contract.go | 3 +- cmd/santa/santa.go | 2 +- cmd/santa/tasks.go | 2 +- go.mod | 20 ++- go.sum | 37 +++-- levels-and-roles/.testdata/application.yaml | 1 + .../completed_levels_and_activated_roles.go | 10 +- levels-and-roles/contract.go | 16 +- tasks/.testdata/application.yaml | 22 +++ tasks/complete_tasks.go | 150 +++++++++++++----- tasks/contract.go | 53 +++++-- tasks/get_tasks.go | 72 +++++++-- tasks/tasks.go | 69 +++++++- .../callfluent/claim_username.txt | 7 + .../callfluent/follow_us_on_twitter.txt | 7 + .../callfluent/invite_friends.txt | 7 + .../translations/callfluent/join_telegram.txt | 7 + .../translations/callfluent/start_mining.txt | 7 + .../callfluent/upload_profile_picture.txt | 7 + .../translations/sealsend/claim_username.txt | 7 + .../sealsend/follow_us_on_twitter.txt | 7 + .../translations/sealsend/invite_friends.txt | 7 + tasks/translations/sealsend/join_telegram.txt | 7 + tasks/translations/sealsend/start_mining.txt | 7 + .../sealsend/upload_profile_picture.txt | 7 + .../translations/sunwaves/claim_username.txt | 7 + .../sunwaves/follow_us_on_twitter.txt | 7 + .../translations/sunwaves/invite_friends.txt | 7 + tasks/translations/sunwaves/join_telegram.txt | 7 + tasks/translations/sunwaves/start_mining.txt | 7 + .../sunwaves/upload_profile_picture.txt | 7 + 40 files changed, 694 insertions(+), 125 deletions(-) create mode 100644 tasks/translations/callfluent/claim_username.txt create mode 100644 tasks/translations/callfluent/follow_us_on_twitter.txt create mode 100644 tasks/translations/callfluent/invite_friends.txt create mode 100644 tasks/translations/callfluent/join_telegram.txt create mode 100644 tasks/translations/callfluent/start_mining.txt create mode 100644 tasks/translations/callfluent/upload_profile_picture.txt create mode 100644 tasks/translations/sealsend/claim_username.txt create mode 100644 tasks/translations/sealsend/follow_us_on_twitter.txt create mode 100644 tasks/translations/sealsend/invite_friends.txt create mode 100644 tasks/translations/sealsend/join_telegram.txt create mode 100644 tasks/translations/sealsend/start_mining.txt create mode 100644 tasks/translations/sealsend/upload_profile_picture.txt create mode 100644 tasks/translations/sunwaves/claim_username.txt create mode 100644 tasks/translations/sunwaves/follow_us_on_twitter.txt create mode 100644 tasks/translations/sunwaves/invite_friends.txt create mode 100644 tasks/translations/sunwaves/join_telegram.txt create mode 100644 tasks/translations/sunwaves/start_mining.txt create mode 100644 tasks/translations/sunwaves/upload_profile_picture.txt diff --git a/application.yaml b/application.yaml index c5ae285..7a28e8a 100644 --- a/application.yaml +++ b/application.yaml @@ -27,6 +27,25 @@ cmd/santa-sleigh: keyPath: cmd/santa-sleigh/.testdata/localhost.key wintr/auth/ice: jwtSecret: bogus +tasksList: &tasksList + - type: claim_username + prize: 100 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/nikename.svg + - type: start_mining + prize: 200 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/mining.svg + - type: upload_profile_picture + prize: 300 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/profile-waiting.svg + - type: follow_us_on_twitter + prize: 400 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/twitter-follow.svg + - type: join_telegram + prize: 500 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/join-telegram-waiting.svg + - type: invite_friends + prize: 600 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/invite.svg friends-invited: &friends-invited wintr/connectors/storage/v2: runDDL: true @@ -200,6 +219,7 @@ badges_test: consumingTopics: *badgesMessageBrokerTopics consumerGroup: santa-local-badges-test tasks: &tasks + tasksV2Enabled: false requiredFriendsInvited: 5 wintr/connectors/storage/v2: runDDL: true @@ -245,6 +265,8 @@ tasks: &tasks - name: users-table - name: mining-sessions-table - name: friends-invited + tasksList: *tasksList + tenantName: sunwaves tasks_test: <<: *tasks messageBroker: @@ -252,6 +274,7 @@ tasks_test: consumingTopics: *tasksMessageBrokerTopics consumerGroup: santa-local-tasks-test levels-and-roles: &levels-and-roles + tasksV2Enabled: false requiredInvitedFriendsToBecomeAmbassador: 3 roleNames: - Snowman @@ -289,6 +312,7 @@ levels-and-roles: &levels-and-roles password: pass replicaURLs: - postgresql://root:pass@localhost:5432/santa + tasksList: *tasksList messageBroker: &levels-and-rolesMessageBroker consumerGroup: levels-and-roles-testing createTopics: true diff --git a/cmd/santa-sleigh/api/docs.go b/cmd/santa-sleigh/api/docs.go index 7167ed2..146cfa6 100644 --- a/cmd/santa-sleigh/api/docs.go +++ b/cmd/santa-sleigh/api/docs.go @@ -378,6 +378,13 @@ const docTemplate = `{ "in": "path", "required": true }, + { + "type": "string", + "description": "language to get tasks translation", + "name": "language", + "in": "path", + "required": true + }, { "description": "Request params. Set it only if task completion requires additional data.", "name": "request", @@ -579,6 +586,31 @@ const docTemplate = `{ "twitterUserHandle": { "type": "string", "example": "jdoe2" + }, + "verificationCode": { + "type": "string", + "example": "ABC" + } + } + }, + "tasks.Metadata": { + "type": "object", + "properties": { + "iconUrl": { + "type": "string", + "example": "https://app.ice.com/web/invite.svg" + }, + "longDescription": { + "type": "string", + "example": "Long description" + }, + "shortDescription": { + "type": "string", + "example": "Short description" + }, + "title": { + "type": "string", + "example": "Claim username" } } }, @@ -592,6 +624,13 @@ const docTemplate = `{ "data": { "$ref": "#/definitions/tasks.Data" }, + "metadata": { + "$ref": "#/definitions/tasks.Metadata" + }, + "prize": { + "type": "number", + "example": 200 + }, "type": { "allOf": [ { diff --git a/cmd/santa-sleigh/api/swagger.json b/cmd/santa-sleigh/api/swagger.json index 8237a0d..5778d19 100644 --- a/cmd/santa-sleigh/api/swagger.json +++ b/cmd/santa-sleigh/api/swagger.json @@ -371,6 +371,13 @@ "in": "path", "required": true }, + { + "type": "string", + "description": "language to get tasks translation", + "name": "language", + "in": "path", + "required": true + }, { "description": "Request params. Set it only if task completion requires additional data.", "name": "request", @@ -572,6 +579,31 @@ "twitterUserHandle": { "type": "string", "example": "jdoe2" + }, + "verificationCode": { + "type": "string", + "example": "ABC" + } + } + }, + "tasks.Metadata": { + "type": "object", + "properties": { + "iconUrl": { + "type": "string", + "example": "https://app.ice.com/web/invite.svg" + }, + "longDescription": { + "type": "string", + "example": "Long description" + }, + "shortDescription": { + "type": "string", + "example": "Short description" + }, + "title": { + "type": "string", + "example": "Claim username" } } }, @@ -585,6 +617,13 @@ "data": { "$ref": "#/definitions/tasks.Data" }, + "metadata": { + "$ref": "#/definitions/tasks.Metadata" + }, + "prize": { + "type": "number", + "example": 200 + }, "type": { "allOf": [ { diff --git a/cmd/santa-sleigh/api/swagger.yaml b/cmd/santa-sleigh/api/swagger.yaml index 2a395d8..f56fbbf 100644 --- a/cmd/santa-sleigh/api/swagger.yaml +++ b/cmd/santa-sleigh/api/swagger.yaml @@ -96,6 +96,24 @@ definitions: twitterUserHandle: example: jdoe2 type: string + verificationCode: + example: ABC + type: string + type: object + tasks.Metadata: + properties: + iconUrl: + example: https://app.ice.com/web/invite.svg + type: string + longDescription: + example: Long description + type: string + shortDescription: + example: Short description + type: string + title: + example: Claim username + type: string type: object tasks.Task: properties: @@ -104,6 +122,11 @@ definitions: type: boolean data: $ref: '#/definitions/tasks.Data' + metadata: + $ref: '#/definitions/tasks.Metadata' + prize: + example: 200 + type: number type: allOf: - $ref: '#/definitions/tasks.Type' @@ -377,6 +400,11 @@ paths: name: userId required: true type: string + - description: language to get tasks translation + in: path + name: language + required: true + type: string - description: Request params. Set it only if task completion requires additional data. in: body diff --git a/cmd/santa-sleigh/santa.go b/cmd/santa-sleigh/santa.go index 41b4b5e..48a9f77 100644 --- a/cmd/santa-sleigh/santa.go +++ b/cmd/santa-sleigh/santa.go @@ -17,7 +17,8 @@ import ( type ( GetTasksArg struct { - UserID string `uri:"userId" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4" swaggerignore:"true" required:"true"` + UserID string `uri:"userId" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4" swaggerignore:"true" required:"true"` + Language string `uri:"language" example:"en" swaggerignore:"true" required:"false"` } GetLevelsAndRolesSummaryArg struct { UserID string `uri:"userId" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4" allowForbiddenGet:"true" swaggerignore:"true" required:"true"` @@ -186,7 +187,7 @@ func (s *service) GetTasks( //nolint:gocritic // False negative. if req.Data.UserID != req.AuthenticatedUser.UserID { return nil, server.Forbidden(errors.Errorf("not allowed. %v != %v", req.Data.UserID, req.AuthenticatedUser.UserID)) } - resp, err := s.tasksProcessor.GetTasks(ctx, req.Data.UserID) + resp, err := s.tasksProcessor.GetTasks(ctx, req.Data.UserID, req.Data.Language) if err != nil { err = errors.Wrapf(err, "failed to GetTasks for data:%#v", req.Data) diff --git a/cmd/santa-sleigh/tasks.go b/cmd/santa-sleigh/tasks.go index 33706da..9749ade 100644 --- a/cmd/santa-sleigh/tasks.go +++ b/cmd/santa-sleigh/tasks.go @@ -27,6 +27,7 @@ func (s *service) setupTasksRoutes(router *server.Router) { // @Param Authorization header string true "Insert your access token" default(Bearer ) // @Param taskType path string true "the type of the task" enums(claim_username,start_mining,upload_profile_picture,follow_us_on_twitter,join_telegram,invite_friends) // @Param userId path string true "the id of the user that completed the task" +// @Param language path string true "language to get tasks translation" // @Param request body CompleteTaskRequestBody false "Request params. Set it only if task completion requires additional data." // @Success 200 "ok" // @Failure 400 {object} server.ErrorResponse "if validations fail" @@ -41,9 +42,6 @@ func (s *service) PseudoCompleteTask( //nolint:gocritic // False negative. ctx context.Context, req *server.Request[CompleteTaskRequestBody, any], ) (*server.Response[any], *server.Response[server.ErrorResponse]) { - if err := req.Data.validate(); err != nil { - return nil, server.UnprocessableEntity(errors.Wrap(err, "validations failed"), invalidPropertiesErrorCode) - } if req.Data.TaskType == tasks.JoinTelegramType && (req.Data.Data == nil || req.Data.Data.TelegramUserHandle == "") { return nil, server.UnprocessableEntity(errors.Errorf("`data`.`telegramUserHandle` required"), invalidPropertiesErrorCode) } @@ -67,13 +65,3 @@ func (s *service) PseudoCompleteTask( //nolint:gocritic // False negative. return server.OK[any](), nil } - -func (arg *CompleteTaskRequestBody) validate() error { - for _, taskType := range &tasks.AllTypes { - if taskType == arg.TaskType { - return nil - } - } - - return errors.Errorf("invalid type `%v`", arg.TaskType) -} diff --git a/cmd/santa/api/docs.go b/cmd/santa/api/docs.go index 89fad1e..0293433 100644 --- a/cmd/santa/api/docs.go +++ b/cmd/santa/api/docs.go @@ -470,6 +470,31 @@ const docTemplate = `{ "twitterUserHandle": { "type": "string", "example": "jdoe2" + }, + "verificationCode": { + "type": "string", + "example": "ABC" + } + } + }, + "tasks.Metadata": { + "type": "object", + "properties": { + "iconUrl": { + "type": "string", + "example": "https://app.ice.com/web/invite.svg" + }, + "longDescription": { + "type": "string", + "example": "Long description" + }, + "shortDescription": { + "type": "string", + "example": "Short description" + }, + "title": { + "type": "string", + "example": "Claim username" } } }, @@ -483,6 +508,13 @@ const docTemplate = `{ "data": { "$ref": "#/definitions/tasks.Data" }, + "metadata": { + "$ref": "#/definitions/tasks.Metadata" + }, + "prize": { + "type": "number", + "example": 200 + }, "type": { "allOf": [ { diff --git a/cmd/santa/api/swagger.json b/cmd/santa/api/swagger.json index 7d0d4eb..67e23a9 100644 --- a/cmd/santa/api/swagger.json +++ b/cmd/santa/api/swagger.json @@ -464,6 +464,31 @@ "twitterUserHandle": { "type": "string", "example": "jdoe2" + }, + "verificationCode": { + "type": "string", + "example": "ABC" + } + } + }, + "tasks.Metadata": { + "type": "object", + "properties": { + "iconUrl": { + "type": "string", + "example": "https://app.ice.com/web/invite.svg" + }, + "longDescription": { + "type": "string", + "example": "Long description" + }, + "shortDescription": { + "type": "string", + "example": "Short description" + }, + "title": { + "type": "string", + "example": "Claim username" } } }, @@ -477,6 +502,13 @@ "data": { "$ref": "#/definitions/tasks.Data" }, + "metadata": { + "$ref": "#/definitions/tasks.Metadata" + }, + "prize": { + "type": "number", + "example": 200 + }, "type": { "allOf": [ { diff --git a/cmd/santa/api/swagger.yaml b/cmd/santa/api/swagger.yaml index 389c7c7..001ebd6 100644 --- a/cmd/santa/api/swagger.yaml +++ b/cmd/santa/api/swagger.yaml @@ -92,6 +92,24 @@ definitions: twitterUserHandle: example: jdoe2 type: string + verificationCode: + example: ABC + type: string + type: object + tasks.Metadata: + properties: + iconUrl: + example: https://app.ice.com/web/invite.svg + type: string + longDescription: + example: Long description + type: string + shortDescription: + example: Short description + type: string + title: + example: Claim username + type: string type: object tasks.Task: properties: @@ -100,6 +118,11 @@ definitions: type: boolean data: $ref: '#/definitions/tasks.Data' + metadata: + $ref: '#/definitions/tasks.Metadata' + prize: + example: 200 + type: number type: allOf: - $ref: '#/definitions/tasks.Type' diff --git a/cmd/santa/contract.go b/cmd/santa/contract.go index 4bb8195..52b7a9a 100644 --- a/cmd/santa/contract.go +++ b/cmd/santa/contract.go @@ -12,7 +12,8 @@ import ( type ( GetTasksArg struct { - UserID string `uri:"userId" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4" swaggerignore:"true" required:"true"` + UserID string `uri:"userId" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4" swaggerignore:"true" required:"true"` + Language string `uri:"language" example:"en" swaggerignore:"true" required:"false"` } GetLevelsAndRolesSummaryArg struct { UserID string `uri:"userId" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4" allowForbiddenGet:"true" swaggerignore:"true" required:"true"` diff --git a/cmd/santa/santa.go b/cmd/santa/santa.go index 8d70e28..de7c589 100644 --- a/cmd/santa/santa.go +++ b/cmd/santa/santa.go @@ -66,7 +66,7 @@ func (s *service) CheckHealth(ctx context.Context) error { return errors.Wrap(err, "get badges failed") } log.Debug("checking health...", "package", "tasks") - if _, err := s.tasksRepository.GetTasks(ctx, "bogus"); err != nil && !errors.Is(err, tasks.ErrRelationNotFound) { + if _, err := s.tasksRepository.GetTasks(ctx, "bogus", ""); err != nil && !errors.Is(err, tasks.ErrRelationNotFound) { return errors.Wrap(err, "get tasks failed") } log.Debug("checking health...", "package", "levels-and-roles") diff --git a/cmd/santa/tasks.go b/cmd/santa/tasks.go index c3adf67..88d0566 100644 --- a/cmd/santa/tasks.go +++ b/cmd/santa/tasks.go @@ -41,7 +41,7 @@ func (s *service) GetTasks( //nolint:gocritic // False negative. if req.Data.UserID != req.AuthenticatedUser.UserID { return nil, server.Forbidden(errors.Errorf("not allowed. %v != %v", req.Data.UserID, req.AuthenticatedUser.UserID)) } - resp, err := s.tasksRepository.GetTasks(ctx, req.Data.UserID) + resp, err := s.tasksRepository.GetTasks(ctx, req.Data.UserID, "") if err != nil { err = errors.Wrapf(err, "failed to GetTasks for data:%#v", req.Data) diff --git a/go.mod b/go.mod index 243029e..f5b12f3 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/ice-blockchain/santa go 1.23 -toolchain go1.23.0 - require ( github.com/goccy/go-json v0.10.3 github.com/hashicorp/go-multierror v1.1.1 @@ -105,7 +103,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.13 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/quic-go/qpack v0.4.0 // indirect @@ -132,11 +130,11 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.9.0 // indirect @@ -152,9 +150,9 @@ require ( golang.org/x/tools v0.24.0 // indirect google.golang.org/api v0.194.0 // indirect google.golang.org/appengine/v2 v2.0.6 // indirect - google.golang.org/genproto v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240823204242-4ba0660f739c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c // indirect google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 9bc6e93..300129e 100644 --- a/go.sum +++ b/go.sum @@ -264,8 +264,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= github.com/opencontainers/runc v1.1.13/go.mod h1:R016aXacfp/gwQBYw2FDGa9m+n6atbLWrYY8hNMT/sA= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -311,7 +311,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -347,18 +346,18 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -459,12 +458,12 @@ google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7 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-20240822170219-fc7c04adadcd h1:2IeVvc1/x7e+pVb40iz8/w2/c/fzmIlOp6ebkOJGw3M= -google.golang.org/genproto v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:JB1IzdOfYpNW7QBoS3aYEw5Zl2Q3OEeNWY/Nb99hSyk= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= -google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c h1:TYOEhrQMrNDTAd2rX9m+WgGr8Ku6YNuj1D7OX6rWSok= +google.golang.org/genproto v0.0.0-20240823204242-4ba0660f739c/go.mod h1:2rC5OendXvZ8wGEo/cSLheztrZDZaSoHanUcd1xtZnw= +google.golang.org/genproto/googleapis/api v0.0.0-20240823204242-4ba0660f739c h1:e0zB268kOca6FbuJkYUGxfwG4DKFZG/8DLyv9Zv66cE= +google.golang.org/genproto/googleapis/api v0.0.0-20240823204242-4ba0660f739c/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c h1:Kqjm4WpoWvwhMPcrAczoTyMySQmYa9Wy2iL6Con4zn8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 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= diff --git a/levels-and-roles/.testdata/application.yaml b/levels-and-roles/.testdata/application.yaml index 96e2234..3701b0b 100644 --- a/levels-and-roles/.testdata/application.yaml +++ b/levels-and-roles/.testdata/application.yaml @@ -5,6 +5,7 @@ logger: encoder: console level: info levels-and-roles: &levels-and-roles + tasksV2Enabled: false requiredInvitedFriendsToBecomeAmbassador: 3 roleNames: - Snowman diff --git a/levels-and-roles/completed_levels_and_activated_roles.go b/levels-and-roles/completed_levels_and_activated_roles.go index add8409..cb19b30 100644 --- a/levels-and-roles/completed_levels_and_activated_roles.go +++ b/levels-and-roles/completed_levels_and_activated_roles.go @@ -84,6 +84,14 @@ func (s *completedTasksSource) Process(ctx context.Context, msg *messagebroker.M return errors.Wrapf(s.upsertProgress(ctx, ct.CompletedTasks, ct.UserID), "failed to upsertProgress for completedTask:%#v", ct) } +func (r *repository) tasksLength() int { + if r.cfg.TasksV2Enabled { + return len(r.cfg.TasksList) + } + + return len(&tasks.AllTypes) +} + func (s *completedTasksSource) upsertProgress(ctx context.Context, completedTasks uint64, userID string) error { if ctx.Err() != nil { return errors.Wrap(ctx.Err(), "context failed") @@ -91,7 +99,7 @@ func (s *completedTasksSource) upsertProgress(ctx context.Context, completedTask pr, err := s.getProgress(ctx, userID, true) if (pr != nil && pr.CompletedLevels != nil && (len(*pr.CompletedLevels) == len(&AllLevelTypes))) || err != nil && !errors.Is(err, storage.ErrRelationNotFound) || - (pr != nil && (pr.CompletedTasks == uint64(len(&tasks.AllTypes)) || + (pr != nil && (pr.CompletedTasks == uint64(s.tasksLength()) || AreLevelsCompleted(pr.CompletedLevels, Level6Type, Level7Type, Level8Type, Level9Type, Level10Type, Level11Type))) { return errors.Wrapf(err, "failed to getProgress for userID:%v", userID) } diff --git a/levels-and-roles/contract.go b/levels-and-roles/contract.go index eef3c4e..5204458 100644 --- a/levels-and-roles/contract.go +++ b/levels-and-roles/contract.go @@ -176,12 +176,18 @@ type ( *repository } config struct { - MiningStreakMilestones map[LevelType]uint64 `yaml:"miningStreakMilestones"` - PingsSentMilestones map[LevelType]uint64 `yaml:"pingsSentMilestones"` - AgendaContactsJoinedMilestones map[LevelType]uint64 `yaml:"agendaContactsJoinedMilestones"` - CompletedTasksMilestones map[LevelType]uint64 `yaml:"completedTasksMilestones"` - RoleNames []RoleType `yaml:"roleNames"` + MiningStreakMilestones map[LevelType]uint64 `yaml:"miningStreakMilestones"` + PingsSentMilestones map[LevelType]uint64 `yaml:"pingsSentMilestones"` + AgendaContactsJoinedMilestones map[LevelType]uint64 `yaml:"agendaContactsJoinedMilestones"` + CompletedTasksMilestones map[LevelType]uint64 `yaml:"completedTasksMilestones"` + RoleNames []RoleType `yaml:"roleNames"` + TasksList []struct { + Type string `yaml:"type" mapstructure:"type"` + Icon string `yaml:"icon" mapstructure:"icon"` + Prize float64 `yaml:"prize" mapstructure:"prize"` + } `yaml:"tasksList" mapstructure:"tasksList"` messagebroker.Config `mapstructure:",squash"` //nolint:tagliatelle // Nope. RequiredInvitedFriendsToBecomeAmbassador uint64 `yaml:"requiredInvitedFriendsToBecomeAmbassador"` + TasksV2Enabled bool `yaml:"tasksV2Enabled" mapstructure:"tasksV2Enabled"` } ) diff --git a/tasks/.testdata/application.yaml b/tasks/.testdata/application.yaml index 2b350ea..837bb87 100644 --- a/tasks/.testdata/application.yaml +++ b/tasks/.testdata/application.yaml @@ -4,7 +4,27 @@ development: true logger: encoder: console level: info +tasksList: &tasksList + - type: claim_username + prize: 100 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/nikename.svg + - type: start_mining + prize: 200 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/mining.svg + - type: upload_profile_picture + prize: 300 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/profile-waiting.svg + - type: follow_us_on_twitter + prize: 400 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/twitter-follow.svg + - type: join_telegram + prize: 500 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/join-telegram-waiting.svg + - type: invite_friends + prize: 600 + icon: https://app.sunwavestoken.com/web/images/Components/Overview/invite.svg tasks: &tasks + tasksV2Enabled: false requiredFriendsInvited: 5 db: &tasksDatabase urls: @@ -47,6 +67,8 @@ tasks: &tasks - name: users-table - name: mining-sessions-table - name: friends-invited + tasksList: *tasksList + tenantName: sunwaves tasks_test: <<: *tasks messageBroker: diff --git a/tasks/complete_tasks.go b/tasks/complete_tasks.go index dc9c270..2d091f8 100644 --- a/tasks/complete_tasks.go +++ b/tasks/complete_tasks.go @@ -23,6 +23,9 @@ func (r *repository) PseudoCompleteTask(ctx context.Context, task *Task) error { if ctx.Err() != nil { return errors.Wrap(ctx.Err(), "unexpected deadline") } + if err := r.validateTask(task); err != nil { + return errors.Wrapf(err, "wrong type for:%+v", task) + } userProgress, err := r.getProgress(ctx, task.UserID, true) if err != nil && !errors.Is(err, ErrRelationNotFound) { return errors.Wrapf(err, "failed to getProgress for userID:%v", task.UserID) @@ -76,6 +79,32 @@ func (r *repository) PseudoCompleteTask(ctx context.Context, task *Task) error { return nil } +func (r *repository) validateTask(arg *Task) error { + if r.cfg.TasksV2Enabled { + for ix := range r.cfg.TasksList { + if Type(r.cfg.TasksList[ix].Type) == arg.Type { + return nil + } + } + } else { + for _, taskType := range &AllTypes { + if taskType == arg.Type { + return nil + } + } + } + + return errors.Errorf("invalid type `%v`", arg.Type) +} + +func (r *repository) tasksLength() int { + if r.cfg.TasksV2Enabled { + return len(r.cfg.TasksList) + } + + return len(&AllTypes) +} + func (p *progress) buildUpdatePseudoCompletedTasksSQL(task *Task, repo *repository) (params []any, sql string) { //nolint:funlen // . for _, tsk := range p.buildTasks(repo) { if tsk.Type == task.Type { @@ -84,14 +113,16 @@ func (p *progress) buildUpdatePseudoCompletedTasksSQL(task *Task, repo *reposito } } } - pseudoCompletedTasks := make(users.Enum[Type], 0, len(&AllTypes)) + pseudoCompletedTasks := make(users.Enum[Type], 0, repo.tasksLength()) if p.PseudoCompletedTasks != nil { pseudoCompletedTasks = append(pseudoCompletedTasks, *p.PseudoCompletedTasks...) } pseudoCompletedTasks = append(pseudoCompletedTasks, task.Type) - sort.SliceStable(pseudoCompletedTasks, func(i, j int) bool { - return TypeOrder[pseudoCompletedTasks[i]] < TypeOrder[pseudoCompletedTasks[j]] - }) + if !repo.cfg.TasksV2Enabled { + sort.SliceStable(pseudoCompletedTasks, func(i, j int) bool { + return TypeOrder[pseudoCompletedTasks[i]] < TypeOrder[pseudoCompletedTasks[j]] + }) + } params = make([]any, 0) params = append(params, task.UserID, p.PseudoCompletedTasks, &pseudoCompletedTasks) fieldIndexes := append(make([]string, 0, 1+1), "$3") @@ -142,7 +173,7 @@ func (r *repository) completeTasks(ctx context.Context, userID string) error { / pr = new(progress) pr.UserID = userID } - if pr.CompletedTasks != nil && len(*pr.CompletedTasks) == len(&AllTypes) { + if pr.CompletedTasks != nil && len(*pr.CompletedTasks) == r.tasksLength() { return nil } completedTasks := pr.reEvaluateCompletedTasks(r) @@ -169,7 +200,7 @@ func (r *repository) completeTasks(ctx context.Context, userID string) error { / } //nolint:nestif // . if completedTasks != nil && len(*completedTasks) > 0 && (pr.CompletedTasks == nil || len(*pr.CompletedTasks) < len(*completedTasks)) { - newlyCompletedTasks := make([]*CompletedTask, 0, len(&AllTypes)) + newlyCompletedTasks := make([]*CompletedTask, 0, r.tasksLength()) outer: for _, completedTask := range *completedTasks { if pr.CompletedTasks != nil { @@ -179,11 +210,24 @@ func (r *repository) completeTasks(ctx context.Context, userID string) error { / } } } - newlyCompletedTasks = append(newlyCompletedTasks, &CompletedTask{ - UserID: userID, - Type: completedTask, - CompletedTasks: uint64(len(*completedTasks)), - }) + if r.cfg.TasksV2Enabled { + for ix := range r.cfg.TasksList { + if completedTask == Type(r.cfg.TasksList[ix].Type) { + newlyCompletedTasks = append(newlyCompletedTasks, &CompletedTask{ + UserID: userID, + Type: completedTask, + CompletedTasks: uint64(len(*completedTasks)), + Prize: r.cfg.TasksList[ix].Prize, + }) + } + } + } else { + newlyCompletedTasks = append(newlyCompletedTasks, &CompletedTask{ + UserID: userID, + Type: completedTask, + CompletedTasks: uint64(len(*completedTasks)), + }) + } } if err = runConcurrently(ctx, r.sendCompletedTaskMessage, newlyCompletedTasks); err != nil { sErr := errors.Wrapf(err, "failed to sendCompletedTaskMessages for userID:%v,completedTasks:%#v", userID, newlyCompletedTasks) @@ -207,49 +251,38 @@ func (r *repository) completeTasks(ctx context.Context, userID string) error { / return nil } -func (p *progress) reEvaluateCompletedTasks(repo *repository) *users.Enum[Type] { //nolint:revive,funlen,gocognit,gocyclo,cyclop // . - if p.CompletedTasks != nil && len(*p.CompletedTasks) == len(&AllTypes) { +func (p *progress) reEvaluateCompletedTasks(repo *repository) *users.Enum[Type] { //nolint:revive,funlen,gocognit // . + if p.CompletedTasks != nil && len(*p.CompletedTasks) == repo.tasksLength() { return p.CompletedTasks } - alreadyCompletedTasks := make(map[Type]any, len(&AllTypes)) + alreadyCompletedTasks := make(map[Type]any, repo.tasksLength()) if p.CompletedTasks != nil { for _, task := range *p.CompletedTasks { alreadyCompletedTasks[task] = struct{}{} } } - completedTasks := make(users.Enum[Type], 0, len(&AllTypes)) - for ix, taskType := range &AllTypes { - if _, alreadyCompleted := alreadyCompletedTasks[taskType]; alreadyCompleted { - completedTasks = append(completedTasks, taskType) + completedTasks := make(users.Enum[Type], 0, repo.tasksLength()) + if repo.cfg.TasksV2Enabled { //nolint:nestif // . + for _, taskType := range &AllTypes { + if _, alreadyCompleted := alreadyCompletedTasks[taskType]; alreadyCompleted { + completedTasks = append(completedTasks, taskType) - continue - } - if len(completedTasks) != ix { - break - } - var completed bool - switch taskType { - case ClaimUsernameType: - completed = p.UsernameSet - case StartMiningType: - completed = p.MiningStarted - case UploadProfilePictureType: - completed = p.ProfilePictureSet - case FollowUsOnTwitterType: - if p.TwitterUserHandle != nil && *p.TwitterUserHandle != "" { - completed = true + continue } - case JoinTelegramType: - if p.TelegramUserHandle != nil && *p.TelegramUserHandle != "" { - completed = true - } - case InviteFriendsType: - if p.FriendsInvited >= repo.cfg.RequiredFriendsInvited { - completed = true + if val := p.gatherCompletedTasks(repo, taskType); val != "" { + completedTasks = append(completedTasks, val) } } - if completed { - completedTasks = append(completedTasks, taskType) + } else { + for ix := range repo.cfg.TasksList { + if _, alreadyCompleted := alreadyCompletedTasks[Type(repo.cfg.TasksList[ix].Type)]; alreadyCompleted { + completedTasks = append(completedTasks, Type(repo.cfg.TasksList[ix].Type)) + + continue + } + if val := p.gatherCompletedTasks(repo, Type(repo.cfg.TasksList[ix].Type)); val != "" { + completedTasks = append(completedTasks, val) + } } } if len(completedTasks) == 0 { @@ -259,6 +292,35 @@ func (p *progress) reEvaluateCompletedTasks(repo *repository) *users.Enum[Type] return &completedTasks } +func (p *progress) gatherCompletedTasks(repo *repository, taskType Type) Type { + var completed bool + switch taskType { + case ClaimUsernameType: + completed = p.UsernameSet + case StartMiningType: + completed = p.MiningStarted + case UploadProfilePictureType: + completed = p.ProfilePictureSet + case FollowUsOnTwitterType: + if p.TwitterUserHandle != nil && *p.TwitterUserHandle != "" { + completed = true + } + case JoinTelegramType: + if p.TelegramUserHandle != nil && *p.TelegramUserHandle != "" { + completed = true + } + case InviteFriendsType: + if p.FriendsInvited >= repo.cfg.RequiredFriendsInvited { + completed = true + } + } + if completed { + return taskType + } + + return "" +} + func (r *repository) sendCompletedTaskMessage(ctx context.Context, completedTask *CompletedTask) error { valueBytes, err := json.MarshalContext(ctx, completedTask) if err != nil { @@ -323,7 +385,7 @@ func (s *miningSessionSource) upsertProgress(ctx context.Context, userID string) return errors.Wrap(ctx.Err(), "context failed") } if pr, err := s.getProgress(ctx, userID, true); (pr != nil && pr.CompletedTasks != nil && - len(*pr.CompletedTasks) == len(&AllTypes)) || err != nil && !errors.Is(err, ErrRelationNotFound) { + len(*pr.CompletedTasks) == s.tasksLength()) || err != nil && !errors.Is(err, ErrRelationNotFound) { return errors.Wrapf(err, "failed to getProgress for userID:%v", userID) } sql := `INSERT INTO task_progress(user_id, mining_started) VALUES ($1, $2) diff --git a/tasks/contract.go b/tasks/contract.go index 882f720..1dd4aac 100644 --- a/tasks/contract.go +++ b/tasks/contract.go @@ -4,8 +4,9 @@ package tasks import ( "context" - _ "embed" + "embed" "io" + "text/template" "github.com/pkg/errors" @@ -28,6 +29,7 @@ const ( var ( ErrRelationNotFound = storage.ErrRelationNotFound ErrRaceCondition = errors.New("race condition") + //nolint:gochecknoglobals // It's just for more descriptive validation messages. AllTypes = [6]Type{ ClaimUsernameType, @@ -53,21 +55,31 @@ type ( Data struct { TwitterUserHandle string `json:"twitterUserHandle,omitempty" example:"jdoe2"` TelegramUserHandle string `json:"telegramUserHandle,omitempty" example:"jdoe1"` + VerificationCode string `json:"verificationCode,omitempty" example:"ABC"` RequiredQuantity uint64 `json:"requiredQuantity,omitempty" example:"3"` } + Metadata struct { + Title string `json:"title,omitempty" example:"Claim username"` + ShortDescription string `json:"shortDescription,omitempty" example:"Short description"` + LongDescription string `json:"longDescription,omitempty" example:"Long description"` + IconURL string `json:"iconUrl,omitempty" example:"https://app.ice.com/web/invite.svg"` + } Task struct { - Data *Data `json:"data,omitempty"` - UserID string `json:"userId,omitempty" swaggerignore:"true" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4"` - Type Type `json:"type" example:"claim_username"` - Completed bool `json:"completed" example:"false"` + Data *Data `json:"data,omitempty"` + Metadata *Metadata `json:"metadata,omitempty"` + UserID string `json:"userId,omitempty" swaggerignore:"true" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4"` + Type Type `json:"type" example:"claim_username"` + Prize float64 `json:"prize" example:"200.0"` + Completed bool `json:"completed" example:"false"` } CompletedTask struct { - UserID string `json:"userId" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4"` - Type Type `json:"type" example:"claim_username"` - CompletedTasks uint64 `json:"completedTasks,omitempty" example:"3"` + UserID string `json:"userId" example:"edfd8c02-75e0-4687-9ac2-1ce4723865c4"` + Type Type `json:"type" example:"claim_username"` + CompletedTasks uint64 `json:"completedTasks,omitempty" example:"3"` + Prize float64 `json:"prize,omitempty" example:"200"` } ReadRepository interface { - GetTasks(ctx context.Context, userID string) ([]*Task, error) + GetTasks(ctx context.Context, userID, languageCode string) ([]*Task, error) } WriteRepository interface { PseudoCompleteTask(ctx context.Context, task *Task) error @@ -88,16 +100,24 @@ type ( const ( applicationYamlKey = "tasks" + defaultLanguage = "en" ) // . var ( //go:embed DDL.sql ddl string + + //nolint:gochecknoglobals // Its loaded once at startup. + allTaskTemplates map[Type]map[languageCode]*taskTemplate + + //go:embed translations + translations embed.FS ) type ( - progress struct { + languageCode = string + progress struct { CompletedTasks *users.Enum[Type] `json:"completedTasks,omitempty" example:"claim_username,start_mining"` PseudoCompletedTasks *users.Enum[Type] `json:"pseudoCompletedTasks,omitempty" example:"claim_username,start_mining"` TwitterUserHandle *string `json:"twitterUserHandle,omitempty" example:"jdoe2"` @@ -108,6 +128,12 @@ type ( ProfilePictureSet bool `json:"profilePictureSet,omitempty" example:"true"` MiningStarted bool `json:"miningStarted,omitempty" example:"true"` } + taskTemplate struct { + title, shortDescription, longDescription *template.Template + Title string `json:"title"` //nolint:revive // That's intended. + ShortDescription string `json:"shortDescription"` //nolint:revive // That's intended. + LongDescription string `json:"longDescription"` //nolint:revive // That's intended. + } tryCompleteTasksCommandSource struct { *processor } @@ -131,7 +157,14 @@ type ( *repository } config struct { + TenantName string `yaml:"tenantName" mapstructure:"tenantName"` + TasksList []struct { + Type string `yaml:"type" mapstructure:"type"` + Icon string `yaml:"icon" mapstructure:"icon"` + Prize float64 `yaml:"prize" mapstructure:"prize"` + } `yaml:"tasksList" mapstructure:"tasksList"` messagebroker.Config `mapstructure:",squash"` //nolint:tagliatelle // Nope. RequiredFriendsInvited uint64 `yaml:"requiredFriendsInvited"` + TasksV2Enabled bool `yaml:"tasksV2Enabled" mapstructure:"tasksV2Enabled"` } ) diff --git a/tasks/get_tasks.go b/tasks/get_tasks.go index 2b37be7..03afdc7 100644 --- a/tasks/get_tasks.go +++ b/tasks/get_tasks.go @@ -10,7 +10,8 @@ import ( storage "github.com/ice-blockchain/wintr/connectors/storage/v2" ) -func (r *repository) GetTasks(ctx context.Context, userID string) (resp []*Task, err error) { +//nolint:funlen // . +func (r *repository) GetTasks(ctx context.Context, userID, language string) (resp []*Task, err error) { if ctx.Err() != nil { return nil, errors.Wrap(ctx.Err(), "unexpected deadline") } @@ -22,8 +23,26 @@ func (r *repository) GetTasks(ctx context.Context, userID string) (resp []*Task, return nil, errors.Wrapf(err, "failed to getProgress for userID:%v", userID) } + tasks := userProgress.buildTasks(r) + if r.cfg.TasksV2Enabled { + lang := language + if language == "" { + lang = defaultLanguage + } + for _, task := range tasks { + tmpl := allTaskTemplates[task.Type][lang] + if _, ok := allTaskTemplates[task.Type][lang]; !ok { + tmpl = allTaskTemplates[task.Type][lang] + } + task.Metadata = &Metadata{ + Title: tmpl.getTitle(nil), + ShortDescription: tmpl.getShortDescription(nil), + LongDescription: tmpl.getLongDescription(nil), + } + } + } - return userProgress.buildTasks(r), nil + return tasks, nil } //nolint:revive //. @@ -101,21 +120,44 @@ func (p *progress) reallyCompleted(task *Task) bool { return reallyCompleted } +//nolint:funlen // . func (r *repository) defaultTasks() (resp []*Task) { - resp = make([]*Task, 0, len(&AllTypes)) - for _, taskType := range &AllTypes { - var ( - data *Data - completed bool - ) - switch taskType { //nolint:exhaustive // We care only about those. - case ClaimUsernameType: - completed = true // To make sure network latency doesn't affect UX. - case InviteFriendsType: - data = &Data{RequiredQuantity: r.cfg.RequiredFriendsInvited} + if r.cfg.TasksV2Enabled { + resp = make([]*Task, 0, len(r.cfg.TasksList)) + for ix := range r.cfg.TasksList { + var ( + data *Data + completed bool + ) + switch Type(r.cfg.TasksList[ix].Type) { //nolint:exhaustive // We care only about those. + case ClaimUsernameType: + completed = true // To make sure network latency doesn't affect UX. + case InviteFriendsType: + data = &Data{RequiredQuantity: r.cfg.RequiredFriendsInvited} + } + resp = append(resp, &Task{ + Data: data, + Type: Type(r.cfg.TasksList[ix].Type), + Completed: completed, + Prize: r.cfg.TasksList[ix].Prize, + }) + } + } else { + resp = make([]*Task, 0, len(&AllTypes)) + for _, taskType := range &AllTypes { + var ( + data *Data + completed bool + ) + switch taskType { //nolint:exhaustive // We care only about those. + case ClaimUsernameType: + completed = true // To make sure network latency doesn't affect UX. + case InviteFriendsType: + data = &Data{RequiredQuantity: r.cfg.RequiredFriendsInvited} + } + resp = append(resp, &Task{Data: data, Type: taskType, Completed: completed}) } - resp = append(resp, &Task{Data: data, Type: taskType, Completed: completed}) } - return + return resp } diff --git a/tasks/tasks.go b/tasks/tasks.go index 61580ac..5a1b2ca 100644 --- a/tasks/tasks.go +++ b/tasks/tasks.go @@ -3,8 +3,12 @@ package tasks import ( + "bytes" "context" + "fmt" + "strings" "sync" + "text/template" "github.com/goccy/go-json" "github.com/hashicorp/go-multierror" @@ -14,6 +18,7 @@ import ( appcfg "github.com/ice-blockchain/wintr/config" messagebroker "github.com/ice-blockchain/wintr/connectors/message_broker" storage "github.com/ice-blockchain/wintr/connectors/storage/v2" + "github.com/ice-blockchain/wintr/log" "github.com/ice-blockchain/wintr/time" ) @@ -48,6 +53,9 @@ func StartProcessor(ctx context.Context, cancel context.CancelFunc) Processor { &friendsInvitedSource{processor: prc}, ) prc.shutdown = closeAll(mbConsumer, prc.mb, prc.db) + if cfg.TasksV2Enabled { + prc.repository.loadTaskTranslationTemplates(cfg.TenantName) + } return prc } @@ -81,7 +89,7 @@ func (p *processor) CheckHealth(ctx context.Context) error { TS *time.Time `json:"ts"` } now := ts{TS: time.Now()} - bytes, err := json.MarshalContext(ctx, now) + val, err := json.MarshalContext(ctx, now) if err != nil { return errors.Wrapf(err, "[health-check] failed to marshal %#v", now) } @@ -90,7 +98,7 @@ func (p *processor) CheckHealth(ctx context.Context) error { Headers: map[string]string{"producer": "santa"}, Key: p.cfg.MessageBroker.Topics[0].Name, Topic: p.cfg.MessageBroker.Topics[0].Name, - Value: bytes, + Value: val, }, responder) return errors.Wrapf(<-responder, "[health-check] failed to send health check message to broker") @@ -145,3 +153,60 @@ func AreTasksCompleted(actual *users.Enum[Type], expectedSubset ...Type) bool { return true } + +func (r *repository) loadTaskTranslationTemplates(tenantName string) { + const totalLanguages = 50 + allTaskTemplates = make(map[Type]map[languageCode]*taskTemplate, len(r.cfg.TasksList)) + for ix := range r.cfg.TasksList { + content, fErr := translations.ReadFile(fmt.Sprintf("translations/%v/%v.txt", strings.ToLower(tenantName), r.cfg.TasksList[ix].Type)) + log.Panic(fErr) //nolint:revive // Wrong. + allTaskTemplates[Type(r.cfg.TasksList[ix].Type)] = make(map[languageCode]*taskTemplate, totalLanguages) + var languageData map[string]*struct { + Title string `json:"title"` + ShortDescription string `json:"shortDescription"` + LongDescription string `json:"longDescription"` + } + log.Panic(json.Unmarshal(content, &languageData)) + for language, data := range languageData { + var tmpl taskTemplate + tmpl.ShortDescription = data.ShortDescription + tmpl.LongDescription = data.LongDescription + tmpl.Title = data.Title + tmpl.title = template.Must(template.New(fmt.Sprintf("task_%v_%v_title", r.cfg.TasksList[ix].Type, language)).Parse(data.Title)) + tmpl.shortDescription = template.Must(template.New(fmt.Sprintf("task_%v_%v_short_description", r.cfg.TasksList[ix].Type, language)).Parse(data.ShortDescription)) //nolint:lll // . + tmpl.longDescription = template.Must(template.New(fmt.Sprintf("task_%v_%v_ling_description", r.cfg.TasksList[ix].Type, language)).Parse(data.LongDescription)) //nolint:lll // . + + allTaskTemplates[Type(r.cfg.TasksList[ix].Type)][language] = &tmpl + } + } +} + +func (t *taskTemplate) getTitle(data any) string { + if data == nil { + return t.Title + } + bf := new(bytes.Buffer) + log.Panic(errors.Wrapf(t.title.Execute(bf, data), "failed to execute title template for data:%#v", data)) + + return bf.String() +} + +func (t *taskTemplate) getShortDescription(data any) string { + if data == nil { + return t.ShortDescription + } + bf := new(bytes.Buffer) + log.Panic(errors.Wrapf(t.shortDescription.Execute(bf, data), "failed to execute short description template for data:%#v", data)) + + return bf.String() +} + +func (t *taskTemplate) getLongDescription(data any) string { + if data == nil { + return t.LongDescription + } + bf := new(bytes.Buffer) + log.Panic(errors.Wrapf(t.longDescription.Execute(bf, data), "failed to execute long description template for data:%#v", data)) + + return bf.String() +} diff --git a/tasks/translations/callfluent/claim_username.txt b/tasks/translations/callfluent/claim_username.txt new file mode 100644 index 0000000..3b9a7fc --- /dev/null +++ b/tasks/translations/callfluent/claim_username.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Claim your nickname", + "shortDescription":"Set it and start earning from your invite.", + "longDescription":"Set it and start earning from your invite." + } +} \ No newline at end of file diff --git a/tasks/translations/callfluent/follow_us_on_twitter.txt b/tasks/translations/callfluent/follow_us_on_twitter.txt new file mode 100644 index 0000000..81e007c --- /dev/null +++ b/tasks/translations/callfluent/follow_us_on_twitter.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Follow us on X", + "shortDescription":"Let's keep in touch, follow us on X.", + "longDescription":"Let's keep in touch, follow us on X." + } +} \ No newline at end of file diff --git a/tasks/translations/callfluent/invite_friends.txt b/tasks/translations/callfluent/invite_friends.txt new file mode 100644 index 0000000..a89b547 --- /dev/null +++ b/tasks/translations/callfluent/invite_friends.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Invite 5 friends", + "shortDescription":"Create your team and increase your earnings.", + "longDescription":"Create your team and increase your earnings." + } +} \ No newline at end of file diff --git a/tasks/translations/callfluent/join_telegram.txt b/tasks/translations/callfluent/join_telegram.txt new file mode 100644 index 0000000..01fcf29 --- /dev/null +++ b/tasks/translations/callfluent/join_telegram.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Join telegram", + "shortDescription":"Be part of our community and join now.", + "longDescription":"Be part of our community and join now." + } +} \ No newline at end of file diff --git a/tasks/translations/callfluent/start_mining.txt b/tasks/translations/callfluent/start_mining.txt new file mode 100644 index 0000000..005f82e --- /dev/null +++ b/tasks/translations/callfluent/start_mining.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Start mining", + "shortDescription":"Your first check-in session.", + "longDescription":"Your first check-in session." + } +} \ No newline at end of file diff --git a/tasks/translations/callfluent/upload_profile_picture.txt b/tasks/translations/callfluent/upload_profile_picture.txt new file mode 100644 index 0000000..af6c101 --- /dev/null +++ b/tasks/translations/callfluent/upload_profile_picture.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Upload profile picture", + "shortDescription":"Upload your profile image.", + "longDescription":"Upload your profile image." + } +} \ No newline at end of file diff --git a/tasks/translations/sealsend/claim_username.txt b/tasks/translations/sealsend/claim_username.txt new file mode 100644 index 0000000..3b9a7fc --- /dev/null +++ b/tasks/translations/sealsend/claim_username.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Claim your nickname", + "shortDescription":"Set it and start earning from your invite.", + "longDescription":"Set it and start earning from your invite." + } +} \ No newline at end of file diff --git a/tasks/translations/sealsend/follow_us_on_twitter.txt b/tasks/translations/sealsend/follow_us_on_twitter.txt new file mode 100644 index 0000000..81e007c --- /dev/null +++ b/tasks/translations/sealsend/follow_us_on_twitter.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Follow us on X", + "shortDescription":"Let's keep in touch, follow us on X.", + "longDescription":"Let's keep in touch, follow us on X." + } +} \ No newline at end of file diff --git a/tasks/translations/sealsend/invite_friends.txt b/tasks/translations/sealsend/invite_friends.txt new file mode 100644 index 0000000..a89b547 --- /dev/null +++ b/tasks/translations/sealsend/invite_friends.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Invite 5 friends", + "shortDescription":"Create your team and increase your earnings.", + "longDescription":"Create your team and increase your earnings." + } +} \ No newline at end of file diff --git a/tasks/translations/sealsend/join_telegram.txt b/tasks/translations/sealsend/join_telegram.txt new file mode 100644 index 0000000..01fcf29 --- /dev/null +++ b/tasks/translations/sealsend/join_telegram.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Join telegram", + "shortDescription":"Be part of our community and join now.", + "longDescription":"Be part of our community and join now." + } +} \ No newline at end of file diff --git a/tasks/translations/sealsend/start_mining.txt b/tasks/translations/sealsend/start_mining.txt new file mode 100644 index 0000000..005f82e --- /dev/null +++ b/tasks/translations/sealsend/start_mining.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Start mining", + "shortDescription":"Your first check-in session.", + "longDescription":"Your first check-in session." + } +} \ No newline at end of file diff --git a/tasks/translations/sealsend/upload_profile_picture.txt b/tasks/translations/sealsend/upload_profile_picture.txt new file mode 100644 index 0000000..af6c101 --- /dev/null +++ b/tasks/translations/sealsend/upload_profile_picture.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Upload profile picture", + "shortDescription":"Upload your profile image.", + "longDescription":"Upload your profile image." + } +} \ No newline at end of file diff --git a/tasks/translations/sunwaves/claim_username.txt b/tasks/translations/sunwaves/claim_username.txt new file mode 100644 index 0000000..3b9a7fc --- /dev/null +++ b/tasks/translations/sunwaves/claim_username.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Claim your nickname", + "shortDescription":"Set it and start earning from your invite.", + "longDescription":"Set it and start earning from your invite." + } +} \ No newline at end of file diff --git a/tasks/translations/sunwaves/follow_us_on_twitter.txt b/tasks/translations/sunwaves/follow_us_on_twitter.txt new file mode 100644 index 0000000..81e007c --- /dev/null +++ b/tasks/translations/sunwaves/follow_us_on_twitter.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Follow us on X", + "shortDescription":"Let's keep in touch, follow us on X.", + "longDescription":"Let's keep in touch, follow us on X." + } +} \ No newline at end of file diff --git a/tasks/translations/sunwaves/invite_friends.txt b/tasks/translations/sunwaves/invite_friends.txt new file mode 100644 index 0000000..a89b547 --- /dev/null +++ b/tasks/translations/sunwaves/invite_friends.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Invite 5 friends", + "shortDescription":"Create your team and increase your earnings.", + "longDescription":"Create your team and increase your earnings." + } +} \ No newline at end of file diff --git a/tasks/translations/sunwaves/join_telegram.txt b/tasks/translations/sunwaves/join_telegram.txt new file mode 100644 index 0000000..01fcf29 --- /dev/null +++ b/tasks/translations/sunwaves/join_telegram.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Join telegram", + "shortDescription":"Be part of our community and join now.", + "longDescription":"Be part of our community and join now." + } +} \ No newline at end of file diff --git a/tasks/translations/sunwaves/start_mining.txt b/tasks/translations/sunwaves/start_mining.txt new file mode 100644 index 0000000..005f82e --- /dev/null +++ b/tasks/translations/sunwaves/start_mining.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Start mining", + "shortDescription":"Your first check-in session.", + "longDescription":"Your first check-in session." + } +} \ No newline at end of file diff --git a/tasks/translations/sunwaves/upload_profile_picture.txt b/tasks/translations/sunwaves/upload_profile_picture.txt new file mode 100644 index 0000000..af6c101 --- /dev/null +++ b/tasks/translations/sunwaves/upload_profile_picture.txt @@ -0,0 +1,7 @@ +{ + "en":{ + "title":"Upload profile picture", + "shortDescription":"Upload your profile image.", + "longDescription":"Upload your profile image." + } +} \ No newline at end of file