Skip to content

feat: implement AlwaysOnline presence feature#33

Open
edilsonoliveirama wants to merge 2 commits intoEvolutionAPI:mainfrom
edilsonoliveirama:feat/always-online
Open

feat: implement AlwaysOnline presence feature#33
edilsonoliveirama wants to merge 2 commits intoEvolutionAPI:mainfrom
edilsonoliveirama:feat/always-online

Conversation

@edilsonoliveirama
Copy link
Copy Markdown

@edilsonoliveirama edilsonoliveirama commented Apr 20, 2026

  • 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

Description

Related Issue

Closes #(issue_number)

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement

Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced

Screenshots (if applicable)

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have tested my changes thoroughly
  • Any dependent changes have been merged and published

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:

  • Introduce an AlwaysOnline-driven presence updater that marks the client as available on a fixed interval.
  • Allow enabling AlwaysOnline via advanced settings to start presence updates without reconnecting the client.

Enhancements:

  • Extract presence update logic into a pure handlePresenceTick function and abstract presence sending via a PresenceSender interface.
  • Simplify presence behavior by removing the previous random unavailable/available cycling logic.

Tests:

  • Add unit tests covering handlePresenceTick behavior for missing instances, disabled AlwaysOnline, successful updates, and presence send failures.

- 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
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 20, 2026

Reviewer's Guide

Implements 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 lifecycle

sequenceDiagram
    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
Loading

Class diagram for AlwaysOnline presence feature and collaborators

classDiagram
    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
Loading

File-Level Changes

Change Details Files
Gate the presence scheduler behind the AlwaysOnline flag and adjust its behavior.
  • Wrap schedulePresenceUpdates startup in a check for mycli.Instance.AlwaysOnline in myEventHandler so presence updates only run when AlwaysOnline is enabled.
  • Change schedulePresenceUpdates to call handlePresenceTick each minute and stop when the instance is missing or AlwaysOnline is false.
  • Update logging within schedulePresenceUpdates to record presence errors and successful available marks, and to log a clear stop reason when AlwaysOnline is disabled or the instance is not found.
  • Preserve killChannel handling so the goroutine exits cleanly on kill signals.
pkg/whatsmeow/service/whatsmeow.go
Introduce a testable presence tick function and abstraction over the WhatsApp client.
  • Add PresenceSender interface with SendPresence to decouple handlePresenceTick from the concrete whatsmeow client.
  • Implement handlePresenceTick to fetch the instance from the repository, early-stop when the instance is missing or AlwaysOnline is false, and send PresenceAvailable via the PresenceSender otherwise.
  • Wire schedulePresenceUpdates to use handlePresenceTick with mycli.instanceRepository and mycli.WAClient as the PresenceSender implementation.
pkg/whatsmeow/service/whatsmeow.go
Start presence updates dynamically when AlwaysOnline is toggled via advanced settings.
  • Track the previous AlwaysOnline value in UpdateInstanceAdvancedSettings before overwriting myClient.Instance.
  • When AlwaysOnline transitions from false to true and the WA client is connected, start schedulePresenceUpdates in a new goroutine without requiring a reconnect.
  • Log the dynamic startup of the presence goroutine when AlwaysOnline is enabled at runtime.
pkg/whatsmeow/service/whatsmeow.go
Remove the previous random presence toggling logic and related dependencies.
  • Delete processPresenceUpdates and its random unavailable/available cycle based on Sao Paulo time.
  • Remove math/rand usage and the variable-interval ticker logic from schedulePresenceUpdates, replacing it with a fixed 1-minute interval.
pkg/whatsmeow/service/whatsmeow.go
Add unit tests for the presence tick behavior using mocks.
  • Create mockInstanceRepository that minimally satisfies instance_repository.InstanceRepository for tests, with a compile-time interface conformance check.
  • Create mockPresenceSender implementing PresenceSender and recording SendPresence calls and presence type.
  • Add tests for handlePresenceTick covering: instance-not-found error, AlwaysOnline=false, AlwaysOnline=true with successful send, and AlwaysOnline=true with SendPresence error but no stop signal.
pkg/whatsmeow/service/whatsmeow_presence_test.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

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

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 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.
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>

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 thread pkg/whatsmeow/service/whatsmeow.go
- 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>
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