feat: implement AlwaysOnline presence feature#33
Open
edilsonoliveirama wants to merge 2 commits intoEvolutionAPI:mainfrom
Open
feat: implement AlwaysOnline presence feature#33edilsonoliveirama wants to merge 2 commits intoEvolutionAPI:mainfrom
edilsonoliveirama wants to merge 2 commits intoEvolutionAPI:mainfrom
Conversation
- Gate schedulePresenceUpdates goroutine behind AlwaysOnline flag (previously ran unconditionally for every instance) - Refactor to extract handlePresenceTick as a pure, testable function - Add PresenceSender interface to decouple from whatsmeow.Client - Start presence goroutine dynamically when AlwaysOnline is toggled ON via the advanced-settings API without requiring reconnect - Remove processPresenceUpdates (random unavailable/available cycle) - Add unit tests covering all 4 tick scenarios
Reviewer's GuideImplements an AlwaysOnline-driven presence mechanism by gating the presence ticker behind the AlwaysOnline flag, extracting a pure handlePresenceTick function with a PresenceSender interface for testability, starting/stopping the presence goroutine based on runtime settings, and adding unit tests for the new presence behavior. Sequence diagram for AlwaysOnline-driven presence ticker lifecyclesequenceDiagram
actor User
participant AdminAPI as whatsmeowService
participant MyClient
participant WAClient
participant Repo as InstanceRepository
participant Ticker as schedulePresenceUpdates
rect rgb(230,230,250)
User->>AdminAPI: UpdateInstanceAdvancedSettings(instanceId)
AdminAPI->>Repo: GetInstanceByID(instanceId)
Repo-->>AdminAPI: Instance (AlwaysOnline toggled)
AdminAPI->>MyClient: Load runtime instance
AdminAPI->>MyClient: Update Instance field
alt AlwaysOnline changed from false to true and WAClient connected
AdminAPI->>Ticker: go schedulePresenceUpdates(MyClient)
end
AdminAPI-->>User: success
end
rect rgb(220,245,255)
loop every minute
Ticker->>Repo: GetInstanceByID(MyClient.userID)
alt error or instance not found
Repo-->>Ticker: error
Ticker->>MyClient: log Presence update error
Ticker->>MyClient: log Stopping presence updates
Ticker-->>x Ticker: stop goroutine
else instance found
Repo-->>Ticker: Instance
alt Instance.AlwaysOnline is false
Ticker->>MyClient: log Stopping presence updates
Ticker-->>x Ticker: stop goroutine
else Instance.AlwaysOnline is true
Ticker->>WAClient: SendPresence(PresenceAvailable)
WAClient-->>Ticker: result
Ticker->>MyClient: log Marked self as available
end
end
end
end
rect rgb(255,235,230)
MyClient->>MyClient: close killChannel[userID]
MyClient->>Ticker: kill signal
Ticker->>MyClient: log Received kill signal
Ticker-->>x Ticker: stop goroutine
end
Class diagram for AlwaysOnline presence feature and collaboratorsclassDiagram
class PresenceSender {
<<interface>>
+SendPresence(ctx Context, p Presence) error
}
class MyClient {
+string userID
+Instance Instance
+InstanceRepository instanceRepository
+PresenceSender WAClient
+map~string~chan bool killChannel
+LoggerWrapper loggerWrapper
+myEventHandler(rawEvt interfaceAny)
}
class whatsmeowService {
+UpdateInstanceAdvancedSettings(instanceId string) error
}
class Instance {
+string ID
+bool AlwaysOnline
}
class InstanceRepository {
+GetInstanceByID(userID string) (Instance, error)
}
class WAClient {
+SendPresence(ctx Context, p Presence) error
+IsConnected() bool
}
class LoggerWrapper {
+GetLogger(userID string) Logger
}
class Logger {
+LogInfo(format string, args ...interfaceAny)
+LogError(format string, args ...interfaceAny)
}
class Presence {
}
class Context {
}
class schedulePresenceUpdates {
+schedulePresenceUpdates(mycli *MyClient)
}
class handlePresenceTickFn {
+handlePresenceTick(ctx Context, userID string, repo InstanceRepository, sender PresenceSender) (bool, error)
}
PresenceSender <|.. WAClient
MyClient o-- Instance
MyClient o-- InstanceRepository
MyClient o-- PresenceSender
MyClient o-- LoggerWrapper
whatsmeowService o-- MyClient
whatsmeowService o-- LoggerWrapper
schedulePresenceUpdates ..> MyClient
schedulePresenceUpdates ..> handlePresenceTickFn
handlePresenceTickFn ..> InstanceRepository
handlePresenceTickFn ..> PresenceSender
WAClient ..|> PresenceSender
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
schedulePresenceUpdates, consider threading a cancellable context through instead of usingcontext.Background()so the presence loop can respect broader service shutdown and avoid potential goroutine leaks beyond the killChannel lifecycle. - The new
PresenceSenderinterface is only used forSendPresence; if you plan to extend presence-related behavior later, you might want to define a narrower, presence-specific interface in a dedicated file to avoid overcoupling to the WhatsApp client type. - In the test
mockInstanceRepository, the unimplemented methods silently return zero values; having thempanicinstead would make it easier to catch accidental usage of methods that are not supposed to be exercised in the presence tests.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `schedulePresenceUpdates`, consider threading a cancellable context through instead of using `context.Background()` so the presence loop can respect broader service shutdown and avoid potential goroutine leaks beyond the killChannel lifecycle.
- The new `PresenceSender` interface is only used for `SendPresence`; if you plan to extend presence-related behavior later, you might want to define a narrower, presence-specific interface in a dedicated file to avoid overcoupling to the WhatsApp client type.
- In the test `mockInstanceRepository`, the unimplemented methods silently return zero values; having them `panic` instead would make it easier to catch accidental usage of methods that are not supposed to be exercised in the presence tests.
## Individual Comments
### Comment 1
<location path="pkg/whatsmeow/service/whatsmeow.go" line_range="802-803" />
<code_context>
+
+// handlePresenceTick fetches the instance state and sends PresenceAvailable when AlwaysOnline
+// is enabled. Returns shouldStop=true when the goroutine should exit (instance gone or flag off).
+func handlePresenceTick(ctx context.Context, userID string, repo instance_repository.InstanceRepository, sender PresenceSender) (shouldStop bool, err error) {
+ instance, err := repo.GetInstanceByID(userID)
+ if err != nil {
+ return true, err
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid treating all repository errors as terminal for the presence goroutine.
Since any error from `GetInstanceByID` sets `shouldStop=true`, transient DB/network failures will permanently stop `schedulePresenceUpdates`. Consider only returning `shouldStop=true` for a definitive "instance not found" case, and for other errors just log and let the goroutine retry on the next tick, so `AlwaysOnline` keeps working through temporary backend issues.
Suggested implementation:
```golang
// handlePresenceTick fetches the instance state and sends PresenceAvailable when AlwaysOnline
// is enabled. Returns shouldStop=true when the goroutine should exit (instance gone or flag off).
func handlePresenceTick(ctx context.Context, userID string, repo instance_repository.InstanceRepository, sender PresenceSender) (shouldStop bool, err error) {
instance, err := repo.GetInstanceByID(userID)
if err != nil {
// Only stop the goroutine when the instance is definitively gone.
// For other (transient) errors, keep the goroutine alive and retry on the next tick.
if errors.Is(err, instance_repository.ErrInstanceNotFound) {
return true, nil
}
return false, nil
}
if !instance.AlwaysOnline {
return true, nil
}
if err := sender.SendPresence(ctx, types.PresenceAvailable); err != nil {
return false, err
}
return false, nil
}
```
To fully implement this change you will also need to:
1. Ensure the `errors` package is imported in `pkg/whatsmeow/service/whatsmeow.go`:
- Add `errors` to the existing import block, e.g.:
```go
import (
"errors"
// ...existing imports...
)
```
2. Make sure `instance_repository.ErrInstanceNotFound` (or an equivalent sentinel error) exists:
- If your repository uses a different name or pattern (e.g. `sql.ErrNoRows` or a custom `IsNotFound(err)` helper), update the `errors.Is(...)` check accordingly:
```go
if errors.Is(err, sql.ErrNoRows) { ... }
// or
if instance_repository.IsNotFound(err) { ... }
```
3. Optionally, if you want observability on transient failures, add logging inside the non-terminal error branch:
```go
if !errors.Is(err, instance_repository.ErrInstanceNotFound) {
myLogger.Warnf("presence tick failed for user %s: %v", userID, err)
return false, nil
}
```
wiring `myLogger` consistently with the rest of this file.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- handlePresenceTick: transient DB/network errors no longer kill the goroutine permanently; only ErrInstanceNotFound (record gone) triggers shouldStop=true so AlwaysOnline survives temporary backend issues - instance_repository: add ErrInstanceNotFound sentinel and wrap gorm.ErrRecordNotFound so callers can distinguish not-found from transient failures without importing gorm directly - schedulePresenceUpdates: accepts context.Context so callers can wire a cancellable/service-level context; adds ctx.Done() select case as an additional shutdown path alongside killChannel - test mocks: unimplemented repository methods now panic instead of silently returning zero values, making accidental usage detectable; added TestHandlePresenceTick_TransientRepoError to cover the new retry-on-transient-error behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Related Issue
Closes #(issue_number)
Type of Change
Testing
Screenshots (if applicable)
Checklist
Additional Notes
Summary by Sourcery
Gate periodic presence updates behind the AlwaysOnline setting and allow them to be started dynamically when the flag is enabled at runtime.
New Features:
Enhancements:
Tests: