Skip to content

feat: adiciona endpoint POST /user/savecontact#119

Open
samuelpc7 wants to merge 1 commit into
evolution-foundation:mainfrom
samuelpc7:feature/save-contact
Open

feat: adiciona endpoint POST /user/savecontact#119
samuelpc7 wants to merge 1 commit into
evolution-foundation:mainfrom
samuelpc7:feature/save-contact

Conversation

@samuelpc7

@samuelpc7 samuelpc7 commented Jul 17, 2026

Copy link
Copy Markdown

Descrição

Adiciona o endpoint POST /user/savecontact para salvar/atualizar um contato na
lista de contatos do WhatsApp da instância, via app state patch (coleção
critical_unblock_low, índice contact) — o mesmo mecanismo usado pela tela
"Novo contato" do WhatsApp Web.

Quando saveOnPhone: true (padrão), seta ContactAction.SaveOnPrimaryAddressbook,
fazendo o dispositivo primário (celular) gravar o contato também na agenda do
sistema (equivale ao toggle "Sincronizar contato com celular").

Motivação

Hoje a API expõe GET /user/contacts para ler contatos, mas não há forma de
adicionar/atualizar um contato programaticamente. Isso é útil para fluxos de
CRM/onboarding que querem nomear os contatos automaticamente.

Como funciona

  • Usa client.SendAppState com appstate.WAPatchCriticalUnblockLow.
  • Não requer bump de dependência — as APIs de appstate/waSyncAction já existem
    no whatsmeow fixado no projeto.
  • O JID é normalizado (remove + inicial) para bater com o padrão dos demais
    contatos, garantindo a gravação na agenda do celular primário.

Contrato

Request
POST /user/savecontact
Header: apikey: <token da instância>
{
"number": "5582988898565",
"fullName": "Fulano de Tal",
"firstName": "Fulano", // opcional; fallback = 1ª palavra de fullName
"saveOnPhone": true // opcional; padrão true
}

Response 200
{ "message": "success" }

Requisitos de runtime

  • As app state keys precisam já estar sincronizadas (ocorre após o pareamento).
  • O celular primário precisa estar online para propagar a gravação na agenda.
  • Se faltar chave, o whatsmeow retorna no app state keys found.

Arquivos alterados

  • pkg/user/service/user_service.goSaveContact + SaveContactStruct
  • pkg/user/handler/user_handler.go — handler SaveContact (com anotações Swagger)
  • pkg/routes/routes.go — rota POST /user/savecontact

Checklist

  • Código segue os padrões do projeto
  • Commit messages descritivas
  • Anotações Swagger adicionadas no handler
  • make swagger — não rodei localmente (Windows sem make); posso regenerar se preferirem

@sourcery-ai

sourcery-ai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new POST /user/savecontact endpoint that saves or updates a WhatsApp contact via app state patch, including optional synchronization with the primary phone’s address book.

Sequence diagram for POST /user/savecontact contact saving flow

sequenceDiagram
  actor User
  participant GinRouter as GinRouter
  participant JidValidationMiddleware as JidValidationMiddleware
  participant UserHandler as UserHandler
  participant UserService as UserService
  participant WhatsmeowClient as WhatsmeowClient

  User->>GinRouter: POST /user/savecontact
  GinRouter->>JidValidationMiddleware: ValidateNumberField
  JidValidationMiddleware-->>GinRouter: validated number
  GinRouter->>UserHandler: SaveContact(ctx)
  UserHandler->>UserHandler: ShouldBindBodyWithJSON
  UserHandler->>UserService: SaveContact(data, instance)
  UserService->>UserService: ensureClientConnected(instance.Id)
  UserService->>UserService: utils.ParseJID(data.Number)
  UserService->>WhatsmeowClient: SendAppState(patch)
  alt [SaveOnPrimaryAddressbook=true]
    WhatsmeowClient-->>PrimaryPhone: [contact synced to phone address book]
  end
  UserHandler-->>User: 200 OK {"message":"success"}
Loading

File-Level Changes

Change Details Files
Introduce SaveContact service method to create/update contacts via WhatsApp app state patches with optional phone address book sync.
  • Extend UserService interface with SaveContact method
  • Define SaveContactStruct request body with number, fullName, optional firstName and saveOnPhone fields
  • Normalize JID phone number by stripping '+' and validate inputs in SaveContact
  • Derive firstName from fullName when not provided and default saveOnPhone to true
  • Construct appstate.PatchInfo with contact mutation using waSyncAction.ContactAction and send via client.SendAppState
  • Add structured logging for success and error cases when saving contacts
pkg/user/service/user_service.go
Expose POST /user/savecontact HTTP endpoint that validates input and delegates to the SaveContact service method.
  • Extend UserHandler interface with SaveContact handler
  • Implement SaveContact handler: resolve instance from context, bind JSON body into SaveContactStruct, validate number and fullName, call service.SaveContact, and return appropriate HTTP status codes
  • Add Swagger annotations documenting request body, success and error responses for /user/savecontact
pkg/user/handler/user_handler.go
Wire the new SaveContact handler into the routing layer with JID validation middleware.
  • Register POST /user/savecontact route under /user group
  • Apply jidValidationMiddleware.ValidateNumberField() to the savecontact route before invoking userHandler.SaveContact
pkg/routes/routes.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In SaveContact handler, ShouldBindBodyWithJSON is called with &data where data is already a pointer type; consider using a non-pointer struct (var data user_service.SaveContactStruct) and passing &data to avoid binding into a **SaveContactStruct.
  • In UserService.SaveContact, you currently use strings.ReplaceAll to strip '+' from the JID user; if the intention is only to remove a leading '+', strings.TrimPrefix(jid.User, "+") would be safer and clearer.
  • When calling client.SendAppState, you use context.Background(); consider threading the request-scoped context from the Gin handler into the service so app state updates can be canceled if the client disconnects.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `SaveContact` handler, `ShouldBindBodyWithJSON` is called with `&data` where `data` is already a pointer type; consider using a non-pointer struct (`var data user_service.SaveContactStruct`) and passing `&data` to avoid binding into a `**SaveContactStruct`.
- In `UserService.SaveContact`, you currently use `strings.ReplaceAll` to strip '+' from the JID user; if the intention is only to remove a leading '+', `strings.TrimPrefix(jid.User, "+")` would be safer and clearer.
- When calling `client.SendAppState`, you use `context.Background()`; consider threading the request-scoped context from the Gin handler into the service so app state updates can be canceled if the client disconnects.

## Individual Comments

### Comment 1
<location path="pkg/user/handler/user_handler.go" line_range="208-209" />
<code_context>
+		return
+	}
+
+	var data *user_service.SaveContactStruct
+	err := ctx.ShouldBindBodyWithJSON(&data)
+	if err != nil {
+		ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
</code_context>
<issue_to_address>
**issue (bug_risk):** Request body binding is using a pointer to pointer, which is unlikely to work as intended.

`data` is declared as `*user_service.SaveContactStruct`, but `&data` is passed to `ShouldBindBodyWithJSON`, producing a `**SaveContactStruct`. Gin expects a single struct pointer. Declare `var data user_service.SaveContactStruct` and call `ctx.ShouldBindBodyWithJSON(&data)` instead, otherwise the body is unlikely to bind correctly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +208 to +209
var data *user_service.SaveContactStruct
err := ctx.ShouldBindBodyWithJSON(&data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Request body binding is using a pointer to pointer, which is unlikely to work as intended.

data is declared as *user_service.SaveContactStruct, but &data is passed to ShouldBindBodyWithJSON, producing a **SaveContactStruct. Gin expects a single struct pointer. Declare var data user_service.SaveContactStruct and call ctx.ShouldBindBodyWithJSON(&data) instead, otherwise the body is unlikely to bind correctly.

@samuelpc7
samuelpc7 force-pushed the feature/save-contact branch from 925c88e to 4b6450e Compare July 17, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant