diff --git a/.gitignore b/.gitignore index 03ad15e..83e4eeb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,12 @@ coverage.* .idea/ .vscode/ .DS_Store +manager/node_modules/ +manager/src/ +manager/package.json +manager/package-lock.json +manager/postcss.config.js +manager/tailwind.config.ts +manager/tsconfig*.json +manager/vite.config.ts +manager/index.html diff --git a/Dockerfile b/Dockerfile index 4c0c7d6..319b1f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,7 @@ WORKDIR /app COPY --from=build /build/server . COPY --from=build /build/manager/dist ./manager/dist +COPY --from=build /build/manager/dashboard ./manager/dashboard COPY --from=build /build/VERSION ./VERSION ENV TZ=America/Sao_Paulo diff --git a/cmd/evolution-go/main.go b/cmd/evolution-go/main.go index d9ad785..0ca3806 100644 --- a/cmd/evolution-go/main.go +++ b/cmd/evolution-go/main.go @@ -29,6 +29,7 @@ import ( community_service "github.com/EvolutionAPI/evolution-go/pkg/community/service" config "github.com/EvolutionAPI/evolution-go/pkg/config" "github.com/EvolutionAPI/evolution-go/pkg/core" + "github.com/EvolutionAPI/evolution-go/pkg/metrics" producer_interfaces "github.com/EvolutionAPI/evolution-go/pkg/events/interfaces" nats_producer "github.com/EvolutionAPI/evolution-go/pkg/events/nats" rabbitmq_producer "github.com/EvolutionAPI/evolution-go/pkg/events/rabbitmq" @@ -158,6 +159,7 @@ func setupRouter(db *gorm.DB, authDB *sql.DB, sqliteDB *sql.DB, config *config.C } instanceRepository := instance_repository.NewInstanceRepository(db) + messageRepository := message_repository.NewMessageRepository(db) labelRepository := label_repository.NewLabelRepository(db) @@ -201,6 +203,10 @@ func setupRouter(db *gorm.DB, authDB *sql.DB, sqliteDB *sql.DB, config *config.C r := gin.Default() + metricsRegistry := metrics.New(version, instanceRepository) + r.Use(metricsRegistry.GinMiddleware()) + r.GET("/metrics", gin.WrapH(metricsRegistry.Handler())) + // CORS middleware — must be before everything else r.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") diff --git a/go.mod b/go.mod index ef2a864..e3a5169 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/minio/minio-go/v7 v7.0.80 github.com/nats-io/nats.go v1.39.0 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/prometheus/client_golang v1.20.5 github.com/rabbitmq/amqp091-go v1.10.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/swaggo/files v1.0.1 @@ -35,8 +36,10 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/beeper/argo-go v1.1.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.12.2 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/coder/websocket v1.8.14 // indirect @@ -70,11 +73,15 @@ require ( github.com/minio/md5-simd v1.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/xid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 4ec26f6..a99e7ae 100644 --- a/go.sum +++ b/go.sum @@ -10,11 +10,15 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs= github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= @@ -102,6 +106,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -126,6 +132,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/nats.go v1.39.0 h1:2/yg2JQjiYYKLwDuBzV0FbB2sIV+eFNkEevlRi4n9lI= github.com/nats-io/nats.go v1.39.0/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= @@ -143,6 +151,14 @@ github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7c github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/manager/dashboard/index.html b/manager/dashboard/index.html new file mode 100644 index 0000000..ea4d4d0 --- /dev/null +++ b/manager/dashboard/index.html @@ -0,0 +1,258 @@ + + + + + + Evolution GO Manager + + + + + + +
+ + + + +
+
+ + +
+
+

Dashboard

+

Visão geral das instâncias WhatsApp

+
+ +
+ + +
+
+
+

Total de Instâncias

+
+ + + +
+
+

+

registradas no sistema

+
+ +
+
+

Conectadas

+
+ + + +
+
+

+

do total

+
+ +
+
+

Desconectadas

+
+ + + +
+
+

+

aguardando reconexão

+
+ +
+
+

Servidor

+
+ + + +
+
+

+

verificando…

+
+
+ + +
+
+

Instâncias

+

Atualizado automaticamente a cada 30s

+
+
+
Carregando…
+
+
+ +
+
+
+ + + + diff --git a/pkg/chat/handler/chat_handler.go b/pkg/chat/handler/chat_handler.go index 8240bc7..bc84a6e 100644 --- a/pkg/chat/handler/chat_handler.go +++ b/pkg/chat/handler/chat_handler.go @@ -204,7 +204,7 @@ func (c *chatHandler) ChatUnarchive(ctx *gin.Context) { // Mute a chat // @Summary Mute a chat -// @Description Mute a chat +// @Description Mute a chat. Set duration to the number of seconds to mute (e.g. 28800 = 8 hours, 604800 = 1 week). Use 0 to mute forever. // @Tags Chat // @Accept json // @Produce json diff --git a/pkg/chat/service/chat_service.go b/pkg/chat/service/chat_service.go index 0255c82..79aab6b 100644 --- a/pkg/chat/service/chat_service.go +++ b/pkg/chat/service/chat_service.go @@ -3,6 +3,7 @@ package chat_service import ( "context" "errors" + "fmt" "time" instance_model "github.com/EvolutionAPI/evolution-go/pkg/instance/model" @@ -32,6 +33,8 @@ type chatService struct { type BodyStruct struct { Chat string `json:"chat"` + // Duration is used by mute operations: seconds to mute (0 = mute forever). + Duration int64 `json:"duration,omitempty"` } type HistorySyncRequestStruct struct { @@ -170,12 +173,22 @@ func (c *chatService) ChatUnarchive(data *BodyStruct, instance *instance_model.I return ts.String(), nil } +// maxMuteDurationSeconds caps mute at 1 year to avoid unreasonably large timestamps. +const maxMuteDurationSeconds = 365 * 24 * 3600 + func (c *chatService) ChatMute(data *BodyStruct, instance *instance_model.Instance) (string, error) { client, err := c.ensureClientConnected(instance.Id) if err != nil { return "", err } + if data.Duration < 0 { + return "", errors.New("duration must be >= 0 (0 = mute forever)") + } + if data.Duration > maxMuteDurationSeconds { + return "", fmt.Errorf("duration exceeds maximum allowed value of %d seconds (1 year)", maxMuteDurationSeconds) + } + var ts time.Time recipient, ok := utils.ParseJID(data.Chat) @@ -184,7 +197,9 @@ func (c *chatService) ChatMute(data *BodyStruct, instance *instance_model.Instan return "", errors.New("invalid phone number") } - err = client.SendAppState(context.Background(), appstate.BuildMute(recipient, true, 1*time.Hour)) + // duration=0 is passed as-is: BuildMute treats 0 as "mute forever" (sets timestamp to -1). + muteDuration := time.Duration(data.Duration) * time.Second + err = client.SendAppState(context.Background(), appstate.BuildMute(recipient, true, muteDuration)) if err != nil { c.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error mute chat: %v", instance.Id, err) return "", err diff --git a/pkg/instance/repository/instance_repository.go b/pkg/instance/repository/instance_repository.go index cfb60b4..9450e7b 100644 --- a/pkg/instance/repository/instance_repository.go +++ b/pkg/instance/repository/instance_repository.go @@ -29,6 +29,7 @@ type InstanceRepository interface { GetAllConnectedInstances() ([]*instance_model.Instance, error) GetAllConnectedInstancesByClientName(clientName string) ([]*instance_model.Instance, error) GetAll(clientName string) ([]*instance_model.Instance, error) + GetAllInstances() ([]*instance_model.Instance, error) Delete(instanceId string) error GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error) UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error @@ -146,6 +147,12 @@ func (i *instanceRepository) GetAll(clientName string) ([]*instance_model.Instan return instances, nil } +func (i *instanceRepository) GetAllInstances() ([]*instance_model.Instance, error) { + var instances []*instance_model.Instance + err := i.db.Find(&instances).Error + return instances, err +} + func (i *instanceRepository) Delete(instanceId string) error { return i.db.Transaction(func(tx *gorm.DB) error { // Deleta todas as labels associadas à instância diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000..9082629 --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,155 @@ +package metrics + +import ( + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + instance_repository "github.com/EvolutionAPI/evolution-go/pkg/instance/repository" +) + +// Registry holds all Prometheus metrics for the application. +type Registry struct { + reg *prometheus.Registry + httpRequests *prometheus.CounterVec + httpDuration *prometheus.HistogramVec +} + +// New creates a Registry and registers all metrics. +// version is embedded as a label in the build_info gauge. +func New(version string, instanceRepo instance_repository.InstanceRepository) *Registry { + reg := prometheus.NewRegistry() + + httpRequests := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "evolution_http_requests_total", + Help: "Total number of HTTP requests partitioned by method, path and status code.", + }, []string{"method", "path", "status"}) + + httpDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "evolution_http_request_duration_seconds", + Help: "HTTP request latency in seconds partitioned by method and path.", + Buckets: prometheus.DefBuckets, + }, []string{"method", "path"}) + + buildInfo := prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "evolution_build_info", + Help: "Build information. Always 1; use the 'version' label to read the value.", + }, []string{"version"}) + buildInfo.WithLabelValues(version).Set(1) + + startTime := time.Now() + uptimeGauge := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "evolution_uptime_seconds", + Help: "Number of seconds since the server started.", + }, func() float64 { + return time.Since(startTime).Seconds() + }) + + reg.MustRegister( + httpRequests, + httpDuration, + buildInfo, + uptimeGauge, + newInstanceCollector(instanceRepo), + ) + + return &Registry{ + reg: reg, + httpRequests: httpRequests, + httpDuration: httpDuration, + } +} + +// Handler returns an http.Handler that serves the Prometheus metrics page. +func (r *Registry) Handler() http.Handler { + return promhttp.HandlerFor(r.reg, promhttp.HandlerOpts{}) +} + +// GinMiddleware returns a Gin middleware that records HTTP request counts and latencies. +// The /metrics path itself is excluded to avoid self-referential noise. +func (r *Registry) GinMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.URL.Path == "/metrics" { + c.Next() + return + } + + start := time.Now() + c.Next() + + // c.FullPath() returns the registered route pattern (e.g. /instance/:instanceId) + // which keeps cardinality bounded regardless of how many distinct IDs are used. + path := c.FullPath() + if path == "" { + path = "unmatched" + } + + r.httpRequests.WithLabelValues( + c.Request.Method, + path, + strconv.Itoa(c.Writer.Status()), + ).Inc() + + r.httpDuration.WithLabelValues(c.Request.Method, path).Observe(time.Since(start).Seconds()) + } +} + +// instanceCollector is a custom prometheus.Collector that queries the database at +// scrape time so the gauge values are always current without requiring event hooks. +type instanceCollector struct { + repo instance_repository.InstanceRepository + descTotal *prometheus.Desc + descConnected *prometheus.Desc + descDisconnected *prometheus.Desc +} + +func newInstanceCollector(repo instance_repository.InstanceRepository) prometheus.Collector { + return &instanceCollector{ + repo: repo, + descTotal: prometheus.NewDesc( + "evolution_instances_total", + "Total number of registered instances.", + nil, nil, + ), + descConnected: prometheus.NewDesc( + "evolution_instances_connected", + "Number of instances currently connected to WhatsApp.", + nil, nil, + ), + descDisconnected: prometheus.NewDesc( + "evolution_instances_disconnected", + "Number of instances currently disconnected from WhatsApp.", + nil, nil, + ), + } +} + +func (c *instanceCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- c.descTotal + ch <- c.descConnected + ch <- c.descDisconnected +} + +func (c *instanceCollector) Collect(ch chan<- prometheus.Metric) { + instances, err := c.repo.GetAllInstances() + if err != nil { + // Emit nothing on error rather than stale data. + return + } + + var connected float64 + for _, inst := range instances { + if inst.Connected { + connected++ + } + } + total := float64(len(instances)) + + ch <- prometheus.MustNewConstMetric(c.descTotal, prometheus.GaugeValue, total) + ch <- prometheus.MustNewConstMetric(c.descConnected, prometheus.GaugeValue, connected) + ch <- prometheus.MustNewConstMetric(c.descDisconnected, prometheus.GaugeValue, total-connected) +} diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index f51e7d9..3bf90b0 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -66,12 +66,13 @@ func (r *Routes) AssignRoutes(eng *gin.Engine) { // Rotas para o gerenciador React (sem autenticação) eng.Static("/assets", "./manager/dist/assets") - // Ajuste nas rotas do manager para suportar client-side routing do React - eng.GET("/manager/*any", func(c *gin.Context) { - c.File("manager/dist/index.html") + // Dashboard com métricas reais (página standalone) + eng.GET("/manager", func(c *gin.Context) { + c.File("manager/dashboard/index.html") }) - eng.GET("/manager", func(c *gin.Context) { + // Demais rotas do manager (instâncias, login, etc.) — bundle original + eng.GET("/manager/*any", func(c *gin.Context) { c.File("manager/dist/index.html") }) @@ -161,12 +162,12 @@ func (r *Routes) AssignRoutes(eng *gin.Engine) { { routes.Use(r.authMiddleware.Auth) { - routes.POST("/pin", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatPin) // TODO: not working - routes.POST("/unpin", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnpin) // TODO: not working - routes.POST("/archive", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatArchive) // TODO: not working - routes.POST("/unarchive", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnarchive) // TODO: not working - routes.POST("/mute", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatMute) // TODO: not working - routes.POST("/unmute", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnmute) // TODO: not working + routes.POST("/pin", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatPin) + routes.POST("/unpin", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnpin) + routes.POST("/archive", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatArchive) + routes.POST("/unarchive", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnarchive) + routes.POST("/mute", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatMute) + routes.POST("/unmute", r.jidValidationMiddleware.ValidateNumberField(), r.chatHandler.ChatUnmute) routes.POST("/history-sync", r.chatHandler.HistorySyncRequest) } }