feat: improve auth identity and recipient parsing
Co-authored-by: Paulo <pmatheus.nsx@gmail.com> Co-authored-by: Fahmid Uddin <fahmid.me@gmail.com> Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
This commit is contained in:
parent
6fac72ee4d
commit
1e8342fbe7
@ -14,8 +14,10 @@
|
||||
- Messages: extract searchable/display text from WhatsApp Business templates, buttons, interactive messages, and list replies. (#79 — thanks @terry-li-hm)
|
||||
- Auth: add `auth --qr-format text` to print the raw WhatsApp QR payload for external renderers. (#22 — thanks @teren-papercutlabs)
|
||||
- Auth: add `auth --phone` for WhatsApp's phone-number pairing flow on headless systems. (#148, #184 — thanks @giovanninibarbosa and @KillerSnails)
|
||||
- Auth: auto-detect a readable linked-device label and default linked-device platform to desktop. (#100 — thanks @pmatheus)
|
||||
- Send: add `send react` to add or clear reactions, with group sender validation. (#151 — thanks @draix)
|
||||
- Send: add `send file --reply-to` for quoted media/document replies. (#68 — thanks @vlassance)
|
||||
- Send: accept common phone-number formatting in recipient flags while still storing digits-only WhatsApp JIDs. (#130 — thanks @fahmidme and @ImLukeF)
|
||||
|
||||
### Security
|
||||
|
||||
|
||||
@ -88,6 +88,7 @@ pnpm wacli media download --chat 1234567890@s.whatsapp.net --id <message-id>
|
||||
|
||||
# Send a message
|
||||
pnpm wacli send text --to 1234567890 --message "hello"
|
||||
# Phone numbers can also be passed as +E164 or formatted input like "+1 (234) 567-8900"
|
||||
|
||||
# Send a quoted reply
|
||||
pnpm wacli send text --to 1234567890 --message "replying" --reply-to <message-id>
|
||||
@ -169,8 +170,8 @@ Global flags:
|
||||
|
||||
## Environment overrides
|
||||
|
||||
- `WACLI_DEVICE_LABEL`: set the linked device label (shown in WhatsApp).
|
||||
- `WACLI_DEVICE_PLATFORM`: override the linked device platform (defaults to `CHROME` if unset or invalid).
|
||||
- `WACLI_DEVICE_LABEL`: override the linked device label shown in WhatsApp (defaults to `wacli - <OS> (<hostname>)` when detectable).
|
||||
- `WACLI_DEVICE_PLATFORM`: override the linked device platform (defaults to `DESKTOP`; invalid values fall back to `CHROME`).
|
||||
- `WACLI_READONLY`: set to `1`, `true`, `yes`, or `on` to enable read-only mode.
|
||||
- `WACLI_STORE_DIR`: override the default store directory.
|
||||
|
||||
|
||||
@ -82,6 +82,6 @@ func newGroupsParticipantsActionCmd(flags *rootFlags, action string) *cobra.Comm
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&group, "jid", "", "group JID (…@g.us)")
|
||||
cmd.Flags().StringSliceVar(&users, "user", nil, "user phone number or JID (repeatable)")
|
||||
cmd.Flags().StringSliceVar(&users, "user", nil, "user phone number (+E164 and formatting ok) or JID (repeatable)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||
@ -19,13 +20,18 @@ func main() {
|
||||
func applyDeviceLabel() {
|
||||
label := strings.TrimSpace(os.Getenv("WACLI_DEVICE_LABEL"))
|
||||
platformRaw := strings.TrimSpace(os.Getenv("WACLI_DEVICE_PLATFORM"))
|
||||
if platformRaw != "" {
|
||||
platform := parsePlatformType(platformRaw)
|
||||
store.DeviceProps.PlatformType = platform.Enum()
|
||||
if platformRaw == "" {
|
||||
platformRaw = "DESKTOP"
|
||||
}
|
||||
if label == "" {
|
||||
return
|
||||
label = detectDeviceLabel(runtime.GOOS, os.Hostname, os.ReadFile)
|
||||
}
|
||||
platform := parsePlatformType(platformRaw)
|
||||
store.DeviceProps.PlatformType = platform.Enum()
|
||||
if label == "" {
|
||||
label = "wacli"
|
||||
}
|
||||
|
||||
store.SetOSInfo(label, [3]uint32{0, 1, 0})
|
||||
store.BaseClientPayload.UserAgent.Device = proto.String(label)
|
||||
store.BaseClientPayload.UserAgent.Manufacturer = proto.String(label)
|
||||
@ -42,3 +48,50 @@ func parsePlatformType(raw string) waCompanionReg.DeviceProps_PlatformType {
|
||||
}
|
||||
return waCompanionReg.DeviceProps_CHROME
|
||||
}
|
||||
|
||||
func detectDeviceLabel(goos string, hostname func() (string, error), readFile func(string) ([]byte, error)) string {
|
||||
host, _ := hostname()
|
||||
host = strings.TrimSpace(host)
|
||||
osName := friendlyOSName(goos, readFile)
|
||||
switch {
|
||||
case host != "" && osName != "":
|
||||
return "wacli - " + osName + " (" + host + ")"
|
||||
case host != "":
|
||||
return "wacli - " + host
|
||||
case osName != "":
|
||||
return "wacli - " + osName
|
||||
default:
|
||||
return "wacli"
|
||||
}
|
||||
}
|
||||
|
||||
func friendlyOSName(goos string, readFile func(string) ([]byte, error)) string {
|
||||
switch goos {
|
||||
case "darwin":
|
||||
return "macOS"
|
||||
case "linux":
|
||||
return linuxDistroName(readFile)
|
||||
case "windows":
|
||||
return "Windows"
|
||||
default:
|
||||
return goos
|
||||
}
|
||||
}
|
||||
|
||||
func linuxDistroName(readFile func(string) ([]byte, error)) string {
|
||||
data, err := readFile("/etc/os-release")
|
||||
if err != nil {
|
||||
return "Linux"
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok || key != "PRETTY_NAME" {
|
||||
continue
|
||||
}
|
||||
value = strings.Trim(strings.TrimSpace(value), `"`)
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return "Linux"
|
||||
}
|
||||
|
||||
38
cmd/wacli/main_test.go
Normal file
38
cmd/wacli/main_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||
)
|
||||
|
||||
func TestParsePlatformType(t *testing.T) {
|
||||
if got := parsePlatformType("desktop"); got != waCompanionReg.DeviceProps_DESKTOP {
|
||||
t.Fatalf("desktop parsed as %v", got)
|
||||
}
|
||||
if got := parsePlatformType("bogus"); got != waCompanionReg.DeviceProps_CHROME {
|
||||
t.Fatalf("bogus parsed as %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectDeviceLabel(t *testing.T) {
|
||||
host := func() (string, error) { return "workstation", nil }
|
||||
readFile := func(string) ([]byte, error) { return []byte(`PRETTY_NAME="Ubuntu 24.04 LTS"`), nil }
|
||||
|
||||
if got := detectDeviceLabel("linux", host, readFile); got != "wacli - Ubuntu 24.04 LTS (workstation)" {
|
||||
t.Fatalf("detectDeviceLabel = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectDeviceLabelFallbacks(t *testing.T) {
|
||||
noHost := func() (string, error) { return "", errors.New("no hostname") }
|
||||
noFile := func(string) ([]byte, error) { return nil, errors.New("missing") }
|
||||
|
||||
if got := detectDeviceLabel("darwin", noHost, noFile); got != "wacli - macOS" {
|
||||
t.Fatalf("darwin label = %q", got)
|
||||
}
|
||||
if got := detectDeviceLabel("", noHost, noFile); got != "wacli" {
|
||||
t.Fatalf("empty label = %q", got)
|
||||
}
|
||||
}
|
||||
@ -34,7 +34,7 @@ func newPresenceTypingCmd(flags *rootFlags) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number or JID")
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number (+E164 and formatting ok) or JID")
|
||||
cmd.Flags().StringVar(&media, "media", "", "media type: 'audio' for recording indicator (default: typing text)")
|
||||
return cmd
|
||||
}
|
||||
@ -50,7 +50,7 @@ func newPresencePausedCmd(flags *rootFlags) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number or JID")
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number (+E164 and formatting ok) or JID")
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ func newSendTextCmd(flags *rootFlags) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number or JID")
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number (+E164 and formatting ok) or JID")
|
||||
cmd.Flags().StringVar(&message, "message", "", "message text")
|
||||
cmd.Flags().StringVar(&replyTo, "reply-to", "", "message ID to quote/reply to")
|
||||
cmd.Flags().StringVar(&replyToSender, "reply-to-sender", "", "sender JID of the quoted message (required for unsynced group replies)")
|
||||
|
||||
@ -80,7 +80,7 @@ func newSendFileCmd(flags *rootFlags) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number or JID")
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number (+E164 and formatting ok) or JID")
|
||||
cmd.Flags().StringVar(&filePath, "file", "", "path to file")
|
||||
cmd.Flags().StringVar(&filename, "filename", "", "display name for the file (defaults to basename of --file)")
|
||||
cmd.Flags().StringVar(&caption, "caption", "", "caption (images/videos/documents)")
|
||||
|
||||
@ -74,7 +74,7 @@ func newSendReactCmd(flags *rootFlags) *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number or JID")
|
||||
cmd.Flags().StringVar(&to, "to", "", "recipient phone number (+E164 and formatting ok) or JID")
|
||||
cmd.Flags().StringVar(&msgID, "id", "", "target message ID")
|
||||
cmd.Flags().StringVar(&emoji, "reaction", "\U0001f44d", "reaction emoji (pass an empty string to remove)")
|
||||
cmd.Flags().StringVar(&sender, "sender", "", "message sender JID (required for group messages)")
|
||||
|
||||
@ -39,6 +39,8 @@ func TestParseUserOrJID(t *testing.T) {
|
||||
{name: "phone", input: "1234567890", wantUser: "1234567890", wantServer: types.DefaultUserServer},
|
||||
{name: "phone with plus", input: "+1234567890", wantUser: "1234567890", wantServer: types.DefaultUserServer},
|
||||
{name: "phone with spaces and plus", input: " +1234567890 ", wantUser: "1234567890", wantServer: types.DefaultUserServer},
|
||||
{name: "formatted phone", input: "+1 (234) 567-8900", wantUser: "12345678900", wantServer: types.DefaultUserServer},
|
||||
{name: "dotted phone", input: "1.234.567.8900", wantUser: "12345678900", wantServer: types.DefaultUserServer},
|
||||
{name: "minimum length phone", input: "1234567", wantUser: "1234567", wantServer: types.DefaultUserServer},
|
||||
{name: "maximum length phone", input: "123456789012345", wantUser: "123456789012345", wantServer: types.DefaultUserServer},
|
||||
{name: "group jid", input: "123@g.us", wantUser: "123", wantServer: types.GroupServer},
|
||||
@ -46,10 +48,9 @@ func TestParseUserOrJID(t *testing.T) {
|
||||
{name: "too short phone", input: "123456", wantErr: true},
|
||||
{name: "too long phone", input: "1234567890123456", wantErr: true},
|
||||
{name: "letters in phone", input: "123abc456", wantErr: true},
|
||||
{name: "punctuation in phone", input: "123-456-7890", wantErr: true},
|
||||
{name: "spaces inside phone", input: "123 456 7890", wantErr: true},
|
||||
{name: "plus inside phone", input: "12+34567", wantErr: true},
|
||||
{name: "double leading plus", input: "++1234567", wantErr: true},
|
||||
{name: "unicode digits rejected", input: "١٢٣٤٥٦٧", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@ -27,17 +27,25 @@ func IsGroupJID(jid types.JID) bool {
|
||||
}
|
||||
|
||||
func normalizePhoneRecipient(s string) (string, error) {
|
||||
phone := strings.TrimPrefix(s, "+")
|
||||
if phone == "" {
|
||||
return "", fmt.Errorf("recipient is required")
|
||||
}
|
||||
if len(phone) < 7 || len(phone) > 15 {
|
||||
return "", fmt.Errorf("invalid phone number %q: must be 7-15 digits", s)
|
||||
}
|
||||
for _, ch := range phone {
|
||||
if ch < '0' || ch > '9' {
|
||||
return "", fmt.Errorf("invalid phone number %q: must contain digits only", s)
|
||||
var phone strings.Builder
|
||||
for i, ch := range s {
|
||||
switch {
|
||||
case ch == '+' && i == 0:
|
||||
continue
|
||||
case ch >= '0' && ch <= '9':
|
||||
phone.WriteRune(ch)
|
||||
case ch == ' ' || ch == '-' || ch == '(' || ch == ')' || ch == '.':
|
||||
continue
|
||||
default:
|
||||
return "", fmt.Errorf("invalid phone number %q: must contain digits, common formatting, or one leading +", s)
|
||||
}
|
||||
}
|
||||
return phone, nil
|
||||
normalized := phone.String()
|
||||
if normalized == "" {
|
||||
return "", fmt.Errorf("recipient is required")
|
||||
}
|
||||
if len(normalized) < 7 || len(normalized) > 15 {
|
||||
return "", fmt.Errorf("invalid phone number %q: must be 7-15 digits", s)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user