Conversation
ashiven
commented
Jan 10, 2026
- Added support for the whatsmeow api
There was a problem hiding this comment.
Pull request overview
This pull request adds WhatsApp messaging support to the household planner application using the whatsmeow library as an alternative to Twilio SMS. The implementation includes a new command-line flag to toggle between WhatsApp and SMS messaging.
Changes:
- Added whatsmeow integration with QR code authentication and message sending capabilities
- Refactored field naming from
PhonenumbertoPhoneNumberfor consistency - Updated Dockerfile to support CGO and SQLite dependencies required by whatsmeow
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/planner/whatsmeow.go | New file implementing WhatsApp client initialization, authentication, and phone number verification |
| pkg/planner/message.go | Updated field references and exported CreateDailyTaskMessage function for use in main.go |
| pkg/planner/household.go | Renamed Phonenumber field to PhoneNumber and added PhoneNumbers() method |
| pkg/planner/assignments.go | Refactored receiver variable names from household to h for consistency |
| pkg/backend/handlers.go | Updated field reference to use PhoneNumber |
| main.go | Added WhatsApp messaging logic with -w flag, slices usage, and message sending loop |
| go.mod/go.sum | Added whatsmeow and related dependencies |
| Dockerfile.backend | Enabled CGO and added SQLite support for whatsmeow database |
| .gitignore | Added devicestore.db to ignored files |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| qrChannel, _ := client.GetQRChannel(context.Background()) | ||
| err := client.Connect() | ||
| if err != nil { | ||
| fmt.Println("[ERROR] Login: ", err) | ||
| } |
There was a problem hiding this comment.
Return value from GetQRChannel is ignored. The second return value (error) should be checked before proceeding with the login process.
| qrChannel, _ := client.GetQRChannel(context.Background()) | |
| err := client.Connect() | |
| if err != nil { | |
| fmt.Println("[ERROR] Login: ", err) | |
| } | |
| qrChannel, err := client.GetQRChannel(context.Background()) | |
| if err != nil { | |
| fmt.Println("[ERROR] Login: ", err) | |
| return | |
| } | |
| err = client.Connect() | |
| if err != nil { | |
| fmt.Println("[ERROR] Login: ", err) | |
| return | |
| } |
| client := planner.NewWhatsmeowClient() | ||
|
|
||
| // NOTE: Needs QR Login via terminal on first startup | ||
| planner.Login(client) | ||
| time.Sleep(time.Second * 30) | ||
|
|
||
| phoneNumbers := myHousehold.PhoneNumbers() | ||
| JIDs := planner.PhoneNumbersToJIDs(client, phoneNumbers) |
There was a problem hiding this comment.
Error handling from NewWhatsmeowClient(), Login(), and PhoneNumbersToJIDs() is absent. Since these functions don't return errors (though they should), the code continues execution even if initialization fails. This could lead to nil pointer dereferences or unexpected behavior when trying to send messages.
| dbLogger := waLogger.Stdout("Database", "DEBUG", true) | ||
| ctx := context.Background() | ||
|
|
||
| container, err := sqlstore.New(ctx, "sqlite3", "file:devicestore.db?_foreign_keys=on", dbLogger) |
There was a problem hiding this comment.
The database file path "file:devicestore.db?_foreign_keys=on" is hardcoded. This makes it difficult to configure for different environments (development, staging, production) and prevents users from specifying a custom location. Consider making this configurable through an environment variable or configuration file.
| container, err := sqlstore.New(ctx, "sqlite3", "file:devicestore.db?_foreign_keys=on", dbLogger) | |
| dbPath := os.Getenv("WHATSAPP_DEVICESTORE_DSN") | |
| if dbPath == "" { | |
| dbPath = "file:devicestore.db?_foreign_keys=on" | |
| } | |
| container, err := sqlstore.New(ctx, "sqlite3", dbPath, dbLogger) |
| func PhoneNumbersToJIDs(client *whatsmeow.Client, phoneNumbers []string) map[string]types.JID { | ||
| JIDs := map[string]types.JID{} | ||
|
|
||
| isOnWhatsAppRes, err := client.IsOnWhatsApp(phoneNumbers) | ||
| if err != nil { | ||
| fmt.Println("[ERROR] IsOnWhatsApp: ", err) | ||
| } | ||
|
|
||
| for _, whatsAppRes := range isOnWhatsAppRes { | ||
| phoneNumber := "+" + whatsAppRes.JID.User | ||
|
|
||
| if whatsAppRes.IsIn { | ||
| fmt.Printf("[INFO] %s found on whatsapp\n", phoneNumber) | ||
| JIDs[phoneNumber] = whatsAppRes.JID | ||
|
|
||
| } else { | ||
| fmt.Printf("[INFO] %s is not on whatsapp\n", phoneNumber) | ||
| JIDs[phoneNumber] = types.JID{} | ||
| } | ||
| } | ||
| return JIDs | ||
| } |
There was a problem hiding this comment.
The function should return an error to allow proper error handling by callers. When client.IsOnWhatsApp() fails, the error is only logged and the function continues, potentially returning incomplete results. Callers cannot determine if the operation was successful.
| client := planner.NewWhatsmeowClient() | ||
|
|
||
| // NOTE: Needs QR Login via terminal on first startup | ||
| planner.Login(client) | ||
| time.Sleep(time.Second * 30) | ||
|
|
||
| phoneNumbers := myHousehold.PhoneNumbers() | ||
| JIDs := planner.PhoneNumbersToJIDs(client, phoneNumbers) |
There was a problem hiding this comment.
The WhatsApp client is being created, authenticated, and phone numbers are being checked on every iteration of the infinite loop (daily). This is inefficient and wasteful - client initialization, login, and phone number verification should be performed once before entering the loop, not on every day's task assignment. Consider moving lines 49-56 outside the loop.
| JID := JIDs[member.PhoneNumber] | ||
| waMessage := &waE2E.Message{Conversation: proto.String(message)} | ||
| _, err := client.SendMessage(context.Background(), JID, waMessage) | ||
| if err != nil { | ||
| fmt.Printf("[ERROR] Failed to deliver message to %s.\n", member.Name) | ||
| } | ||
| } |
There was a problem hiding this comment.
When a phone number is not on WhatsApp, an empty JID (types.JID{}) is stored in the map (line 82 in whatsmeow.go). In the message sending loop, this empty JID is then used to attempt sending a message without any validation. This will likely cause SendMessage to fail. Consider checking if the JID is valid before attempting to send, or filtering out members without valid JIDs.
| func NewWhatsmeowClient() *whatsmeow.Client { | ||
| dbLogger := waLogger.Stdout("Database", "DEBUG", true) | ||
| ctx := context.Background() | ||
|
|
||
| container, err := sqlstore.New(ctx, "sqlite3", "file:devicestore.db?_foreign_keys=on", dbLogger) | ||
| if err != nil { | ||
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | ||
| } | ||
|
|
||
| device, err := container.GetFirstDevice(ctx) | ||
| if err != nil { | ||
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | ||
| } | ||
|
|
||
| clientLogger := waLogger.Stdout("Client", "INFO", true) | ||
| client := whatsmeow.NewClient(device, clientLogger) | ||
|
|
||
| return client |
There was a problem hiding this comment.
Error is not returned or properly handled. When creating the SQL store fails, the function continues execution with a nil container, which will cause a panic on the next line when calling container.GetFirstDevice(). The error should be returned to the caller for proper handling.
| func NewWhatsmeowClient() *whatsmeow.Client { | |
| dbLogger := waLogger.Stdout("Database", "DEBUG", true) | |
| ctx := context.Background() | |
| container, err := sqlstore.New(ctx, "sqlite3", "file:devicestore.db?_foreign_keys=on", dbLogger) | |
| if err != nil { | |
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | |
| } | |
| device, err := container.GetFirstDevice(ctx) | |
| if err != nil { | |
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | |
| } | |
| clientLogger := waLogger.Stdout("Client", "INFO", true) | |
| client := whatsmeow.NewClient(device, clientLogger) | |
| return client | |
| func NewWhatsmeowClient() (*whatsmeow.Client, error) { | |
| dbLogger := waLogger.Stdout("Database", "DEBUG", true) | |
| ctx := context.Background() | |
| container, err := sqlstore.New(ctx, "sqlite3", "file:devicestore.db?_foreign_keys=on", dbLogger) | |
| if err != nil { | |
| return nil, fmt.Errorf("NewWhatsmeowClient: failed to create SQL store: %w", err) | |
| } | |
| device, err := container.GetFirstDevice(ctx) | |
| if err != nil { | |
| return nil, fmt.Errorf("NewWhatsmeowClient: failed to get first device: %w", err) | |
| } | |
| clientLogger := waLogger.Stdout("Client", "INFO", true) | |
| client := whatsmeow.NewClient(device, clientLogger) | |
| return client, nil |
| func NewWhatsmeowClient() *whatsmeow.Client { | ||
| dbLogger := waLogger.Stdout("Database", "DEBUG", true) | ||
| ctx := context.Background() | ||
|
|
||
| container, err := sqlstore.New(ctx, "sqlite3", "file:devicestore.db?_foreign_keys=on", dbLogger) | ||
| if err != nil { | ||
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | ||
| } | ||
|
|
||
| device, err := container.GetFirstDevice(ctx) | ||
| if err != nil { | ||
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | ||
| } | ||
|
|
||
| clientLogger := waLogger.Stdout("Client", "INFO", true) | ||
| client := whatsmeow.NewClient(device, clientLogger) | ||
|
|
||
| return client |
There was a problem hiding this comment.
The function signature should return an error to allow proper error handling by callers. Currently, NewWhatsmeowClient returns only a *whatsmeow.Client, but errors from sqlstore.New() and container.GetFirstDevice() are only logged, not propagated. This prevents callers from handling initialization failures appropriately.
| func NewWhatsmeowClient() *whatsmeow.Client { | |
| dbLogger := waLogger.Stdout("Database", "DEBUG", true) | |
| ctx := context.Background() | |
| container, err := sqlstore.New(ctx, "sqlite3", "file:devicestore.db?_foreign_keys=on", dbLogger) | |
| if err != nil { | |
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | |
| } | |
| device, err := container.GetFirstDevice(ctx) | |
| if err != nil { | |
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | |
| } | |
| clientLogger := waLogger.Stdout("Client", "INFO", true) | |
| client := whatsmeow.NewClient(device, clientLogger) | |
| return client | |
| func NewWhatsmeowClient() (*whatsmeow.Client, error) { | |
| dbLogger := waLogger.Stdout("Database", "DEBUG", true) | |
| ctx := context.Background() | |
| container, err := sqlstore.New(ctx, "sqlite3", "file:devicestore.db?_foreign_keys=on", dbLogger) | |
| if err != nil { | |
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | |
| return nil, err | |
| } | |
| device, err := container.GetFirstDevice(ctx) | |
| if err != nil { | |
| fmt.Println("[ERROR] NewWhatsmeowClient: ", err) | |
| return nil, err | |
| } | |
| clientLogger := waLogger.Stdout("Client", "INFO", true) | |
| client := whatsmeow.NewClient(device, clientLogger) | |
| return client, nil |