Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ coverage.*
.idea/
.vscode/
.DS_Store

# ImobDeal — arquivos locais pra rodar o Evolution Go no PC
docker-compose.yml
init-db.sql
deploy_ssh.py
check_deploy.py
*.local.py
43 changes: 43 additions & 0 deletions pkg/instance/handler/instance_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type InstanceHandler interface {
Connect(ctx *gin.Context)
Reconnect(ctx *gin.Context)
Disconnect(ctx *gin.Context)
SetPresence(ctx *gin.Context)
Logout(ctx *gin.Context)
Delete(ctx *gin.Context)
Status(ctx *gin.Context)
Expand Down Expand Up @@ -205,6 +206,48 @@ func (i *instanceHandler) Disconnect(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"message": "success"})
}

// IMOBDEAL PATCH: SetPresence endpoint
// @Summary Set instance presence (available/unavailable)
// @Description Marks the instance as available or unavailable WITHOUT disconnecting.
// @Description Mimics WhatsApp Web's behavior when minimized — the phone resumes
// @Description receiving push notifications while the linked device stays connected.
// @Tags Instance
// @Accept json
// @Produce json
// @Param body body instance_service.SetPresenceStruct true "State: available or unavailable"
// @Success 200 {object} gin.H "Presence updated"
// @Failure 400 {object} gin.H "Invalid state"
// @Failure 500 {object} gin.H "Internal server error"
// @Router /instance/presence [post]
func (i *instanceHandler) SetPresence(ctx *gin.Context) {
getInstance := ctx.MustGet("instance")

instance, ok := getInstance.(*instance_model.Instance)
if !ok {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "instance not found"})
return
}

var data instance_service.SetPresenceStruct
if err := ctx.ShouldBindJSON(&data); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Validate input here so misuse returns 4xx and service errors remain 5xx
if data.State != "available" && data.State != "unavailable" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid state, must be 'available' or 'unavailable'"})
return
}

if err := i.instanceService.SetPresence(ctx.Request.Context(), &data, instance); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

ctx.JSON(http.StatusOK, gin.H{"message": "success", "state": data.State})
}

// Logout from instance
// @Summary Logout from instance
// @Description Logout from instance
Expand Down
43 changes: 43 additions & 0 deletions pkg/instance/service/instance_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type InstanceService interface {
Connect(data *ConnectStruct, instance *instance_model.Instance) (*instance_model.Instance, string, string, error)
Reconnect(instance *instance_model.Instance) error
Disconnect(instance *instance_model.Instance) (*instance_model.Instance, error)
SetPresence(ctx context.Context, data *SetPresenceStruct, instance *instance_model.Instance) error
Logout(instance *instance_model.Instance) (*instance_model.Instance, error)
Status(instance *instance_model.Instance) (*StatusStruct, error)
GetQr(instance *instance_model.Instance) (*QrcodeStruct, error)
Expand Down Expand Up @@ -79,6 +80,12 @@ type ConnectStruct struct {
NatsEnable string `json:"natsEnable"`
}

// IMOBDEAL PATCH: struct para a rota POST /instance/presence
// state deve ser "available" ou "unavailable"
type SetPresenceStruct struct {
State string `json:"state" binding:"required"`
}

type StatusStruct struct {
Connected bool
LoggedIn bool
Expand Down Expand Up @@ -319,6 +326,42 @@ func (i instances) Disconnect(instance *instance_model.Instance) (*instance_mode
return instance, nil
}

// IMOBDEAL PATCH: SetPresence expõe SendPresence do whatsmeow como endpoint HTTP.
// Permite marcar o cliente como "available" (ativo, intercepta push do celular) ou
// "unavailable" (standby, celular volta a receber push) sem desconectar a sessão.
// Idêntico ao comportamento do WhatsApp Web quando minimizado/em background.
// A validação do campo State acontece no handler HTTP (retorna 400 em caso de valor inválido);
// qualquer erro que chega aqui é considerado 5xx.
func (i instances) SetPresence(ctx context.Context, data *SetPresenceStruct, instance *instance_model.Instance) error {
client, err := i.ensureClientConnected(instance.Id)
if err != nil {
return err
}

if !client.IsConnected() || !client.IsLoggedIn() {
return errors.New("client is not connected or not logged in")
}

var state types.Presence
switch data.State {
case "available":
state = types.PresenceAvailable
case "unavailable":
state = types.PresenceUnavailable
default:
// Should never reach here because handler validates first, but keep as safety net.
return errors.New("invalid state")
}

if err := client.SendPresence(ctx, state); err != nil {
i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] SetPresence failed: %v", instance.Id, err)
return err
}

i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Presence set to %s", instance.Id, data.State)
return nil
}

func (i instances) Logout(instance *instance_model.Instance) (*instance_model.Instance, error) {
client, err := i.ensureClientConnected(instance.Id)
if err != nil {
Expand Down
9 changes: 7 additions & 2 deletions pkg/message/service/message_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,17 +406,22 @@ func (m *messageService) EditMessage(data *EditMessageStruct, instance *instance
return "", "", errors.New("invalid phone number")
}

// IMOBDEAL PATCH: usar ExtendedTextMessage em vez de Conversation.
// SendText envia como ExtendedTextMessage; o WhatsApp ignora silenciosamente
// edições quando o tipo difere do original.
resp, err := client.SendMessage(
context.Background(),
recipient,
client.BuildEdit(
recipient,
data.MessageID,
&waE2E.Message{
Conversation: proto.String(data.Message),
ExtendedTextMessage: &waE2E.ExtendedTextMessage{
Text: &data.Message,
},
}))
if err != nil {
m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error revoking message: %v", instance.Id, err)
m.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error editing message: %v", instance.Id, err)
return "", "", err
}

Expand Down
1 change: 1 addition & 0 deletions pkg/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func (r *Routes) AssignRoutes(eng *gin.Engine) {
routes.POST("/pair", r.jidValidationMiddleware.ValidateNumberField(), r.instanceHandler.Pair)
routes.POST("/disconnect", r.instanceHandler.Disconnect)
routes.POST("/reconnect", r.instanceHandler.Reconnect)
routes.POST("/presence", r.instanceHandler.SetPresence) // IMOBDEAL PATCH
routes.DELETE("/logout", r.instanceHandler.Logout)
routes.GET("/:instanceId/advanced-settings", r.instanceHandler.GetAdvancedSettings)
routes.PUT("/:instanceId/advanced-settings", r.instanceHandler.UpdateAdvancedSettings)
Expand Down