diff --git a/.env.sample b/.env.sample index 69f742a6..a3361298 100644 --- a/.env.sample +++ b/.env.sample @@ -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 diff --git a/API.md b/API.md index 1cc0556e..891c665c 100644 --- a/API.md +++ b/API.md @@ -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_ diff --git a/README.md b/README.md index e797ceff..f40ccaaf 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. diff --git a/docker-compose-swarm.yaml b/docker-compose-swarm.yaml index 9e06c927..f57afc1d 100644 --- a/docker-compose-swarm.yaml +++ b/docker-compose-swarm.yaml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 66731838..e7aa070e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/main.go b/main.go index f669dfbf..408c239d 100644 --- a/main.go +++ b/main.go @@ -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" @@ -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") @@ -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") @@ -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 @@ -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) diff --git a/presence_config_test.go b/presence_config_test.go new file mode 100644 index 00000000..4ef6ad3b --- /dev/null +++ b/presence_config_test.go @@ -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) + } + }) + } +} diff --git a/wmiau.go b/wmiau.go index a8fec072..330f04d0 100644 --- a/wmiau.go +++ b/wmiau.go @@ -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{}) @@ -941,12 +950,7 @@ 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" @@ -954,16 +958,11 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { 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