Skip to content
Open
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
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ WUZAPI_WEBHOOK_USE_PROXY=true

# WuzAPI Session Configuration
SESSION_DEVICE_NAME=WuzAPI
# "available" (legacy default) or "unavailable" (preserves phone push notifications)
WUZAPI_AUTO_PRESENCE=available

# Database configuration
DB_USER=wuzapi
Expand Down
6 changes: 4 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1011,8 +1011,10 @@ curl -X POST -H 'Token: 1234ABCD' -H 'Content-Type: application/json' --data '{"

Subscribes to a contact's presence updates (online/offline and last seen). After
subscribing, your configured webhook receives `Presence` events for that contact. You
should be online yourself to receive presence (wuzapi sends an available presence on
connect). Whether `last_seen` is available depends on the contact's privacy settings.
should be online yourself to receive presence. By default WuzAPI sends an available
presence on connect. When `WUZAPI_AUTO_PRESENCE=unavailable` is configured to preserve
primary-phone push notifications, set your global presence to available before
subscribing. Whether `last_seen` is available depends on the contact's privacy settings.

endpoint: _/user/presence/subscribe_

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ you can use to alter behaviour
* -logtype : format for logs, either console (default) or json
* -color : enable colored output for console logs
* -osname : Connection OS Name in Whatsapp
* -autopresence : automatic presence after connecting, either available (default) or unavailable
* -skipmedia : Skip downloading media from messages
* -wadebug : enable whatsmeow debug, either INFO or DEBUG levels are suported

Expand Down Expand Up @@ -127,6 +128,7 @@ WUZAPI_GLOBAL_HMAC_KEY=your_global_hmac_key_here
TZ=America/New_York
WEBHOOK_FORMAT=json
SESSION_DEVICE_NAME=WuzAPI
WUZAPI_AUTO_PRESENCE=available
WUZAPI_PORT=8080
WUZAPI_GLOBAL_WEBHOOK=https://your-global-webhook.url
WEBHOOK_RETRY_ENABLED=true
Expand Down Expand Up @@ -169,8 +171,14 @@ WEBHOOK_FORMAT=json # or "form" for the default
SESSION_DEVICE_NAME=WuzAPI
WUZAPI_PORT=8080 # Port for the WuzAPI server
WUZAPI_GLOBAL_WEBHOOK= # Global webhook URL for all instances
WUZAPI_AUTO_PRESENCE=available # use unavailable to preserve primary-phone push notifications
```

`WUZAPI_AUTO_PRESENCE` controls the presence announced after a session connects or
its push name changes. The default `available` value preserves the existing behavior
and enables contact presence updates. Set it to `unavailable` to keep the linked
client offline so WhatsApp continues sending push notifications to the primary phone.

### RabbitMQ Integration
WuzAPI supports sending WhatsApp events to a RabbitMQ queue for global event distribution. When enabled, all WhatsApp events will be published to the specified queue regardless of individual user webhook configurations.

Expand Down
1 change: 1 addition & 0 deletions docker-compose-swarm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ services:
- TZ=${TZ:-America/Sao_Paulo}
- WEBHOOK_FORMAT=${WEBHOOK_FORMAT:-json}
- SESSION_DEVICE_NAME=${SESSION_DEVICE_NAME:-WuzAPI}
- WUZAPI_AUTO_PRESENCE=${WUZAPI_AUTO_PRESENCE:-available}
# RabbitMQ configuration Optional
- RABBITMQ_URL=amqp://wuzapi:wuzapi@rabbitmq:5672/
- RABBITMQ_QUEUE=whatsapp_events
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ services:
- TZ=${TZ:-America/Sao_Paulo}
- WEBHOOK_FORMAT=${WEBHOOK_FORMAT:-json}
- SESSION_DEVICE_NAME=${SESSION_DEVICE_NAME:-WuzAPI}
- WUZAPI_AUTO_PRESENCE=${WUZAPI_AUTO_PRESENCE:-available}
# RabbitMQ configuration Optional
- RABBITMQ_URL=amqp://wuzapi:wuzapi@rabbitmq:5672/
- RABBITMQ_QUEUE=whatsapp_events
Expand Down
25 changes: 25 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"time"

"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
waLog "go.mau.fi/whatsmeow/util/log"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -52,6 +53,7 @@ var (
skipMedia = flag.Bool("skipmedia", false, "Do not attempt to download media in messages")
osName = flag.String("osname", "Mac OS 10", "Connection OSName in Whatsapp")
platformType = flag.String("platformtype", "DESKTOP", "Device platform type (DESKTOP, IPAD, ANDROID_TABLET, IOS_PHONE, ANDROID_PHONE, etc.)")
autoPresenceMode = flag.String("autopresence", "available", "Automatic presence after connecting (available or unavailable)")
colorOutput = flag.Bool("color", false, "Enable colored output for console logs")
sslcert = flag.String("sslcertificate", "", "SSL Certificate File")
sslprivkey = flag.String("sslprivatekey", "", "SSL Certificate Private Key File")
Expand All @@ -64,6 +66,7 @@ var (
dataDir = flag.String("datadir", "", "Data directory for database and session files (defaults to executable directory)")

globalHMACKeyEncrypted []byte
automaticPresence = types.PresenceAvailable

webhookRetryEnabled = flag.Bool("webhookretry", true, "Enable webhook retry mechanism")
webhookRetryCount = flag.Int("retrycount", 5, "Number of times to retry failed webhooks")
Expand All @@ -84,6 +87,17 @@ var privateIPBlocks []*net.IPNet

const version = "1.0.8"

func parseAutomaticPresence(value string) (types.Presence, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case string(types.PresenceAvailable):
return types.PresenceAvailable, nil
case string(types.PresenceUnavailable):
return types.PresenceUnavailable, nil
default:
return "", fmt.Errorf("invalid automatic presence %q: expected available or unavailable", value)
}
}

// killchannel maps a userID to its session goroutine's kill channel. It is
// accessed from HTTP request goroutines (Connect/Disconnect/logout/delete) and
// from the per-session startClient goroutine, so every map operation must be
Expand Down Expand Up @@ -288,6 +302,17 @@ func main() {
*platformType = v
}

if v := os.Getenv("WUZAPI_AUTO_PRESENCE"); v != "" {
*autoPresenceMode = v
}

configuredPresence, err := parseAutomaticPresence(*autoPresenceMode)
if err != nil {
log.Fatal().Err(err).Msg("Invalid automatic session presence configuration")
}
automaticPresence = configuredPresence
log.Info().Str("presence", string(automaticPresence)).Msg("Automatic session presence configured")

if *versionFlag {
fmt.Printf("WuzAPI version %s\n", version)
os.Exit(0)
Expand Down
34 changes: 34 additions & 0 deletions presence_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package main

import (
"testing"

"go.mau.fi/whatsmeow/types"
)

func TestParseAutomaticPresence(t *testing.T) {
tests := []struct {
name string
value string
want types.Presence
wantErr bool
}{
{name: "available", value: "available", want: types.PresenceAvailable},
{name: "unavailable", value: "unavailable", want: types.PresenceUnavailable},
{name: "normalizes case and whitespace", value: " UNAVAILABLE ", want: types.PresenceUnavailable},
{name: "rejects unknown value", value: "offline", wantErr: true},
{name: "rejects empty value", value: "", wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseAutomaticPresence(tt.value)
if (err != nil) != tt.wantErr {
t.Fatalf("parseAutomaticPresence(%q) error = %v, wantErr %v", tt.value, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("parseAutomaticPresence(%q) = %q, want %q", tt.value, got, tt.want)
}
})
}
}
29 changes: 14 additions & 15 deletions wmiau.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,15 @@ func fileToBase64(filepath string) (string, string, error) {
return base64.StdEncoding.EncodeToString(data), mimeType, nil
}

func (mycli *MyClient) sendAutomaticPresence() {
err := mycli.WAClient.SendPresence(context.Background(), automaticPresence)
if err != nil {
log.Warn().Err(err).Str("presence", string(automaticPresence)).Msg("Failed to send automatic presence")
} else {
log.Info().Str("presence", string(automaticPresence)).Msg("Set automatic presence")
}
}

func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
txtid := mycli.userID
postmap := make(map[string]interface{})
Expand All @@ -941,29 +950,19 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) {
switch evt := rawEvt.(type) {
case *events.AppStateSyncComplete:
if len(mycli.WAClient.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
err := mycli.WAClient.SendPresence(context.Background(), types.PresenceAvailable)
if err != nil {
log.Warn().Err(err).Msg("Failed to send available presence")
} else {
log.Info().Msg("Marked self as available")
}
mycli.sendAutomaticPresence()
}
case *events.Connected, *events.PushNameSetting:
postmap["type"] = "Connected"
dowebhook = 1
if len(mycli.WAClient.Store.PushName) == 0 {
break
}
// Send presence available when connecting and when the pushname is changed.
// This makes sure that outgoing messages always have the right pushname.
err := mycli.WAClient.SendPresence(context.Background(), types.PresenceAvailable)
if err != nil {
log.Warn().Err(err).Msg("Failed to send available presence")
} else {
log.Info().Msg("Marked self as available")
}
// Announce the push name with the configured presence when connecting and
// when the push name changes.
mycli.sendAutomaticPresence()
sqlStmt := `UPDATE users SET connected=1 WHERE id=$1`
_, err = mycli.db.Exec(sqlStmt, mycli.userID)
_, err := mycli.db.Exec(sqlStmt, mycli.userID)
if err != nil {
log.Error().Err(err).Msg(sqlStmt)
return
Expand Down