diff --git a/pkg/entities/guild.go b/pkg/entities/guild.go index a8277574..82857a89 100644 --- a/pkg/entities/guild.go +++ b/pkg/entities/guild.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "math/rand" "time" "github.com/bwmarrin/discordgo" @@ -15,6 +16,7 @@ import ( baseerrs "github.com/defipod/mochi/pkg/model/errors" "github.com/defipod/mochi/pkg/request" "github.com/defipod/mochi/pkg/response" + "github.com/defipod/mochi/pkg/util" ) func (e *Entity) CreateGuild(guild request.CreateGuildRequest) error { @@ -352,3 +354,62 @@ func (e *Entity) ValidateUser(ids []string, guildId string) ([]string, error) { return res, nil } + +func (e *Entity) GuildReportRoles(guildId string) (*response.GuildReportRoles, error) { + guildInfo, err := e.svc.Discord.GuildWithCounts(guildId) + if err != nil { + e.log.Errorf(err, "[entity.Statistic] cannot get guild info from Discord") + return nil, err + } + + // Discord API not count number of members in each role + // - need to get all guild member to see what roles they have and count + // - only allow 1000 members per request, so need to loop until all members are counted + after := "" + limit := 1000 + countRole := make(map[string]int64, 0) + + for { + guildMembers, err := e.discord.GuildMembers(guildId, after, limit) + if err != nil { + return nil, err + } + for _, member := range guildMembers { + for _, role := range member.Roles { + _, ok := countRole[role] + if !ok { + countRole[role] = 1 + } else { + countRole[role] = countRole[role] + 1 + } + + } + } + + if len(guildMembers) < limit { + break + } + after = guildMembers[len(guildMembers)-1].User.ID + } + + // mapping to response + guildReportRoles := make([]response.GuildReportRoleDetail, 0) + for _, role := range guildInfo.Roles { + if role.ID != guildId { + // change percentage: temp random value until implement database logic + rand.Seed(time.Now().UnixNano()) + changePercentage := util.RandFloats(0.0, 100.0) + guildReportRoles = append(guildReportRoles, response.GuildReportRoleDetail{ + Id: role.ID, + Name: role.Name, + NrOfMember: countRole[role.ID], + ChangePercentage: changePercentage, + }) + } + } + + return &response.GuildReportRoles{ + LastUpdated: time.Now(), + Roles: guildReportRoles, + }, nil +} diff --git a/pkg/handler/guild/guild.go b/pkg/handler/guild/guild.go index 71ee3b5d..68a64351 100644 --- a/pkg/handler/guild/guild.go +++ b/pkg/handler/guild/guild.go @@ -203,3 +203,21 @@ func (h *Handler) CreateGuild(c *gin.Context) { c.JSON(http.StatusOK, response.CreateResponse(response.ResponseMessage{Message: "OK"}, nil, nil, nil)) } + +func (h *Handler) GuildReportRoles(c *gin.Context) { + var req request.GuildRequest + if err := c.ShouldBindUri(&req); err != nil { + h.log.Fields(logger.Fields{"req": req}).Error(err, "[handler.Statistic] - failed to read query") + c.JSON(http.StatusBadRequest, response.CreateResponse[any](nil, nil, errors.New("invalid request"), nil)) + return + } + + resp, err := h.entities.GuildReportRoles(req.GuildId) + if err != nil { + h.log.Fields(logger.Fields{"req": req}).Error(err, "[handler.Statistic] - failed to statistic") + c.JSON(http.StatusInternalServerError, response.CreateResponse[any](nil, nil, err, nil)) + return + } + + c.JSON(http.StatusOK, response.CreateResponse(resp, nil, nil, nil)) +} diff --git a/pkg/handler/guild/interface.go b/pkg/handler/guild/interface.go index 46be9d73..76ca00ba 100644 --- a/pkg/handler/guild/interface.go +++ b/pkg/handler/guild/interface.go @@ -10,4 +10,6 @@ type IHandler interface { UpdateGuild(c *gin.Context) GetGuildRoles(c *gin.Context) ValidateUser(c *gin.Context) + // report + GuildReportRoles(c *gin.Context) } diff --git a/pkg/request/guild.go b/pkg/request/guild.go index 081acdb9..96f47bbc 100644 --- a/pkg/request/guild.go +++ b/pkg/request/guild.go @@ -30,3 +30,7 @@ type ValidateUserRequest struct { Ids string `form:"ids"` GuildId string `form:"guild_id"` } + +type GuildRequest struct { + GuildId string `uri:"guild_id" binding:"required"` +} diff --git a/pkg/response/guild.go b/pkg/response/guild.go index 6bee0dcb..0e0cd940 100644 --- a/pkg/response/guild.go +++ b/pkg/response/guild.go @@ -1,6 +1,8 @@ package response import ( + "time" + "github.com/bwmarrin/discordgo" "github.com/defipod/mochi/pkg/model" @@ -36,3 +38,15 @@ type DiscordGuildResponse struct { type ListMyGuildsResponse struct { Data []DiscordGuildResponse `json:"data"` } + +type GuildReportRoles struct { + Roles []GuildReportRoleDetail `json:"roles"` + LastUpdated time.Time `json:"last_updated"` +} + +type GuildReportRoleDetail struct { + Id string `json:"id"` + Name string `json:"name"` + NrOfMember int64 `json:"nr_of_member"` + ChangePercentage float64 `json:"change_percentage"` +} diff --git a/pkg/routes/v1.go b/pkg/routes/v1.go index 23f4f5d5..b85202e0 100644 --- a/pkg/routes/v1.go +++ b/pkg/routes/v1.go @@ -29,6 +29,11 @@ func NewRoutes(r *gin.Engine, h *handler.Handler, cfg config.Config) { guildGroup.GET("/user-managed", middleware.AuthGuard(cfg), h.Guild.ListMyGuilds) guildGroup.PUT("/:guild_id", h.Guild.UpdateGuild) guildGroup.GET("/validate-user", h.Guild.ValidateUser) + + reportGuildGroup := guildGroup.Group("/:guild_id/report") + { + reportGuildGroup.GET("/roles", h.Guild.GuildReportRoles) + } } profileGroup := v1.Group("/profiles") diff --git a/pkg/service/discord/discord.go b/pkg/service/discord/discord.go index 51cc2117..f6035c01 100644 --- a/pkg/service/discord/discord.go +++ b/pkg/service/discord/discord.go @@ -87,6 +87,16 @@ func (d *Discord) GetGuildEmojis() ([]*discordgo.Emoji, error) { return emojis, nil } +func (d *Discord) GuildWithCounts(guildId string) (*discordgo.Guild, error) { + guild, err := d.session.GuildWithCounts(guildId) + if err != nil { + d.log.Errorf(err, "[discord.GuildWithCounts] - failed to get guild with counts: %s", guildId) + return nil, err + } + + return guild, nil +} + func (d *Discord) NotifyNewGuild(guildID string, count int) error { // get new guild info guild, err := d.session.Guild(guildID) diff --git a/pkg/service/discord/mocks/service.go b/pkg/service/discord/mocks/service.go index c17b80c6..cc541936 100644 --- a/pkg/service/discord/mocks/service.go +++ b/pkg/service/discord/mocks/service.go @@ -171,6 +171,21 @@ func (mr *MockServiceMockRecorder) GetUser(userID interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockService)(nil).GetUser), userID) } +// GuildWithCounts mocks base method. +func (m *MockService) GuildWithCounts(guildId string) (*discordgo.Guild, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GuildWithCounts", guildId) + ret0, _ := ret[0].(*discordgo.Guild) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GuildWithCounts indicates an expected call of GuildWithCounts. +func (mr *MockServiceMockRecorder) GuildWithCounts(guildId interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GuildWithCounts", reflect.TypeOf((*MockService)(nil).GuildWithCounts), guildId) +} + // NotifyAddNewCollection mocks base method. func (m *MockService) NotifyAddNewCollection(guildID, collectionName, symbol, chain, image string) error { m.ctrl.T.Helper() diff --git a/pkg/service/discord/service.go b/pkg/service/discord/service.go index 17e86a64..ec2c4156 100644 --- a/pkg/service/discord/service.go +++ b/pkg/service/discord/service.go @@ -42,6 +42,7 @@ type Service interface { GetGuildMembers(guildID string) ([]*discordgo.Member, error) GetGuild(guildID string) (*discordgo.Guild, error) GetGuildRoles(guildID string) ([]*model.DiscordGuildRole, error) + GuildWithCounts(guildId string) (*discordgo.Guild, error) // User GetUser(userID string) (*discordgo.User, error) diff --git a/pkg/util/util.go b/pkg/util/util.go index 799aea81..56777f35 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -883,3 +883,7 @@ func ValidateFileMarkdown(s string) bool { return matched } + +func RandFloats(min, max float64) float64 { + return min + rand.Float64()*(max-min) +}