From 543c1b290604fc753e3147ae5a9624a0e0c7c222 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Tue, 27 Jun 2023 16:39:37 +0700 Subject: [PATCH 1/4] feat: get employee location (#628) --- pkg/controller/employee/list.go | 9 +++++++ pkg/controller/employee/new.go | 1 + pkg/handler/employee/employee.go | 29 +++++++++++++++++++++ pkg/handler/employee/interface.go | 1 + pkg/routes/v1.go | 10 +++++++ pkg/routes/v1_test.go | 6 +++++ pkg/store/employee/employee.go | 11 ++++++++ pkg/store/employee/interface.go | 1 + pkg/view/employee.go | 43 +++++++++++++++++++++++++++++++ 9 files changed, 111 insertions(+) diff --git a/pkg/controller/employee/list.go b/pkg/controller/employee/list.go index 40a0976b0..4ac05a578 100644 --- a/pkg/controller/employee/list.go +++ b/pkg/controller/employee/list.go @@ -57,3 +57,12 @@ func (r *controller) List(workingStatuses []string, body GetListEmployeeInput, u return employees, total, nil } + +func (r *controller) ListWithLocation() ([]*model.Employee, error) { + employees, err := r.store.Employee.SimpleList(r.repo.DB()) + if err != nil { + return nil, err + } + + return employees, nil +} diff --git a/pkg/controller/employee/new.go b/pkg/controller/employee/new.go index 58871da2a..9bb3d0ff6 100644 --- a/pkg/controller/employee/new.go +++ b/pkg/controller/employee/new.go @@ -40,4 +40,5 @@ type IController interface { UpdateRole(userID string, input UpdateRoleInput) (err error) GetLineManagers(userInfo *model.CurrentLoggedUserInfo) (employees []*model.Employee, err error) UpdateBaseSalary(l logger.Logger, employeeID string, body UpdateBaseSalaryInput) (employee *model.BaseSalary, err error) + ListWithLocation() (employees []*model.Employee, err error) } diff --git a/pkg/handler/employee/employee.go b/pkg/handler/employee/employee.go index 6f6574637..dfcc52794 100644 --- a/pkg/handler/employee/employee.go +++ b/pkg/handler/employee/employee.go @@ -754,3 +754,32 @@ func (h *handler) UpdateBaseSalary(c *gin.Context) { c.JSON(http.StatusOK, view.CreateResponse[any](view.ToBaseSalary(emp), nil, nil, nil, "")) } + +// ListWithLocation godoc +// @Summary Get employees list with location +// @Description Get employees list with location +// @Tags Employee +// @Accept json +// @Produce json +// @Param Authorization header string true "jwt token" +// @Param Body body request.UpdateBaseSalaryInput true "Body" +// @Success 200 {object} view.UpdateBaseSalaryResponse +// @Failure 400 {object} view.ErrorResponse +// @Failure 404 {object} view.ErrorResponse +// @Failure 500 {object} view.ErrorResponse +// @Router /public/employees [get] +func (h *handler) ListWithLocation(c *gin.Context) { + l := h.logger.Fields(logger.Fields{ + "handler": "employee", + "method": "ListWithLocation", + }) + + employees, err := h.controller.Employee.ListWithLocation() + if err != nil { + l.Error(err, "failed to list employees") + errs.ConvertControllerErr(c, err) + return + } + + c.JSON(http.StatusOK, view.CreateResponse[any](view.ToEmployeesWithLocation(employees), nil, nil, nil, "")) +} diff --git a/pkg/handler/employee/interface.go b/pkg/handler/employee/interface.go index 8209e55aa..5aa416fc4 100644 --- a/pkg/handler/employee/interface.go +++ b/pkg/handler/employee/interface.go @@ -7,6 +7,7 @@ type IHandler interface { Details(c *gin.Context) GetLineManagers(c *gin.Context) List(c *gin.Context) + ListWithLocation(c *gin.Context) UpdateEmployeeStatus(c *gin.Context) UpdateGeneralInfo(c *gin.Context) UpdateSkills(c *gin.Context) diff --git a/pkg/routes/v1.go b/pkg/routes/v1.go index 8bdd7e8aa..e5e2ee6dd 100644 --- a/pkg/routes/v1.go +++ b/pkg/routes/v1.go @@ -328,4 +328,14 @@ func loadV1Routes(r *gin.Engine, h *handler.Handler, repo store.DBRepo, s *store braineryGroup.GET("/metrics", amw.WithAuth, pmw.WithPerm(model.PermissionBraineryLogsRead), h.BraineryLog.GetMetrics) braineryGroup.POST("/sync", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.BraineryLog.Sync) } + + ///////////////// + // PUBLIC API GROUP + ///////////////// + + // assets + publicGroup := v1.Group("/public") + { + publicGroup.GET("/employees", h.Employee.ListWithLocation) + } } diff --git a/pkg/routes/v1_test.go b/pkg/routes/v1_test.go index 616f01e13..8a81aba2a 100644 --- a/pkg/routes/v1_test.go +++ b/pkg/routes/v1_test.go @@ -817,6 +817,12 @@ func Test_loadV1Routes(t *testing.T) { Handler: "github.com/dwarvesf/fortress-api/pkg/handler/brainerylogs.IHandler.Sync-fm", }, }, + "/api/v1/public/employees": { + "GET": { + Method: "GET", + Handler: "github.com/dwarvesf/fortress-api/pkg/handler/employee.IHandler.ListWithLocation-fm", + }, + }, } l := logger.NewLogrusLogger() diff --git a/pkg/store/employee/employee.go b/pkg/store/employee/employee.go index b67e8b16d..0a19863f9 100644 --- a/pkg/store/employee/employee.go +++ b/pkg/store/employee/employee.go @@ -242,3 +242,14 @@ func (s *store) GetByDiscordID(db *gorm.DB, discordID string) (*model.Employee, var employee *model.Employee return employee, db.Joins("JOIN discord_accounts ON discord_accounts.id = employees.discord_account_id AND discord_accounts.discord_id = ?", discordID).Order("created_at").First(&employee).Error } + +// SimpleList get employees by query and pagination +func (s *store) SimpleList(db *gorm.DB) ([]*model.Employee, error) { + var employees []*model.Employee + + query := db.Where("deleted_at IS NULL AND working_status <> ?", model.WorkingStatusLeft). + Order("created_at"). + Preload("DiscordAccount", "deleted_at IS NULL") + + return employees, query.Find(&employees).Error +} diff --git a/pkg/store/employee/interface.go b/pkg/store/employee/interface.go index 4efc312a6..6d80f9ab9 100644 --- a/pkg/store/employee/interface.go +++ b/pkg/store/employee/interface.go @@ -22,6 +22,7 @@ type IStore interface { GetLineManagersOfPeers(db *gorm.DB, employeeID string) ([]*model.Employee, error) GetMenteesByID(db *gorm.DB, employeeID string) ([]*model.Employee, error) GetByDiscordID(db *gorm.DB, discordID string) (*model.Employee, error) + SimpleList(db *gorm.DB) ([]*model.Employee, error) IsExist(db *gorm.DB, id string) (bool, error) diff --git a/pkg/view/employee.go b/pkg/view/employee.go index c4b21d951..657582cbb 100644 --- a/pkg/view/employee.go +++ b/pkg/view/employee.go @@ -663,3 +663,46 @@ func ToBasicEmployeeInvitationData(in *model.EmployeeInvitation) *EmployeeInvita return rs } + +type EmployeeLocationListResponse struct { + Data []EmployeeLocation `json:"data"` +} + +type EmployeeLocation struct { + DiscordID string `json:"discordID"` + FullName string `json:"fullName"` + DisplayName string `json:"displayName"` + Avatar string `json:"avatar"` + Address EmployeeAddress `json:"address"` +} +type EmployeeAddress struct { + Address string `json:"address"` + Country string `json:"country"` + City string `json:"city"` + Lat string `json:"lat"` + Long string `json:"long"` +} + +func ToEmployeesWithLocation(in []*model.Employee) []EmployeeLocation { + rs := make([]EmployeeLocation, len(in)) + for i, v := range in { + discordID := "" + if v.DiscordAccount != nil { + discordID = v.DiscordAccount.DiscordID + } + rs[i] = EmployeeLocation{ + DiscordID: discordID, + FullName: v.FullName, + DisplayName: v.DisplayName, + Avatar: v.Avatar, + Address: EmployeeAddress{ + Address: v.City + ", " + v.Country, + Country: v.Country, + City: v.City, + Lat: v.Lat, + Long: v.Long, + }, + } + } + return rs +} From 4cb52cf288cfa0de738959439a81031ef63d3ef7 Mon Sep 17 00:00:00 2001 From: namnhce Date: Tue, 27 Jun 2023 19:53:26 +0700 Subject: [PATCH 2/4] chore: return chapter data in emp loc resp --- docs/docs.go | 239 ++++++++++++++++++++++++++++++- docs/swagger.json | 239 ++++++++++++++++++++++++++++++- docs/swagger.yaml | 157 +++++++++++++++++++- pkg/handler/employee/employee.go | 4 +- pkg/store/employee/employee.go | 4 +- pkg/view/employee.go | 2 + 6 files changed, 638 insertions(+), 7 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 7f9095a90..7b3bfb594 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -306,6 +306,62 @@ const docTemplate = `{ } ], "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/view.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + } + } + } + }, + "/brainery-logs/metrics": { + "get": { + "description": "Get brainery metric", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get brainery metric", + "parameters": [ + { + "type": "string", + "description": "jwt token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Time view", + "name": "view", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/view.BraineryMetric" + } + }, "400": { "description": "Bad Request", "schema": { @@ -5508,6 +5564,47 @@ const docTemplate = `{ } } }, + "/public/employees": { + "get": { + "description": "Get employees list with location", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Employee" + ], + "summary": "Get employees list with location", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/view.EmployeeLocationListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + } + } + } + }, "/surveys": { "get": { "description": "Get list event", @@ -6345,6 +6442,20 @@ const docTemplate = `{ } } }, + "model.City": { + "type": "object", + "properties": { + "lat": { + "type": "string" + }, + "long": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "model.Client": { "type": "object", "properties": { @@ -6499,7 +6610,7 @@ const docTemplate = `{ "cities": { "type": "array", "items": { - "type": "integer" + "$ref": "#/definitions/model.City" } }, "code": { @@ -9855,6 +9966,41 @@ const docTemplate = `{ } } }, + "view.BraineryMetric": { + "type": "object", + "properties": { + "contributors": { + "type": "array", + "items": { + "$ref": "#/definitions/view.Post" + } + }, + "latestPosts": { + "type": "array", + "items": { + "$ref": "#/definitions/view.Post" + } + }, + "newContributors": { + "type": "array", + "items": { + "$ref": "#/definitions/view.Post" + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "topContributors": { + "type": "array", + "items": { + "$ref": "#/definitions/view.TopContributor" + } + } + } + }, "view.Chapter": { "type": "object", "properties": { @@ -10265,6 +10411,26 @@ const docTemplate = `{ } } }, + "view.EmployeeAddress": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "lat": { + "type": "string" + }, + "long": { + "type": "string" + } + } + }, "view.EmployeeContentData": { "type": "object", "properties": { @@ -10492,6 +10658,43 @@ const docTemplate = `{ } } }, + "view.EmployeeLocation": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/view.EmployeeAddress" + }, + "avatar": { + "type": "string" + }, + "chapters": { + "type": "array", + "items": { + "$ref": "#/definitions/view.Chapter" + } + }, + "discordID": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "fullName": { + "type": "string" + } + } + }, + "view.EmployeeLocationListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/view.EmployeeLocation" + } + } + } + }, "view.EmployeeProjectData": { "type": "object", "properties": { @@ -11422,6 +11625,26 @@ const docTemplate = `{ } } }, + "view.Post": { + "type": "object", + "properties": { + "discordID": { + "type": "string" + }, + "publishedAt": { + "type": "string" + }, + "reward": { + "type": "number" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "view.ProfileData": { "type": "object", "properties": { @@ -12139,6 +12362,20 @@ const docTemplate = `{ } } }, + "view.TopContributor": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "discordID": { + "type": "string" + }, + "ranking": { + "type": "integer" + } + } + }, "view.Topic": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 038746f0a..fc6ad01e9 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -298,6 +298,62 @@ } ], "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/view.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + } + } + } + }, + "/brainery-logs/metrics": { + "get": { + "description": "Get brainery metric", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Project" + ], + "summary": "Get brainery metric", + "parameters": [ + { + "type": "string", + "description": "jwt token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Time view", + "name": "view", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/view.BraineryMetric" + } + }, "400": { "description": "Bad Request", "schema": { @@ -5500,6 +5556,47 @@ } } }, + "/public/employees": { + "get": { + "description": "Get employees list with location", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Employee" + ], + "summary": "Get employees list with location", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/view.EmployeeLocationListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/view.ErrorResponse" + } + } + } + } + }, "/surveys": { "get": { "description": "Get list event", @@ -6337,6 +6434,20 @@ } } }, + "model.City": { + "type": "object", + "properties": { + "lat": { + "type": "string" + }, + "long": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "model.Client": { "type": "object", "properties": { @@ -6491,7 +6602,7 @@ "cities": { "type": "array", "items": { - "type": "integer" + "$ref": "#/definitions/model.City" } }, "code": { @@ -9847,6 +9958,41 @@ } } }, + "view.BraineryMetric": { + "type": "object", + "properties": { + "contributors": { + "type": "array", + "items": { + "$ref": "#/definitions/view.Post" + } + }, + "latestPosts": { + "type": "array", + "items": { + "$ref": "#/definitions/view.Post" + } + }, + "newContributors": { + "type": "array", + "items": { + "$ref": "#/definitions/view.Post" + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "topContributors": { + "type": "array", + "items": { + "$ref": "#/definitions/view.TopContributor" + } + } + } + }, "view.Chapter": { "type": "object", "properties": { @@ -10257,6 +10403,26 @@ } } }, + "view.EmployeeAddress": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "lat": { + "type": "string" + }, + "long": { + "type": "string" + } + } + }, "view.EmployeeContentData": { "type": "object", "properties": { @@ -10484,6 +10650,43 @@ } } }, + "view.EmployeeLocation": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/view.EmployeeAddress" + }, + "avatar": { + "type": "string" + }, + "chapters": { + "type": "array", + "items": { + "$ref": "#/definitions/view.Chapter" + } + }, + "discordID": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "fullName": { + "type": "string" + } + } + }, + "view.EmployeeLocationListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/view.EmployeeLocation" + } + } + } + }, "view.EmployeeProjectData": { "type": "object", "properties": { @@ -11414,6 +11617,26 @@ } } }, + "view.Post": { + "type": "object", + "properties": { + "discordID": { + "type": "string" + }, + "publishedAt": { + "type": "string" + }, + "reward": { + "type": "number" + }, + "title": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, "view.ProfileData": { "type": "object", "properties": { @@ -12131,6 +12354,20 @@ } } }, + "view.TopContributor": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "discordID": { + "type": "string" + }, + "ranking": { + "type": "integer" + } + } + }, "view.Topic": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 89c6d235e..091bbd512 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -102,6 +102,15 @@ definitions: updatedAt: type: string type: object + model.City: + properties: + lat: + type: string + long: + type: string + name: + type: string + type: object model.Client: properties: address: @@ -203,7 +212,7 @@ definitions: properties: cities: items: - type: integer + $ref: '#/definitions/model.City' type: array code: type: string @@ -2444,6 +2453,29 @@ definitions: type: type: string type: object + view.BraineryMetric: + properties: + contributors: + items: + $ref: '#/definitions/view.Post' + type: array + latestPosts: + items: + $ref: '#/definitions/view.Post' + type: array + newContributors: + items: + $ref: '#/definitions/view.Post' + type: array + tags: + items: + type: string + type: array + topContributors: + items: + $ref: '#/definitions/view.TopContributor' + type: array + type: object view.Chapter: properties: code: @@ -2711,6 +2743,19 @@ definitions: name: type: string type: object + view.EmployeeAddress: + properties: + address: + type: string + city: + type: string + country: + type: string + lat: + type: string + long: + type: string + type: object view.EmployeeContentData: properties: url: @@ -2861,6 +2906,30 @@ definitions: $ref: '#/definitions/view.EmployeeData' type: array type: object + view.EmployeeLocation: + properties: + address: + $ref: '#/definitions/view.EmployeeAddress' + avatar: + type: string + chapters: + items: + $ref: '#/definitions/view.Chapter' + type: array + discordID: + type: string + displayName: + type: string + fullName: + type: string + type: object + view.EmployeeLocationListResponse: + properties: + data: + items: + $ref: '#/definitions/view.EmployeeLocation' + type: array + type: object view.EmployeeProjectData: properties: avatar: @@ -3465,6 +3534,19 @@ definitions: $ref: '#/definitions/model.Position' type: array type: object + view.Post: + properties: + discordID: + type: string + publishedAt: + type: string + reward: + type: number + title: + type: string + url: + type: string + type: object view.ProfileData: properties: address: @@ -3933,6 +4015,15 @@ definitions: data: $ref: '#/definitions/view.SurveyTopicDetail' type: object + view.TopContributor: + properties: + count: + type: integer + discordID: + type: string + ranking: + type: integer + type: object view.Topic: properties: comments: @@ -4616,6 +4707,10 @@ paths: produces: - application/json responses: + "200": + description: OK + schema: + $ref: '#/definitions/view.MessageResponse' "400": description: Bad Request schema: @@ -4627,6 +4722,39 @@ paths: summary: Create brainery logs tags: - Project + /brainery-logs/metrics: + get: + consumes: + - application/json + description: Get brainery metric + parameters: + - description: jwt token + in: header + name: Authorization + required: true + type: string + - description: Time view + in: query + name: view + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/view.BraineryMetric' + "400": + description: Bad Request + schema: + $ref: '#/definitions/view.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/view.ErrorResponse' + summary: Get brainery metric + tags: + - Project /clients: get: consumes: @@ -8071,6 +8199,33 @@ paths: summary: Get Icy Weekly Distribution tags: - Project + /public/employees: + get: + consumes: + - application/json + description: Get employees list with location + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/view.EmployeeLocationListResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/view.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/view.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/view.ErrorResponse' + summary: Get employees list with location + tags: + - Employee /surveys: get: consumes: diff --git a/pkg/handler/employee/employee.go b/pkg/handler/employee/employee.go index dfcc52794..697d7d3ff 100644 --- a/pkg/handler/employee/employee.go +++ b/pkg/handler/employee/employee.go @@ -761,9 +761,7 @@ func (h *handler) UpdateBaseSalary(c *gin.Context) { // @Tags Employee // @Accept json // @Produce json -// @Param Authorization header string true "jwt token" -// @Param Body body request.UpdateBaseSalaryInput true "Body" -// @Success 200 {object} view.UpdateBaseSalaryResponse +// @Success 200 {object} view.EmployeeLocationListResponse // @Failure 400 {object} view.ErrorResponse // @Failure 404 {object} view.ErrorResponse // @Failure 500 {object} view.ErrorResponse diff --git a/pkg/store/employee/employee.go b/pkg/store/employee/employee.go index 0a19863f9..bff6286f7 100644 --- a/pkg/store/employee/employee.go +++ b/pkg/store/employee/employee.go @@ -249,7 +249,9 @@ func (s *store) SimpleList(db *gorm.DB) ([]*model.Employee, error) { query := db.Where("deleted_at IS NULL AND working_status <> ?", model.WorkingStatusLeft). Order("created_at"). - Preload("DiscordAccount", "deleted_at IS NULL") + Preload("DiscordAccount", "deleted_at IS NULL"). + Preload("EmployeeChapters", "deleted_at IS NULL"). + Preload("EmployeeChapters.Chapter", "deleted_at IS NULL") return employees, query.Find(&employees).Error } diff --git a/pkg/view/employee.go b/pkg/view/employee.go index 657582cbb..bb971091d 100644 --- a/pkg/view/employee.go +++ b/pkg/view/employee.go @@ -673,6 +673,7 @@ type EmployeeLocation struct { FullName string `json:"fullName"` DisplayName string `json:"displayName"` Avatar string `json:"avatar"` + Chapters []Chapter `json:"chapters"` Address EmployeeAddress `json:"address"` } type EmployeeAddress struct { @@ -695,6 +696,7 @@ func ToEmployeesWithLocation(in []*model.Employee) []EmployeeLocation { FullName: v.FullName, DisplayName: v.DisplayName, Avatar: v.Avatar, + Chapters: ToChapters(v.EmployeeChapters), Address: EmployeeAddress{ Address: v.City + ", " + v.Country, Country: v.Country, From 4a688700096491e1a8d6750f9b2de48cf846febd Mon Sep 17 00:00:00 2001 From: Thomas Nguyen <51191083+thangnt294@users.noreply.github.com> Date: Tue, 27 Jun 2023 23:05:04 +0700 Subject: [PATCH 3/4] feat: cronjob api to post brainery report (#627) * chore: sort tags * chore: add top contributors and sort tags by popularity * feat: cronjob reports brainery metrics --- pkg/controller/brainerylogs/create.go | 29 ++++ pkg/controller/brainerylogs/get_metrics.go | 52 ++++++ pkg/controller/brainerylogs/new.go | 32 ++++ pkg/controller/controller.go | 23 +-- pkg/handler/brainerylogs/brainery_log.go | 79 +++------ pkg/handler/discord/discord.go | 62 +++++-- pkg/handler/discord/errs/errors.go | 8 + pkg/handler/discord/interface.go | 1 + pkg/handler/discord/request/request.go | 21 +++ pkg/handler/handler.go | 6 +- pkg/model/discord_message.go | 11 ++ pkg/routes/v1.go | 1 + pkg/routes/v1_test.go | 6 + pkg/service/discord/discord.go | 182 +++++++++++++++++++++ pkg/service/discord/service.go | 3 + 15 files changed, 434 insertions(+), 82 deletions(-) create mode 100644 pkg/controller/brainerylogs/create.go create mode 100644 pkg/controller/brainerylogs/get_metrics.go create mode 100644 pkg/controller/brainerylogs/new.go create mode 100644 pkg/handler/discord/errs/errors.go create mode 100644 pkg/handler/discord/request/request.go diff --git a/pkg/controller/brainerylogs/create.go b/pkg/controller/brainerylogs/create.go new file mode 100644 index 000000000..9b8715426 --- /dev/null +++ b/pkg/controller/brainerylogs/create.go @@ -0,0 +1,29 @@ +package brainerylogs + +import ( + "errors" + + "github.com/dwarvesf/fortress-api/pkg/model" + "gorm.io/gorm" +) + +// Create creates a new brainery log +func (c *controller) Create(log model.BraineryLog) (model.BraineryLog, error) { + emp, err := c.store.Employee.GetByDiscordID(c.repo.DB(), log.DiscordID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + c.logger.Errorf(err, "failed to get employee by discordID", "discordID", log.DiscordID) + return model.BraineryLog{}, err + } + + if !errors.Is(err, gorm.ErrRecordNotFound) { + log.EmployeeID = emp.ID + } + + _, err = c.store.BraineryLog.Create(c.repo.DB(), []model.BraineryLog{log}) + if err != nil { + c.logger.Errorf(err, "failed to create brainery logs", "braineryLog", log) + return model.BraineryLog{}, err + } + + return log, nil +} diff --git a/pkg/controller/brainerylogs/get_metrics.go b/pkg/controller/brainerylogs/get_metrics.go new file mode 100644 index 000000000..e6477b5b5 --- /dev/null +++ b/pkg/controller/brainerylogs/get_metrics.go @@ -0,0 +1,52 @@ +package brainerylogs + +import ( + "errors" + "time" + + "gorm.io/gorm" + + "github.com/dwarvesf/fortress-api/pkg/logger" + "github.com/dwarvesf/fortress-api/pkg/model" + "github.com/dwarvesf/fortress-api/pkg/utils/timeutil" +) + +// GetMetrics returns brainery metrics +func (c *controller) GetMetrics(queryView string) (latestPosts []*model.BraineryLog, logs []*model.BraineryLog, ncids []string, err error) { + l := c.logger.Fields(logger.Fields{ + "controller": "brainerylogs", + "method": "GetBraineryMetrics", + }) + + // default is weekly + now := time.Now() + end := timeutil.GetEndDayOfWeek(now) + start := timeutil.GetStartDayOfWeek(now) + if queryView == "monthly" { + start = timeutil.FirstDayOfMonth(int(now.Month()), now.Year()) + end = timeutil.LastDayOfMonth(int(now.Month()), now.Year()) + } + + // latest 10 posts + latestPosts, err = c.store.BraineryLog.GetLimitByTimeRange(c.repo.DB(), &time.Time{}, &end, 10) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorf(err, "failed to get latest posts by time range", "start", start, "end", end) + return nil, nil, nil, err + } + + // weekly or monthly posts + logs, err = c.store.BraineryLog.GetLimitByTimeRange(c.repo.DB(), &start, &end, 1000) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorf(err, "failed to get logs by time range", "start", start, "end", end) + return nil, nil, nil, err + } + + // ncids = new contributor discord IDs + ncids, err = c.store.BraineryLog.GetNewContributorDiscordIDs(c.repo.DB(), &start, &end) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorf(err, "failed to get new contributor discord IDs by time range", "start", start, "end", end) + return nil, nil, nil, err + } + + return latestPosts, logs, ncids, nil +} diff --git a/pkg/controller/brainerylogs/new.go b/pkg/controller/brainerylogs/new.go new file mode 100644 index 000000000..38dd26e69 --- /dev/null +++ b/pkg/controller/brainerylogs/new.go @@ -0,0 +1,32 @@ +package brainerylogs + +import ( + "github.com/dwarvesf/fortress-api/pkg/config" + "github.com/dwarvesf/fortress-api/pkg/logger" + "github.com/dwarvesf/fortress-api/pkg/model" + "github.com/dwarvesf/fortress-api/pkg/service" + "github.com/dwarvesf/fortress-api/pkg/store" +) + +type controller struct { + store *store.Store + service *service.Service + logger logger.Logger + repo store.DBRepo + config *config.Config +} + +func New(store *store.Store, repo store.DBRepo, service *service.Service, logger logger.Logger, cfg *config.Config) IController { + return &controller{ + store: store, + repo: repo, + service: service, + logger: logger, + config: cfg, + } +} + +type IController interface { + Create(log model.BraineryLog) (model.BraineryLog, error) + GetMetrics(queryView string) (latestPosts []*model.BraineryLog, logs []*model.BraineryLog, ncids []string, err error) +} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 22bfc1f91..e3e83249e 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -3,6 +3,7 @@ package controller import ( "github.com/dwarvesf/fortress-api/pkg/config" "github.com/dwarvesf/fortress-api/pkg/controller/auth" + "github.com/dwarvesf/fortress-api/pkg/controller/brainerylogs" "github.com/dwarvesf/fortress-api/pkg/controller/client" "github.com/dwarvesf/fortress-api/pkg/controller/discord" "github.com/dwarvesf/fortress-api/pkg/controller/employee" @@ -14,19 +15,21 @@ import ( ) type Controller struct { - Auth auth.IController - Client client.IController - Employee employee.IController - Invoice invoice.IController - Discord discord.IController + Auth auth.IController + BraineryLog brainerylogs.IController + Client client.IController + Employee employee.IController + Invoice invoice.IController + Discord discord.IController } func New(store *store.Store, repo store.DBRepo, service *service.Service, worker *worker.Worker, logger logger.Logger, cfg *config.Config) *Controller { return &Controller{ - Auth: auth.New(store, repo, service, logger, cfg), - Client: client.New(store, repo, service, logger, cfg), - Employee: employee.New(store, repo, service, logger, cfg), - Invoice: invoice.New(store, repo, service, worker, logger, cfg), - Discord: discord.New(store, repo, service, logger, cfg), + Auth: auth.New(store, repo, service, logger, cfg), + BraineryLog: brainerylogs.New(store, repo, service, logger, cfg), + Client: client.New(store, repo, service, logger, cfg), + Employee: employee.New(store, repo, service, logger, cfg), + Invoice: invoice.New(store, repo, service, worker, logger, cfg), + Discord: discord.New(store, repo, service, logger, cfg), } } diff --git a/pkg/handler/brainerylogs/brainery_log.go b/pkg/handler/brainerylogs/brainery_log.go index 69379d65d..aa894f682 100644 --- a/pkg/handler/brainerylogs/brainery_log.go +++ b/pkg/handler/brainerylogs/brainery_log.go @@ -1,43 +1,43 @@ package brainerylogs import ( - "errors" "net/http" "regexp" "strings" "time" - "github.com/dwarvesf/fortress-api/pkg/store/employee" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" - "gorm.io/gorm" "github.com/dwarvesf/fortress-api/pkg/config" + "github.com/dwarvesf/fortress-api/pkg/controller" "github.com/dwarvesf/fortress-api/pkg/handler/brainerylogs/request" "github.com/dwarvesf/fortress-api/pkg/logger" "github.com/dwarvesf/fortress-api/pkg/model" "github.com/dwarvesf/fortress-api/pkg/service" "github.com/dwarvesf/fortress-api/pkg/store" - "github.com/dwarvesf/fortress-api/pkg/utils/timeutil" + "github.com/dwarvesf/fortress-api/pkg/store/employee" "github.com/dwarvesf/fortress-api/pkg/view" ) type handler struct { - store *store.Store - service *service.Service - logger logger.Logger - repo store.DBRepo - config *config.Config + controller *controller.Controller + store *store.Store + service *service.Service + logger logger.Logger + repo store.DBRepo + config *config.Config } // New returns a handler -func New(store *store.Store, repo store.DBRepo, service *service.Service, logger logger.Logger, cfg *config.Config) IHandler { +func New(controller *controller.Controller, store *store.Store, repo store.DBRepo, service *service.Service, logger logger.Logger, cfg *config.Config) IHandler { return &handler{ - store: store, - repo: repo, - service: service, - logger: logger, - config: cfg, + controller: controller, + store: store, + repo: repo, + service: service, + logger: logger, + config: cfg, } } @@ -89,21 +89,10 @@ func (h *handler) Create(c *gin.Context) { Reward: body.Reward, } - emp, err := h.store.Employee.GetByDiscordID(h.repo.DB(), body.DiscordID) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - l.Errorf(err, "failed to get employee by discordID", "discordID", body.DiscordID) - c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, body, "")) - return - } - - if !errors.Is(err, gorm.ErrRecordNotFound) { - b.EmployeeID = emp.ID - } - - _, err = h.store.BraineryLog.Create(h.repo.DB(), []model.BraineryLog{b}) + log, err := h.controller.BraineryLog.Create(b) if err != nil { l.Errorf(err, "failed to create brainery logs", "braineryLog", b) - c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, body, "")) + c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, log, "")) return } @@ -126,41 +115,15 @@ func (h *handler) GetMetrics(c *gin.Context) { l := h.logger.Fields( logger.Fields{ "handler": "brainerylogs", - "method": "GetMetric", + "method": "GetMetrics", }, ) queryView := c.DefaultQuery("view", "weekly") - // default is weekly - now := time.Now() - end := timeutil.GetEndDayOfWeek(now) - start := timeutil.GetStartDayOfWeek(now) - if queryView == "monthly" { - start = timeutil.FirstDayOfMonth(int(now.Month()), now.Year()) - end = timeutil.LastDayOfMonth(int(now.Month()), now.Year()) - } - - // latest 10 posts - latestPosts, err := h.store.BraineryLog.GetLimitByTimeRange(h.repo.DB(), &time.Time{}, &end, 10) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - l.Errorf(err, "failed to get latest posts by time range", "start", start, "end", end) - c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, "")) - return - } - - // weekly or monthly posts - logs, err := h.store.BraineryLog.GetLimitByTimeRange(h.repo.DB(), &start, &end, 1000) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - l.Errorf(err, "failed to get logs by time range", "start", start, "end", end) - c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, "")) - return - } - - // ncids = new contributor discord IDs - ncids, err := h.store.BraineryLog.GetNewContributorDiscordIDs(h.repo.DB(), &start, &end) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - l.Errorf(err, "failed to get new contributor discord IDs by time range", "start", start, "end", end) + latestPosts, logs, ncids, err := h.controller.BraineryLog.GetMetrics(queryView) + if err != nil { + l.Error(err, "failed to get brainery metrics") c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, "")) return } diff --git a/pkg/handler/discord/discord.go b/pkg/handler/discord/discord.go index e5f213efd..183bab5c0 100644 --- a/pkg/handler/discord/discord.go +++ b/pkg/handler/discord/discord.go @@ -7,9 +7,12 @@ import ( "strings" "time" + "github.com/bwmarrin/discordgo" "github.com/gin-gonic/gin" "github.com/dwarvesf/fortress-api/pkg/config" + "github.com/dwarvesf/fortress-api/pkg/controller" + "github.com/dwarvesf/fortress-api/pkg/handler/discord/request" "github.com/dwarvesf/fortress-api/pkg/logger" "github.com/dwarvesf/fortress-api/pkg/model" "github.com/dwarvesf/fortress-api/pkg/service" @@ -22,20 +25,22 @@ import ( ) type handler struct { - store *store.Store - service *service.Service - logger logger.Logger - repo store.DBRepo - config *config.Config + controller *controller.Controller + store *store.Store + service *service.Service + logger logger.Logger + repo store.DBRepo + config *config.Config } -func New(store *store.Store, repo store.DBRepo, service *service.Service, logger logger.Logger, cfg *config.Config) IHandler { +func New(controller *controller.Controller, store *store.Store, repo store.DBRepo, service *service.Service, logger logger.Logger, cfg *config.Config) IHandler { return &handler{ - store: store, - repo: repo, - service: service, - logger: logger, - config: cfg, + controller: controller, + store: store, + repo: repo, + service: service, + logger: logger, + config: cfg, } } @@ -235,3 +240,38 @@ func (h *handler) OnLeaveMessage(c *gin.Context) { h.logger.Infof("Discord message sent: %s", msg) c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, nil, nil, "ok")) } + +// ReportBraineryMetrics reports brainery metrics to a channel +func (h *handler) ReportBraineryMetrics(c *gin.Context) { + body := request.BraineryReportInput{} + if err := c.ShouldBindJSON(&body); err != nil { + h.logger.Error(err, "failed to decode body") + c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, err, body, "")) + return + } + if err := body.Validate(); err != nil { + h.logger.Errorf(err, "failed to validate data", "body", body) + c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, err, body, "")) + return + } + + latestPosts, logs, ncids, err := h.controller.BraineryLog.GetMetrics(body.View) + if err != nil { + h.logger.Error(err, "failed to get brainery metrics") + c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, err, nil, "")) + return + } + + metrics := view.ToBraineryMetric(latestPosts, logs, ncids, body.View) + + //send message to Discord channel + var discordMsg *discordgo.Message + discordMsg, err = h.service.Discord.ReportBraineryMetrics(body.View, &metrics, body.ChannelID) + if err != nil { + h.logger.Error(err, "failed to report brainery metrics discord message") + c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, err, discordMsg, "")) + return + } + + c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, nil, nil, "ok")) +} diff --git a/pkg/handler/discord/errs/errors.go b/pkg/handler/discord/errs/errors.go new file mode 100644 index 000000000..7a121d56e --- /dev/null +++ b/pkg/handler/discord/errs/errors.go @@ -0,0 +1,8 @@ +package errs + +import "errors" + +var ( + ErrEmptyReportView = errors.New("view is empty") + ErrEmptyChannelID = errors.New("channelID is empty") +) diff --git a/pkg/handler/discord/interface.go b/pkg/handler/discord/interface.go index d3e0a87a1..1ef5e270f 100644 --- a/pkg/handler/discord/interface.go +++ b/pkg/handler/discord/interface.go @@ -6,4 +6,5 @@ type IHandler interface { SyncDiscordInfo(c *gin.Context) BirthdayDailyMessage(c *gin.Context) OnLeaveMessage(c *gin.Context) + ReportBraineryMetrics(c *gin.Context) } diff --git a/pkg/handler/discord/request/request.go b/pkg/handler/discord/request/request.go new file mode 100644 index 000000000..054d3290c --- /dev/null +++ b/pkg/handler/discord/request/request.go @@ -0,0 +1,21 @@ +package request + +import ( + "github.com/dwarvesf/fortress-api/pkg/handler/discord/errs" +) + +type BraineryReportInput struct { + View string `json:"view" binding:"required"` + ChannelID string `json:"channelID" binding:"required"` +} + +func (input BraineryReportInput) Validate() error { + if len(input.View) == 0 { + return errs.ErrEmptyReportView + } + + if len(input.ChannelID) == 0 { + return errs.ErrEmptyChannelID + } + return nil +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 5dc73ee02..43b22fe4c 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -39,6 +39,7 @@ type Handler struct { Audit audit.IHandler Auth auth.IHandler BankAccount bankaccount.IHandler + BraineryLog brainerylogs.IHandler Client client.IHandler Dashboard dashboard.IHandler Discord discord.IHandler @@ -56,7 +57,6 @@ type Handler struct { Valuation valuation.IHandler Webhook webhook.IHandler Vault vault.IHandler - BraineryLog brainerylogs.IHandler } func New(store *store.Store, repo store.DBRepo, service *service.Service, ctrl *controller.Controller, worker *worker.Worker, logger logger.Logger, cfg *config.Config) *Handler { @@ -66,9 +66,10 @@ func New(store *store.Store, repo store.DBRepo, service *service.Service, ctrl * Audit: audit.New(store, repo, service, logger, cfg), Auth: auth.New(ctrl, logger, cfg), BankAccount: bankaccount.New(store, repo, service, logger, cfg), + BraineryLog: brainerylogs.New(ctrl, store, repo, service, logger, cfg), Client: client.New(ctrl, store, repo, service, logger, cfg), Dashboard: dashboard.New(store, repo, service, logger, cfg, util.New()), - Discord: discord.New(store, repo, service, logger, cfg), + Discord: discord.New(ctrl, store, repo, service, logger, cfg), Employee: employee.New(ctrl, store, repo, service, logger, cfg), Engagement: engagement.New(ctrl, store, repo, service, logger, cfg), Feedback: feedback.New(store, repo, service, logger, cfg), @@ -83,6 +84,5 @@ func New(store *store.Store, repo store.DBRepo, service *service.Service, ctrl * Valuation: valuation.New(store, repo, service, logger, cfg), Webhook: webhook.New(ctrl, store, repo, service, logger, cfg, worker), Vault: vault.New(store, repo, service, logger, cfg), - BraineryLog: brainerylogs.New(store, repo, service, logger, cfg), } } diff --git a/pkg/model/discord_message.go b/pkg/model/discord_message.go index 74b5e26b5..bf03ccfca 100644 --- a/pkg/model/discord_message.go +++ b/pkg/model/discord_message.go @@ -1,5 +1,7 @@ package model +import "github.com/bwmarrin/discordgo" + type DiscordMessage struct { AvatarURL string `json:"avatar_url"` Content string `json:"content"` @@ -54,3 +56,12 @@ type DiscordMessageComponent struct { Type int64 `json:"type"` URL interface{} `json:"url"` } + +type OriginalDiscordMessage struct { + RawContent string + ContentArgs []string + ChannelId string + GuildId string + Author *discordgo.User + Roles []string +} diff --git a/pkg/routes/v1.go b/pkg/routes/v1.go index e5e2ee6dd..194ca5622 100644 --- a/pkg/routes/v1.go +++ b/pkg/routes/v1.go @@ -27,6 +27,7 @@ func loadV1Routes(r *gin.Engine, h *handler.Handler, repo store.DBRepo, s *store cronjob.POST("/sync-project-member-status", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Project.SyncProjectMemberStatus) cronjob.POST("/store-vault-transaction", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Vault.StoreVaultTransaction) cronjob.POST("/index-engagement-messages", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Engagement.IndexMessages) + cronjob.POST("/brainery-reports", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Discord.ReportBraineryMetrics) } ///////////////// diff --git a/pkg/routes/v1_test.go b/pkg/routes/v1_test.go index 8a81aba2a..2e5ec0210 100644 --- a/pkg/routes/v1_test.go +++ b/pkg/routes/v1_test.go @@ -645,6 +645,12 @@ func Test_loadV1Routes(t *testing.T) { Handler: "github.com/dwarvesf/fortress-api/pkg/handler/engagement.IHandler.IndexMessages-fm", }, }, + "/cronjobs/brainery-reports": { + "POST": { + Method: "POST", + Handler: "github.com/dwarvesf/fortress-api/pkg/handler/discord.IHandler.ReportBraineryMetrics-fm", + }, + }, "/webhooks/n8n": { "POST": { Method: "POST", diff --git a/pkg/service/discord/discord.go b/pkg/service/discord/discord.go index 967699269..0525007ef 100644 --- a/pkg/service/discord/discord.go +++ b/pkg/service/discord/discord.go @@ -3,6 +3,7 @@ package discord import ( "bytes" "encoding/json" + "fmt" "io" "net/http" "strconv" @@ -10,8 +11,11 @@ import ( "time" "github.com/bwmarrin/discordgo" + "github.com/shopspring/decimal" + "github.com/dwarvesf/fortress-api/pkg/config" "github.com/dwarvesf/fortress-api/pkg/model" + "github.com/dwarvesf/fortress-api/pkg/view" ) var ( @@ -326,3 +330,181 @@ func (d *discordClient) GetMessagesAfterCursor( return allMessages, nil } + +func (d *discordClient) ReportBraineryMetrics(queryView string, braineryMetric *view.BraineryMetric, channelID string) (*discordgo.Message, error) { + var messageEmbed []*discordgo.MessageEmbedField + totalICY := decimal.NewFromInt(0) + content := "" + + var newBraineryPost []view.Post + newBraineryPost = append(newBraineryPost, braineryMetric.Contributors...) + newBraineryPost = append(newBraineryPost, braineryMetric.NewContributors...) + + if len(newBraineryPost) == 0 { + content += "There is no new brainery note in this period. This is where we keep track of our **top 10 latest** Brainery notes:\n\n" + + for _, itm := range braineryMetric.LatestPosts { + content += fmt.Sprintf("• [%s](%s) <@%v>\n", itm.Title, itm.URL, itm.DiscordID) + } + } else { + newBraineryPostStr := "" + for _, itm := range newBraineryPost { + totalICY = totalICY.Add(itm.Reward) + newBraineryPostStr += fmt.Sprintf("• [%s](%s) <@%v>\n", itm.Title, itm.URL, itm.DiscordID) + } + + if len(newBraineryPostStr) > 0 { + content += "**Latest Notes** :fire::fire::fire:\n" + content += newBraineryPostStr + "\n" + } + } + + if queryView == "monthly" { + topContributor := calculateTopContributor(braineryMetric.TopContributors) + content += topContributor + "\n" + } + + newContributor := "" + if len(braineryMetric.NewContributors) > 0 { + ids := make(map[string]bool) + for _, itm := range braineryMetric.NewContributors { + v, ok := ids[itm.DiscordID] + if ok && v { + continue + } + ids[itm.DiscordID] = true + newContributor += fmt.Sprintf("<@%v> ", itm.DiscordID) + } + } + + if newContributor != "" { + content += "**New Contributors**\n" + content += newContributor + "\n" + } + + if totalICY.GreaterThan(decimal.NewFromInt(0)) { + content += "\n**Total Reward Distributed**\n" + content += totalICY.String() + " ICY 🧊" + } + + tags := "" + if len(braineryMetric.Tags) > 0 { + for _, tag := range braineryMetric.Tags { + tags += fmt.Sprintf("#%v ", tag) + } + } + + if len(tags) > 0 { + embedField := &discordgo.MessageEmbedField{ + Name: "Tags", + Value: tags, + Inline: false, + } + + messageEmbed = append(messageEmbed, embedField) + } + + msg := &discordgo.MessageEmbed{ + Title: fmt.Sprintf("BRAINERY %s REPORT", strings.ToTitle(queryView)), + Fields: messageEmbed, + Description: content, + Footer: &discordgo.MessageEmbedFooter{ + IconURL: "https://cdn.discordapp.com/avatars/564764617545482251/9c9bd4aaba164fc0b92f13f052405b4d.webp?size=160", + Text: "?help to see all commands", + }, + } + + return d.SendEmbeddedMessageWithChannel(nil, msg, channelID) +} + +func calculateTopContributor(topContributors []view.TopContributor) string { + topContributorStr := "" + if len(topContributors) == 0 { + return "" + } + + countMap := make(map[int][]string) + var uniqueCounts []int + + for _, contributor := range topContributors { + if contributor.Count > 1 { + count := contributor.Count + discordID := contributor.DiscordID + countMap[count] = append(countMap[count], discordID) + + // Check if count is already in uniqueCounts + found := false + for _, uniqueCount := range uniqueCounts { + if uniqueCount == count { + found = true + break + } + } + + // If count is not found, add it to uniqueCounts + if !found { + uniqueCounts = append(uniqueCounts, count) + } + } + } + + emojiMap := map[int]string{ + 0: ":first_place:", + 1: ":second_place:", + 2: ":third_place:", + } + + // Iterate over uniqueCounts to access Discord IDs in order + for idx, count := range uniqueCounts { + discordIDs := countMap[count] + discordIDStr := "" + for i := 0; i < len(discordIDs); i++ { + discordIDStr += "<@" + discordIDs[i] + ">, " + } + + emojiIdx := idx + if idx > 2 { + emojiIdx = 2 + } + + topContributorStr += fmt.Sprintf("%v %v (x%v) \n", emojiMap[emojiIdx], strings.TrimSuffix(discordIDStr, ", "), count) + } + + topContributor := "" + if len(topContributorStr) > 0 { + topContributor += "**Top Contributors**\n" + topContributor += topContributorStr + } + + return topContributor +} + +func (d *discordClient) SendEmbeddedMessageWithChannel(original *model.OriginalDiscordMessage, embed *discordgo.MessageEmbed, channelId string) (*discordgo.Message, error) { + msg, err := d.session.ChannelMessageSendEmbed(channelId, normalize(original, embed)) + return msg, err +} + +// normalize add some default to embedded message if not set +func normalize(original *model.OriginalDiscordMessage, response *discordgo.MessageEmbed) *discordgo.MessageEmbed { + if response.Timestamp == "" { + response.Timestamp = time.Now().Format(time.RFC3339) + } + + // I did something tricky here, if timestamp is custom, we don't want to show it, because in case of user want to add a custom date time format in the footer + // instead of automatically add it, we don't want to show it twice. + if response.Timestamp == "custom" { + response.Timestamp = "" + } + + if response.Color == 0 { + // default df color #D14960 + response.Color = 13715808 + } + if response.Footer == nil { + response.Footer = &discordgo.MessageEmbedFooter{ + IconURL: "https://cdn.discordapp.com/avatars/564764617545482251/9c9bd4aaba164fc0b92f13f052405b4d.webp?size=160", + Text: "?help to see all commands", + } + } + return response +} diff --git a/pkg/service/discord/service.go b/pkg/service/discord/service.go index 67b353c95..fd99eaf57 100644 --- a/pkg/service/discord/service.go +++ b/pkg/service/discord/service.go @@ -4,6 +4,7 @@ import ( "github.com/bwmarrin/discordgo" "github.com/dwarvesf/fortress-api/pkg/model" + "github.com/dwarvesf/fortress-api/pkg/view" ) type IService interface { @@ -26,4 +27,6 @@ type IService interface { GetChannels() ([]*discordgo.Channel, error) GetMessagesAfterCursor(channelID string, cursorMessageID string, lastMessageID string) ([]*discordgo.Message, error) + ReportBraineryMetrics(queryView string, braineryMetric *view.BraineryMetric, channelID string) (*discordgo.Message, error) + SendEmbeddedMessageWithChannel(original *model.OriginalDiscordMessage, embed *discordgo.MessageEmbed, channelId string) (*discordgo.Message, error) } From ed12a990c3e069b8790b4fe2413c8b29943f28bc Mon Sep 17 00:00:00 2001 From: namnhce Date: Wed, 28 Jun 2023 00:14:53 +0700 Subject: [PATCH 4/4] chore: add country data --- migrations/seed/metadata.sql | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/migrations/seed/metadata.sql b/migrations/seed/metadata.sql index af8e9987d..dfc72664e 100644 --- a/migrations/seed/metadata.sql +++ b/migrations/seed/metadata.sql @@ -1,6 +1,14 @@ INSERT INTO public.countries (id, deleted_at, created_at, updated_at, name, code, cities) VALUES -('4ef64490-c906-4192-a7f9-d2221dadfe4c',NULL,'2022-11-08 08:06:56.068148','2022-11-08 08:06:56.068148','Vietnam','+84','["Hồ Chí Minh", "An Giang", "Bà Rịa-Vũng Tàu", "Bình Dương", "Bình Định", "Bình Phước", "Bình Thuận", "Bạc Liêu", "Bắc Giang", "Bắc Kạn", "Bắc Ninh", "Bến Tre", "Cao Bằng", "Cà Mau", "Cần Thơ", "Điện Biên", "Đà Nẵng", "Đắk Lắk", "Đồng Nai", "Đắk Nông", "Đồng Tháp", "Gia Lai", "Hoà Bình", "Hà Giang", "Hà Nam", "Hà Nội", "Hà Tĩnh", "Hải Dương", "Hải Phòng", "Hậu Giang", "Hưng Yên", "Khánh Hòa", "Kiên Giang", "Kon Tum", "Lai Châu", "Lâm Đồng", "Lạng Sơn", "Lào Cai", "Long An", "Nam Định", "Nghệ An", "Ninh Bình", "Ninh Thuận", "Phú Thọ", "Phú Yên", "Quảng Bình", "Quảng Nam", "Quảng Ngãi", "Quảng Ninh", "Quảng Trị", "Sóc Trăng", "Sơn La", "Thanh Hóa", "Thái Bình", "Thái Nguyên", "Thừa Thiên Huế", "Tiền Giang", "Trà Vinh", "Tuyên Quang", "Tây Ninh", "Vĩnh Long", "Vĩnh Phúc", "Yên Bái"]'), -('da9031ce-0d6e-4344-b97a-a2c44c66153e',NULL,'2022-11-08 08:08:09.881727','2022-11-08 08:08:09.881727','Singapore','+65','["Singapore"]'); +('e08bf154-2e04-413e-83b3-433a18a030e4', null, '2022-11-24 15:48:08.594250', '2022-11-24 15:48:08.594250', 'France', '+31', '[{"lat": "48.8588475", "long": "2.3058358", "name": "Paris"}, {"lat": "43.2803157", "long": "5.2982488", "name": "Marseille"}, {"lat": "45.7579433", "long": "4.7939309", "name": "Lyon"}]'), +('c17531fc-5ed5-4253-bd7d-867536a12767', null, '2022-11-24 15:27:44.754417', '2022-11-24 15:27:44.754417', 'Canada', '+1', '[{"lat": "37.6705273", "long": "-81.0907044", "name": "Toronto"}, {"lat": "45.2484593", "long": "-76.4239444", "name": "Ottawa"}, {"lat": "49.2576306", "long": "-123.2797851", "name": "Vancouver"}]'), +('da9031ce-0d6e-4344-b97a-a2c44c66153e', null, '2022-11-08 08:08:09.881727', '2022-11-08 08:08:09.881727', 'Singapore', '+65', '[{"lat": "1.3139987", "long": "103.7618497", "name": "Singapore"}]'), +('bdf64a3e-73b8-44d5-a6c1-c5f13806dff8', null, '2022-11-24 15:47:01.130926', '2022-11-24 15:47:01.130926', 'Australia', '+61', '[{"lat": "-37.9721066", "long": "144.7235094", "name": "Melbourne"}, {"lat": "-33.8481343", "long": "150.7671702", "name": "Sydney"}, {"lat": "-35.3135796", "long": "148.8179818", "name": "Canberra"}]'), +('3b185196-b32f-48b3-9dfd-0ee7023498e9', null, '2022-11-24 15:46:30.443316', '2022-11-24 15:46:30.443316', 'Malaysia', '+60', '[{"lat": "3.1385027", "long": "101.6045891", "name": "Kuala Lumpur"}, {"lat": "5.3700333", "long": "100.4036479", "name": "Seberang Perai"}, {"lat": "3.1379827", "long": "101.5669791", "name": "Petaling Jaya"}, {"lat": "1.5448546", "long": "103.6684278", "name": "Johor Bahru"}]'), +('dd462da0-406d-4564-93a6-357e22f3ce70', null, '2022-11-24 15:27:44.754417', '2022-11-24 15:27:44.754417', 'Indonesia', '+62', '[{"lat": "-6.2297401", "long": "106.7471176", "name": "Jakarta"}, {"lat": "-7.2756176", "long": "112.6714843", "name": "Surabaya"}]'), +('cab534b8-4481-4a41-bb26-292dcfba1bb7', null, '2022-11-24 15:47:55.340160', '2022-11-24 15:47:55.340160', 'Netherlands', '+31', '[{"lat": "51.9279573", "long": "4.4084289", "name": "Rotterdam"}, {"lat": "52.354637", "long": "4.8215612", "name": "Amsterdam"}]'), +('c8fb7cea-9803-467d-b3a0-dc1943663313', null, '2022-11-24 15:27:44.754417', '2022-11-24 15:27:44.754417', 'United Kingdom', '+44', '[{"lat": "51.5285262", "long": "-0.2664005", "name": "London"}, {"lat": "53.4120759", "long": "-3.0719387", "name": "Liverpool"}, {"lat": "53.4722171", "long": "-2.305862", "name": "Manchester"}, {"lat": "51.4683856", "long": "-2.746598", "name": "Britol"}, {"lat": "52.4973205", "long": "-1.9460307", "name": "Birmingham"}, {"lat": "53.8059014", "long": "-1.6916122", "name": "Leeds"}]'), +('370f9f3b-1440-47a3-8606-97642a37974d', null, '2022-11-24 15:27:44.754417', '2022-11-24 15:27:44.754417', 'United States', '+1', '[{"lat": "32.318230", "long": "-86.902298", "name": "Alabama"}, {"lat": "66.160507", "long": "-153.369141", "name": "Alaska"}, {"lat": "34.048927", "long": "-111.093735", "name": "Arizona"}, {"lat": "34.799999", "long": "-92.199997", "name": "Arkansas"}, {"lat": "36.778259", "long": "-119.417931", "name": "California"}, {"lat": "39.113014", "long": "-105.358887", "name": "Colorado"}, {"lat": "41.599998", "long": "-72.699997", "name": "Connecticut"}, {"lat": "39.000000", "long": "-75.500000", "name": "Delaware"}, {"lat": "27.994402", "long": "-81.760254", "name": "Florida"}, {"lat": "33.247875", "long": "-83.441162", "name": "Georgia"}, {"lat": "19.741755", "long": "-155.844437", "name": "Hawaii"}, {"lat": "44.068203", "long": "-114.742043", "name": "Idaho"}, {"lat": "40.000000", "long": "-89.000000", "name": "Illinois"}, {"lat": "40.273502", "long": "-86.126976", "name": "Indiana"}, {"lat": "42.032974", "long": "-93.581543", "name": "Iowa"}, {"lat": "38.500000", "long": "-98.000000", "name": "Kansas"}, {"lat": "37.839333", "long": "-84.270020", "name": "Kentucky"}, {"lat": "30.391830", "long": "-92.329102", "name": "Louisiana"}, {"lat": "45.367584", "long": "-68.972168", "name": "Maine"}, {"lat": "39.045753", "long": "-76.641273", "name": "Maryland"}, {"lat": "42.407211", "long": "-71.382439", "name": "Massachusetts"}, {"lat": "44.182205", "long": "-84.506836", "name": "Michigan"}, {"lat": "46.392410", "long": "-94.636230", "name": "Minnesota"}, {"lat": "33.000000", "long": "-90.000000", "name": "Mississippi"}, {"lat": "38.573936", "long": "-92.603760", "name": "Missouri"}, {"lat": "46.965260", "long": "-109.533691", "name": "Montana"}, {"lat": "41.500000", "long": "-100.000000", "name": "Nebraska"}, {"lat": "39.876019", "long": "-117.224121", "name": "Nevada"}, {"lat": "44.000000", "long": "-71.500000", "name": "New Hampshire"}, {"lat": "39.833851", "long": "-74.871826", "name": "New Jersey"}, {"lat": "34.307144", "long": "-106.018066", "name": "New Mexico"}, {"lat": "43.000000", "long": "-75.000000", "name": "New York"}, {"lat": "35.782169", "long": "-80.793457", "name": "North Carolina"}, {"lat": "47.650589", "long": "-100.437012", "name": "North Dakota"}, {"lat": "40.367474", "long": "-82.996216", "name": "Ohio"}, {"lat": "36.084621", "long": "-96.921387", "name": "Oklahoma"}, {"lat": "44.000000", "long": "-120.500000", "name": "Oregon"}, {"lat": "41.203323", "long": "-77.194527", "name": "Pennsylvania"}, {"lat": "41.742325", "long": "-71.742332", "name": "Rhode Island"}, {"lat": "33.836082", "long": "-81.163727", "name": "South Carolina"}, {"lat": "44.500000", "long": "-100.000000", "name": "South Dakota"}, {"lat": "35.860119", "long": "-86.660156", "name": "Tennessee"}, {"lat": "31.000000", "long": "-100.000000", "name": "Texas"}, {"lat": "39.419220", "long": "-111.950684", "name": "Utah"}, {"lat": "44.000000", "long": "-72.699997", "name": "Vermont"}, {"lat": "37.926868", "long": "-78.024902", "name": "Virginia"}, {"lat": "47.751076", "long": "-120.740135", "name": "Washington"}, {"lat": "39.000000", "long": "-80.500000", "name": "West Virginia"}, {"lat": "44.500000", "long": "-89.500000", "name": "Wisconsin"}, {"lat": "43.075970", "long": "-107.290283", "name": "Wyoming"}]'), +('4ef64490-c906-4192-a7f9-d2221dadfe4c', null, '2022-11-08 08:06:56.068148', '2022-11-08 08:06:56.068148', 'Vietnam', '+84', '[{"lat": "10.82302", "long": "106.62965", "name": "Hồ Chí Minh"}, {"lat": "21.0245", "long": "105.84117", "name": "Hà Nội"}, {"lat": "16.066666666667", "long": "108.23333333333", "name": "Đà Nẵng"}, {"lat": "10.033333333333", "long": "105.78333333333", "name": "Cần Thơ"}, {"lat": "20.8", "long": "106.66666666667", "name": "Hải Phòng"}, {"lat": "10.5", "long": "105.16666666667", "name": "An Giang"}, {"lat": "14.75", "long": "107.91666666667", "name": "Kon Tum"}, {"lat": "10.404166666667", "long": "107.14166666667", "name": "Bà Rịa - Vũng Tàu"}, {"lat": "22.283333333333", "long": "103.25", "name": "Lai Châu"}, {"lat": "21.333333333333", "long": "106.43333333333", "name": "Bắc Giang"}, {"lat": "11.95", "long": "108.43333333333", "name": "Lâm Đồng"}, {"lat": "22.25", "long": "105.83333333333", "name": "Bắc Kạn"}, {"lat": "21.75", "long": "106.5", "name": "Lạng Sơn"}, {"lat": "9.3", "long": "105.5", "name": "Bạc Liêu"}, {"lat": "22.3", "long": "104.16666666667", "name": "Lào Cai"}, {"lat": "21.1", "long": "106.1", "name": "Bắc Ninh"}, {"lat": "10.666666666667", "long": "106.16666666667", "name": "Long An"}, {"lat": "10.166666666667", "long": "106.5", "name": "Bến Tre"}, {"lat": "20.25", "long": "106.25", "name": "Nam Định"}, {"lat": "14.166666666667", "long": "109.0", "name": "Bình Định"}, {"lat": "19.333333333333", "long": "104.83333333333", "name": "Nghệ An"}, {"lat": "11.166666666667", "long": "106.66666666667", "name": "Bình Dương"}, {"lat": "20.250924", "long": "105.974808", "name": "Ninh Bình"}, {"lat": "11.75", "long": "106.91666666667", "name": "Bình Phước"}, {"lat": "11.75", "long": "108.83333333333", "name": "Ninh Thuận"}, {"lat": "11.083333333333", "long": "108.08333333333", "name": "Bình Thuận"}, {"lat": "21.333333333333", "long": "105.16666666667", "name": "Phú Thọ"}, {"lat": "9.0833333333333", "long": "105.08333333333", "name": "Cà Mau"}, {"lat": "13.166666666667", "long": "109.16666666667", "name": "Phú Yên"}, {"lat": "17.5", "long": "106.33333333333", "name": "Quảng Bình"}, {"lat": "22.75", "long": "106.08333333333", "name": "Cao Bằng"}, {"lat": "15.583333333333", "long": "107.91666666667", "name": "Quảng Nam"}, {"lat": "15.0", "long": "108.66666666667", "name": "Quảng Ngãi"}, {"lat": "12.666666666667", "long": "108.05", "name": "Đắk Lắk"}, {"lat": "21.25", "long": "107.33333333333", "name": "Quảng Ninh"}, {"lat": "11.983333333333", "long": "107.7", "name": "Đắk Nông"}, {"lat": "16.75", "long": "107.0", "name": "Quảng Trị"}, {"lat": "21.383333333333", "long": "103.01666666667", "name": "Điện Biên"}, {"lat": "9.6666666666667", "long": "105.83333333333", "name": "Sóc Trăng"}, {"lat": "11.110278", "long": "107.181111", "name": "Đồng Nai"}, {"lat": "21.166666666667", "long": "104.0", "name": "Sơn La"}, {"lat": "10.455015", "long": "105.633957", "name": "Đồng Tháp"}, {"lat": "11.333333333333", "long": "106.16666666667", "name": "Tây Ninh"}, {"lat": "13.75", "long": "108.25", "name": "Gia Lai"}, {"lat": "20.5", "long": "106.33333333333", "name": "Thái Bình"}, {"lat": "22.75", "long": "105.0", "name": "Hà Giang"}, {"lat": "21.666666666667", "long": "105.83333333333", "name": "Thái Nguyên"}, {"lat": "20.533333333333", "long": "105.96666666667", "name": "Hà Nam"}, {"lat": "20.0", "long": "105.5", "name": "Thanh Hóa"}, {"lat": "16.333333333333", "long": "107.58333333333", "name": "Thừa Thiên - Huế"}, {"lat": "18.333333333333", "long": "105.9", "name": "Hà Tĩnh"}, {"lat": "10.416666666667", "long": "106.16666666667", "name": "Tiền Giang"}, {"lat": "20.916666666667", "long": "106.33333333333", "name": "Hải Dương"}, {"lat": "9.8333333333333", "long": "106.25", "name": "Trà Vinh"}, {"lat": "9.7833333333333", "long": "105.46666666667", "name": "Hậu Giang"}, {"lat": "22.1167", "long": "105.25", "name": "Tuyên Quang"}, {"lat": "20.666666666667", "long": "105.33333333333", "name": "Hòa Bình"}, {"lat": "10.166666666667", "long": "106.0", "name": "Vĩnh Long"}, {"lat": "20.816666666667", "long": "106.05", "name": "Hưng Yên"}, {"lat": "21.3", "long": "105.6", "name": "Vĩnh Phúc"}, {"lat": "12.333333333333", "long": "109.0", "name": "Khánh Hòa"}, {"lat": "21.5", "long": "104.66666666667", "name": "Yên Bái"}, {"lat": "10.0", "long": "105.16666666667", "name": "Kiên Giang"}]'); INSERT INTO public.currencies (id, deleted_at, created_at, updated_at, name, symbol, locale, type) VALUES ('06a699ed-618b-400b-ac8c-8739956fa8e7', NULL, '2019-02-20 04:24:14.967209+00', '2019-02-20 04:24:14.967209+00', 'GBP', '£', 'en-gb', 'fiat'),