X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/2e2524881dfe8f6c5cabfd9d342d5d3492953bd4..b473b56ebf00e6b4d6cf16664f93e1958fbe2080:/whatsapp.go?ds=sidebyside diff --git a/whatsapp.go b/whatsapp.go index 5b07300..0c68a44 100644 --- a/whatsapp.go +++ b/whatsapp.go @@ -2,11 +2,10 @@ package main import ( "context" - "crypto/sha256" - "encoding/hex" + "database/sql" "errors" "fmt" - "log" + "log/slog" "net/url" "os" "path/filepath" @@ -16,13 +15,13 @@ import ( "time" "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/proto/waCompanionReg" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" - // The session store of the library opens SQLite with this pure Go // driver. _ "modernc.org/sqlite" @@ -67,6 +66,13 @@ const ( // gets a notice. whatsappNoticeDelay = 10 * time.Minute + // whatsappRetryDelay is the first wait after a failed starting + // connection. + whatsappRetryDelay = 2 * time.Second + + // whatsappRetryLimit is the longest wait between starting connections. + whatsappRetryLimit = 5 * time.Minute + // whatsappRequests is the number of pair and unlink commands that can // wait for Run. whatsappRequests = 8 @@ -77,20 +83,6 @@ const ( // whatsappNotLinked is the answer of a WhatsApp tool without a link. whatsappNotLinked = "WhatsApp is not linked. Tell the user to run /whatsapp pair PHONE in the terminal or in Telegram." - - // whatsappSendTime is the longest time of a send. - whatsappSendTime = 30 * time.Second - - // whatsappSendBytes is the most bytes of the text of a send. - whatsappSendBytes = 65536 - - // whatsappSendIDBytes is the most bytes of the ID of a send. - whatsappSendIDBytes = 100 - - // The states of a send record. - whatsappSending = "sending" - whatsappSent = "sent" - whatsappFailed = "failed" ) // WhatsAppClient is the part of the WhatsApp client that the daemon uses. The @@ -112,7 +104,7 @@ type WhatsAppClient interface { // the account, so a file that another user can read is an error. The clock // gives the times of the status, logger gets the failures, and newClient makes // the client of a device with its event handler. -func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logger *log.Logger, newClient func(device *store.Device, handle func(event any) bool) WhatsAppClient) (*WhatsApp, error) { +func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logger *slog.Logger, newClient func(device *store.Device, handle func(event any) bool) WhatsAppClient) (*WhatsApp, error) { w := &WhatsApp{ dir: filepath.Join(dir, whatsappDirName), clock: clock, @@ -125,6 +117,7 @@ func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logge } var err error + w.state, err = openWhatsAppState(filepath.Join(w.dir, whatsappStateFileName), whatsappBatchQuiet, whatsappBatchWindow) if err != nil { return nil, err @@ -153,6 +146,7 @@ func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logge if w.device.ID != nil { w.linked = true w.inbox.waitForCatchup() + w.own = whatsappOwnIDs(w.device.ID.User, w.device.LID.User) } @@ -170,7 +164,9 @@ func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logge // of the client that asked. A logout from the phone comes as an event of the // library, and Run deletes the session. The mutex protects the session, the // client, and the state of the connection, which the event handler of the -// library changes from its own goroutines. +// library changes from its own goroutines. The sessionMutex serializes session +// creation, close, removal, and snapshots. It is held before mutex, and stays +// held when removal releases mutex for client callbacks. // // The inbox puts the text messages into batches and logs them, and groups // holds the names of the groups. Both get a WhatsAppLink from link for each @@ -186,7 +182,7 @@ func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logge type WhatsApp struct { dir string clock func() time.Time - logger *log.Logger + logger *slog.Logger newClient func(device *store.Device, handle func(event any) bool) WhatsAppClient noticeDelay time.Duration requests chan WhatsAppRequest @@ -195,6 +191,7 @@ type WhatsApp struct { groups *WhatsAppGroups inbox *WhatsAppInbox + sessionMutex sync.Mutex mutex sync.Mutex container *sqlstore.Container device *store.Device @@ -203,6 +200,7 @@ type WhatsApp struct { own []string notify func(Event) connected bool + retrying bool connectedAt time.Time disconnectedAt time.Time disconnections int @@ -214,21 +212,51 @@ type WhatsApp struct { // Run connects a linked device, logs the closed batches in l, runs the pair and // unlink commands, and handles a logout from the phone, until ctx ends. Its // notes go to the log of l, and notify gets the notices for all clients. A -// failed first connection is logged, and the library tries again when the -// failure is one of the network. Run returns the error of a write of the log or -// of the state file, which stops the daemon, because then messages would be -// lost. +// failed first connection starts the same outage notice as a lost connection. A +// timer in the select retries it with a doubled wait from 2 seconds to 5 +// minutes, without a retry goroutine or a wait that blocks commands or the +// inbox. A connection, unlink, logout, or ctx end stops retries. The library +// owns retries after a connection opens. Run returns the error of a write of +// the log or of the state file, which stops the daemon, because then messages +// would be lost. func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) error { w.mutex.Lock() + w.notify = notify linked := w.linked + w.retrying = linked w.mutex.Unlock() - if linked { - w.connect(ctx) + timer := time.NewTimer(whatsappRetryDelay) + timer.Stop() + defer timer.Stop() + + var retry <-chan time.Time + + delay := whatsappRetryDelay + connect := func() { + retry = nil + if w.connect(ctx) { + timer.Reset(delay) + + retry = timer.C + delay = min(2*delay, whatsappRetryLimit) + } + } + + stopRetry := func() { + timer.Stop() + + retry = nil + + w.mutex.Lock() + + w.retrying = false + w.mutex.Unlock() } var pairing sync.WaitGroup + cancel := func() {} defer func() { cancel() @@ -236,6 +264,10 @@ func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) err w.close() }() + if linked { + connect() + } + for { err := w.inbox.deliver(ctx, l, w.link(), false) if err != nil { @@ -249,7 +281,11 @@ func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) err return err case <-w.inbox.wake: case <-w.inbox.due(): + case <-retry: + connect() case <-w.logouts: + stopRetry() + err = w.loggedOut(ctx, l) if err != nil { return err @@ -257,10 +293,13 @@ func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) err case request := <-w.requests: cancel() pairing.Wait() + cancel = func() {} switch { case request.unlink: + stopRetry() + err = w.unlink(ctx, l, request.answer) if err != nil { return err @@ -292,6 +331,7 @@ func (w *WhatsApp) Command(args string, answer func(text string)) { switch { case len(fields) == 1 && fields[0] == "status": answer(w.Status()) + return case len(fields) == 1 && fields[0] == "unlink": request.unlink = true @@ -299,12 +339,14 @@ func (w *WhatsApp) Command(args string, answer func(text string)) { phone, valid := whatsappPhone(strings.Join(fields[1:], "")) if !valid { answer("PHONE must be the number of your WhatsApp account with its country code: 7 to 15 digits, such as +15550001111.") + return } request.phone = phone default: answer(whatsappUsage) + return } @@ -336,8 +378,62 @@ func (w *WhatsApp) Status() string { } } +// BackupSession writes a consistent SQLite snapshot to path while the session +// cannot be created, closed, or removed. The source opens without creation +// permission. No session file means no linked state to copy. +func (w *WhatsApp) BackupSession(ctx context.Context, path string) (err error) { + w.sessionMutex.Lock() + defer w.sessionMutex.Unlock() + + w.mutex.Lock() + defer w.mutex.Unlock() + + _, err = os.Stat(w.sessionPath()) + if errors.Is(err, os.ErrNotExist) { + return nil + } + + if err != nil { + return fmt.Errorf("read session file: %w", err) + } + + err = os.MkdirAll(filepath.Dir(path), 0o700) + if err != nil { + return fmt.Errorf("create session snapshot directory: %w", err) + } + + address, err := whatsappSessionAddress(w.sessionPath()) + if err != nil { + return fmt.Errorf("find session database: %w", err) + } + + database, err := sql.Open("sqlite", address+"&mode=rw") + if err != nil { + return fmt.Errorf("open session snapshot source: %w", err) + } + + defer func() { + err = errors.Join(err, database.Close()) + }() + + _, err = database.ExecContext(ctx, "VACUUM INTO ?", path) + if err != nil { + return fmt.Errorf("snapshot session database: %w", err) + } + + err = os.Chmod(path, 0o600) + if err != nil { + return fmt.Errorf("set session snapshot mode: %w", err) + } + + return nil +} + // Close closes the session database. func (w *WhatsApp) Close() error { + w.sessionMutex.Lock() + defer w.sessionMutex.Unlock() + w.mutex.Lock() defer w.mutex.Unlock() @@ -356,6 +452,12 @@ func (w *WhatsApp) Find(ctx context.Context, name string) (string, error) { return whatsappFind(ctx, w.link(), w.groups, name) } +// Send returns the answer of whatsapp_send for the send id of text to chat, as +// whatsappSend says. +func (w *WhatsApp) Send(ctx context.Context, id, chat, text string) (string, error) { + return whatsappSend(ctx, w.link(), w.groups, w.state, w.clock, id, chat, text) +} + func (w *WhatsApp) sessionPath() string { return filepath.Join(w.dir, whatsappSessionFileName) } @@ -406,22 +508,71 @@ func (w *WhatsApp) link() WhatsAppLink { } } -// connect makes the client of the linked device and connects it. -func (w *WhatsApp) connect(ctx context.Context) { +// connect tries the starting connection of the linked device with one client. +// It returns whether Run must retry. A failed call starts the shared outage +// state and logs its redacted cause once, because a returned ConnectContext +// error does not emit Disconnected. Connected logs recovery after +// authentication. +func (w *WhatsApp) connect(ctx context.Context) bool { w.mutex.Lock() - w.client = w.newClient(w.device, w.handle) + + if ctx.Err() != nil || !w.linked || !w.retrying || w.closed { + w.mutex.Unlock() + + return false + } + + if w.client == nil { + w.client = w.newClient(w.device, w.handle) + } + client := w.client w.mutex.Unlock() err := client.ConnectContext(ctx) - if err != nil { - w.logger.Printf("connect: %s", w.redact(err.Error())) + + w.mutex.Lock() + + if err == nil || ctx.Err() != nil || !w.retrying || !w.linked { + w.retrying = false + w.mutex.Unlock() + + return false + } + + first := w.startOutage() + cause := whatsappRedact(w.own, err.Error()) + w.mutex.Unlock() + + if first { + w.logger.Warn("first connect failed, retrying", "err", cause) + } + + return true +} + +// startOutage starts the shared outage time and notice timer once. The caller +// holds the mutex. Its result tells whether this is the first failure. +func (w *WhatsApp) startOutage() bool { + first := w.linked && (w.connected || w.disconnectedAt.IsZero()) + if first { + w.disconnectedAt = w.clock() + w.disconnections++ + disconnections := w.disconnections + w.timer = time.AfterFunc(w.noticeDelay, func() { + w.disconnectedLong(disconnections) + }) } + + w.connected = false + + return first } // close ends the connection and the timer of the notice, and stops all notices. func (w *WhatsApp) close() { w.mutex.Lock() + w.closed = true if w.timer != nil { w.timer.Stop() @@ -445,6 +596,8 @@ func (w *WhatsApp) loggedOut(ctx context.Context, l *TurnLoop) error { return nil } + w.logger.Error("logged out by the phone, run /whatsapp pair PHONE") + err := w.inbox.deliver(ctx, l, w.link(), true) if err != nil { return err @@ -452,12 +605,12 @@ func (w *WhatsApp) loggedOut(ctx context.Context, l *TurnLoop) error { err = w.removeSession() if err != nil { - w.logger.Printf("delete the session after the logout: %v", err) + w.logger.Error("session delete failed", "err", err) } err = l.note("WhatsApp was unlinked from the phone.") if err != nil { - w.logger.Printf("note the logout: %v", err) + w.logger.Error("chat note failed", "err", err) } w.broadcast("WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again.") @@ -472,12 +625,14 @@ func (w *WhatsApp) loggedOut(ctx context.Context, l *TurnLoop) error { // file. func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text string)) error { w.mutex.Lock() + linked := w.linked client := w.client w.mutex.Unlock() if !linked { answer("WhatsApp is not linked.") + return nil } @@ -487,6 +642,7 @@ func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text str } var logoutErr error + if client != nil { ctx, cancel := context.WithTimeout(ctx, whatsappLogoutTime) defer cancel() @@ -497,16 +653,20 @@ func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text str err = w.removeSession() if err != nil { answer("The unlink failed: " + w.redact(err.Error())) + return nil } + w.logger.Info("unlinked") + err = l.note("WhatsApp is unlinked.") if err != nil { - w.logger.Printf("note the unlink: %v", err) + w.logger.Error("chat note failed", "err", err) } if client == nil || logoutErr != nil { answer("WhatsApp is unlinked here, but the phone did not get the logout. In WhatsApp on your phone, open Linked devices and remove the device.") + return nil } @@ -516,14 +676,21 @@ func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text str } // removeSession ends the connection, closes the session database, and deletes -// its files. +// its files. The session lock covers the cleanup after mutex is released, so a +// snapshot or a new session cannot open files that cleanup removes. func (w *WhatsApp) removeSession() error { + w.sessionMutex.Lock() + defer w.sessionMutex.Unlock() + w.mutex.Lock() + client := w.client w.client = nil w.linked = false + w.retrying = false w.own = nil w.connected = false + w.disconnectedAt = time.Time{} if w.timer != nil { w.timer.Stop() } @@ -541,6 +708,7 @@ func (w *WhatsApp) removeSession() error { } var err error + if container != nil { err = container.Close() } @@ -570,6 +738,7 @@ func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, device, err := w.newDevice(ctx) if err != nil { answer("The pairing failed: " + err.Error()) + return } @@ -588,6 +757,7 @@ func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, if err != nil { answer("The connection to WhatsApp failed: " + err.Error()) + return } @@ -595,6 +765,7 @@ func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, case item, open := <-qr: if !open || item.Event != whatsmeow.QRChannelEventCode { answer("WhatsApp did not start the pairing. Try again later.") + return } case <-ctx.Done(): @@ -605,9 +776,12 @@ func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, return } + // The server accepts only a display name of the form "Browser (OS)" + // with a common browser and OS, and refuses the pairing otherwise. code, err := client.PairPhone(ctx, phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)") if err != nil { answer("WhatsApp refused the pairing: " + err.Error() + ". Check the phone number and run /whatsapp pair PHONE again.") + return } @@ -616,6 +790,7 @@ func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, for { var item whatsmeow.QRChannelItem + open := true select { case item, open = <-qr: @@ -630,16 +805,19 @@ func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, switch { case !open || item.Event == whatsmeow.QRChannelTimeout.Event: answer(whatsappPairExpired) + return case item.Event == whatsmeow.QRChannelEventCode: continue case item.Event == whatsmeow.QRChannelSuccess.Event: linked = true + w.linkedBy(client) + w.logger.Info("linked") err = l.note("WhatsApp is linked.") if err != nil { - w.logger.Printf("note the link: %v", err) + w.logger.Error("chat note failed", "err", err) } answer("WhatsApp is linked to +" + phone + ".") @@ -647,6 +825,7 @@ func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, return default: answer("The pairing failed: " + whatsappItemError(item) + ". Run /whatsapp pair PHONE again.") + return } } @@ -656,6 +835,9 @@ func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, // directory and the file of the session first, with their modes, when they do // not exist. func (w *WhatsApp) newDevice(ctx context.Context) (*store.Device, error) { + w.sessionMutex.Lock() + defer w.sessionMutex.Unlock() + w.mutex.Lock() defer w.mutex.Unlock() @@ -699,9 +881,14 @@ func (w *WhatsApp) linkedBy(client WhatsAppClient) { // goes to the inbox. The names of the groups follow the events of the groups. A // connection starts a catch-up of the inbox, which ends at OfflineSyncCompleted // or after its catchupLimit. A disconnection keeps the time of its start while -// the library tries again. After noticeDelay of a disconnection, all clients -// get one notice, and they get one more when the connection comes back. A -// disconnection that the library does not try again gives a notice at once. +// Run or the library tries again. After noticeDelay of a disconnection, all +// clients get one notice, and they get one more when the connection comes back. +// A disconnection that the library does not try again gives a notice at once. +// +// The log of the daemon gets one line for each change of the connection of the +// linked device: connected, a failed first connection, a lost connection, and +// connected again with the length of the outage. The retries of the library and +// Run between them log nothing. func (w *WhatsApp) handle(event any) bool { switch event := event.(type) { case *events.Message: @@ -714,21 +901,39 @@ func (w *WhatsApp) handle(event any) bool { return true case *events.JoinedGroup: w.groups.set(event.JID.String(), event.Name) + return true case *events.OfflineSyncCompleted: w.inbox.endCatchup() + return true case *events.Connected: w.inbox.startCatchup() } - var notice string + var notice, line string + + level := slog.LevelInfo + + var attrs []any + w.mutex.Lock() + switch event := event.(type) { case *events.PairSuccess: w.own = whatsappOwnIDs(event.ID.User, event.LID.User) case *events.Connected: + switch { + case !w.linked || w.connected: + case w.disconnectedAt.IsZero(): + line = "connected" + default: + line = "connected again" + attrs = append(attrs, "after", w.clock().Sub(w.disconnectedAt)) + } + w.connected = true + w.retrying = false w.connectedAt = w.clock() if w.timer != nil { w.timer.Stop() @@ -739,30 +944,42 @@ func (w *WhatsApp) handle(event any) bool { notice = "WhatsApp is connected again." } case *events.Disconnected: - if w.linked && (w.connected || w.disconnectedAt.IsZero()) { - w.disconnectedAt = w.clock() - w.disconnections++ - disconnections := w.disconnections - w.timer = time.AfterFunc(w.noticeDelay, func() { - w.disconnectedLong(disconnections) - }) + connected := w.connected + if w.startOutage() { + level = slog.LevelWarn + line = "disconnected, retrying" + if !connected { + line = "first connect failed, retrying" + } } - - w.connected = false case *events.LoggedOut: + w.retrying = false select { case w.logouts <- struct{}{}: default: } case events.PermanentDisconnect: + w.retrying = false if w.linked { w.connected = false w.noticed = true notice = "WhatsApp is disconnected and does not connect again by itself: " + event.PermanentDisconnectDescription() + ". Restart the daemon to try again." + level = slog.LevelError + line = "disconnected for good, restart to retry" + attrs = append(attrs, "err", whatsappRedact(w.own, event.PermanentDisconnectDescription())) } } + + if w.closed { + line = "" + } + w.mutex.Unlock() + if line != "" { + w.logger.Log(context.Background(), level, line, attrs...) + } + if notice != "" { w.broadcast(notice) } @@ -774,6 +991,7 @@ func (w *WhatsApp) handle(event any) bool { // disconnection with the number disconnections ended. func (w *WhatsApp) disconnectedLong(disconnections int) { w.mutex.Lock() + long := !w.closed && w.linked && !w.connected && !w.noticed && w.disconnections == disconnections if long { w.noticed = true @@ -790,6 +1008,7 @@ func (w *WhatsApp) disconnectedLong(disconnections int) { // broadcast gives text as a notice to all clients. func (w *WhatsApp) broadcast(text string) { w.mutex.Lock() + notify := w.notify closed := w.closed w.mutex.Unlock() @@ -810,88 +1029,6 @@ func (w *WhatsApp) redact(text string) string { return whatsappRedact(w.own, text) } -// Send sends text to chat, the ID of a private chat or a group, and returns the -// answer of the tool. id names the send: a call with an id that has a record -// gives the answer of the record and sends nothing, and the same id with -// another chat or text is an error. The record "sending" is durable before the -// send, so a stop of the daemon during the send gives an answer that the result -// is unknown, and not a second send. A disconnected WhatsApp sends nothing and -// makes no record. -func (w *WhatsApp) Send(ctx context.Context, id, chat, text string) (string, error) { - w.mutex.Lock() - linked := w.linked - device := w.device - client := w.client - connected := w.connected - own := slices.Clone(w.own) - w.mutex.Unlock() - - if !linked { - return whatsappNotLinked, nil - } - - jid, err := whatsappSendCheck(id, chat, text, own) - if err != nil { - return "", err - } - - digest := sha256.Sum256([]byte(text)) - record := WhatsAppSend{ - ID: id, - Chat: chat, - Digest: hex.EncodeToString(digest[:]), - State: whatsappSending, - Time: w.clock(), - } - - held, found := w.state.Send(id) - switch { - case found && (held.Chat != record.Chat || held.Digest != record.Digest): - return "", fmt.Errorf("the id %s belongs to another message: give a new id for each message", id) - case found && held.State == whatsappSending: - return "The send with this id started, but its result is unknown, because the daemon stopped during it. Ask the user to look at the chat in WhatsApp before you send the message again with a new id.", nil - case found: - return held.Answer + " This is the answer of the first call with this id: nothing was sent again.", nil - case client == nil || !connected: - return "", errors.New("WhatsApp is disconnected, so nothing was sent: try again later") - } - - record.MessageID = client.GenerateMessageID() - err = w.state.PutSend(record) - if err != nil { - return "", err - } - - sendCtx, cancel := context.WithTimeout(ctx, whatsappSendTime) - defer cancel() - - response, err := client.SendMessage(sendCtx, jid, &waE2E.Message{ - Conversation: new(text), - }, whatsmeow.SendRequestExtra{ - ID: record.MessageID, - }) - - record.State = whatsappSent - if err != nil { - record.State = whatsappFailed - record.Answer = "The send failed: " + w.redact(err.Error()) + "." - } else { - name := w.groups.name(ctx, w.link(), jid.String()) - if jid.Server != types.GroupServer { - name = whatsappContactName(ctx, device, jid, "") - } - - record.Answer = "Sent to " + name + " at " + response.Timestamp.In(w.clock().Location()).Format("15:04") + "." - } - - err = w.state.PutSend(record) - if err != nil { - return "", err - } - - return record.Answer, nil -} - // WhatsAppLink is a copy of the link of the device at one time, for the parts // of WhatsApp that do not own it: the groups, the batches, find, and send. type WhatsAppLink struct { @@ -910,14 +1047,24 @@ type WhatsAppRequest struct { answer func(text string) } +// whatsappSetDeviceProps sets the device properties that the library sends at +// the pairing. They are global values of the library, so the daemon sets them +// once before any client exists. The phone shows the linked device with the +// name OneChat and a desktop icon only when the platform type is known. +func whatsappSetDeviceProps() { + store.SetOSInfo("OneChat", [3]uint32{0, 1, 0}) + + store.DeviceProps.PlatformType = waCompanionReg.DeviceProps_DESKTOP.Enum() +} + // whatsappNewClient returns the client of the library for device, with handle -// as its event handler. The client tries again after a failed first connection, -// acknowledges a message only after handle returns true, and downloads no old -// history. The phone shows the device with the name OneChat. +// as its event handler. Run owns failed starting connections of linked devices, +// so only an unlinked device enables the initial retry of the library for +// pairing. The library still retries a lost connection. The client acknowledges +// a message only after handle returns true, and downloads no old history. func whatsappNewClient(device *store.Device, handle func(event any) bool) WhatsAppClient { - store.SetOSInfo("OneChat", [3]uint32{0, 1, 0}) client := whatsmeow.NewClient(device, waLog.Noop) - client.InitialAutoReconnect = true + client.InitialAutoReconnect = device.ID == nil client.SynchronousAck = true client.ManualHistorySyncDownload = true client.AddEventHandlerWithSuccessStatus(handle) @@ -967,30 +1114,10 @@ func whatsappContactName(ctx context.Context, device *store.Device, jid types.JI } } -// whatsappSendCheck returns the JID of chat, and an error when id, chat, or -// text cannot go to a send. The chat is a private chat or a group, and not the -// chat of the user with itself. -func whatsappSendCheck(id, chat, text string, own []string) (types.JID, error) { - jid, err := types.ParseJID(chat) - switch { - case strings.TrimSpace(id) == "" || len(id) > whatsappSendIDBytes: - return types.EmptyJID, fmt.Errorf("the id must have 1 to %d bytes", whatsappSendIDBytes) - case strings.TrimSpace(text) == "" || len(text) > whatsappSendBytes: - return types.EmptyJID, fmt.Errorf("the text must have 1 to %d bytes", whatsappSendBytes) - case err != nil || jid.User == "" || jid != jid.ToNonAD() || jid.String() != chat: - return types.EmptyJID, fmt.Errorf("%q is not the ID of a WhatsApp chat: find the ID with whatsapp_find", chat) - case jid.Server != types.DefaultUserServer && jid.Server != types.HiddenUserServer && jid.Server != types.GroupServer: - return types.EmptyJID, fmt.Errorf("%q is not a private chat or a group", chat) - case slices.Contains(own, jid.User): - return types.EmptyJID, errors.New("this is the chat of the user with itself: send to another chat") - } - - return jid, nil -} - // whatsappOwnIDs returns the own number and the own LID that are not empty. func whatsappOwnIDs(ids ...string) []string { var own []string + for _, id := range ids { if id != "" { own = append(own, id)