19 "go.mau.fi/whatsmeow/proto/waCompanionReg"
20 "go.mau.fi/whatsmeow/proto/waE2E"
21 "go.mau.fi/whatsmeow/store"
22 "go.mau.fi/whatsmeow/store/sqlstore"
23 "go.mau.fi/whatsmeow/types"
24 "go.mau.fi/whatsmeow/types/events"
25 waLog "go.mau.fi/whatsmeow/util/log"
26 // The session store of the library opens SQLite with this pure Go
28 _ "modernc.org/sqlite"
32 // whatsappDirName is the directory of the WhatsApp files in the data
34 whatsappDirName = "whatsapp"
36 // whatsappSessionFileName is the session database of the library in
38 whatsappSessionFileName = "session.db"
40 // whatsappCommand starts the input lines of the WhatsApp commands, such
41 // as "/whatsapp status".
42 whatsappCommand = "/whatsapp"
44 // whatsappUsage is the answer to a WhatsApp command that does not
46 whatsappUsage = "Use /whatsapp pair PHONE, /whatsapp status, or /whatsapp unlink. PHONE is the number of your WhatsApp account with its country code, such as +15550001111."
48 // whatsappPairSteps tells the user what to do with the pairing code,
49 // which comes in the next message.
50 whatsappPairSteps = "In WhatsApp on your phone, open Linked devices, tap Link a device, then tap Link with phone number instead, and enter the code of the next message. The code expires in about 2 minutes."
52 // whatsappPairExpired is the answer when the pairing code expires.
53 whatsappPairExpired = "The pairing code expired. Run /whatsapp pair PHONE again for a new code."
55 // whatsappTimeLayout is the layout of the times in the answers.
56 whatsappTimeLayout = "2006-01-02 15:04"
58 // whatsappPairTimeout is the longest time of a pairing. The library
59 // closes the pairing connection after about 160 seconds.
60 whatsappPairTimeout = 3 * time.Minute
62 // whatsappLogoutTimeout is the longest time of the logout call of an
64 whatsappLogoutTimeout = 30 * time.Second
66 // whatsappOutageNoticeDelay is the delay from a disconnection to its
67 // notice to the user.
68 whatsappOutageNoticeDelay = 10 * time.Minute
70 // whatsappRetryDelay is the first wait after a failed starting
72 whatsappRetryDelay = 2 * time.Second
74 // whatsappRetryLimit is the longest wait between starting connections.
75 whatsappRetryLimit = 5 * time.Minute
77 // whatsappRequests is the number of pair and unlink commands that can
81 // whatsappNotLinked is the answer of a WhatsApp tool without a link.
82 whatsappNotLinked = "WhatsApp is not linked. Tell the user to run /whatsapp pair PHONE in the terminal or in Telegram."
85 // WhatsAppClient is the part of the WhatsApp client that the daemon uses. The
86 // real one is a *whatsmeow.Client from whatsappNewClient, and the tests use a
88 type WhatsAppClient interface {
89 ConnectContext(ctx context.Context) error
91 GetQRChannel(ctx context.Context) (<-chan whatsmeow.QRChannelItem, error)
92 PairPhone(ctx context.Context, phone string, showPushNotification bool, clientType whatsmeow.PairClientType, clientDisplayName string) (string, error)
93 Logout(ctx context.Context) error
94 GetJoinedGroups(ctx context.Context) ([]*types.GroupInfo, error)
95 Download(ctx context.Context, message whatsmeow.DownloadableMessage) ([]byte, error)
96 Upload(ctx context.Context, data []byte, mediaType whatsmeow.MediaType) (whatsmeow.UploadResponse, error)
97 GenerateMessageID() types.MessageID
98 SendMessage(ctx context.Context, to types.JID, message *waE2E.Message, extra ...whatsmeow.SendRequestExtra) (whatsmeow.SendResponse, error)
101 // WhatsApp is the link of the daemon to the WhatsApp account of the user, as a
102 // linked device. The session database of the library in dir holds the keys of
103 // the device. Run keeps a linked device connected, and the library connects
104 // again by itself after a network failure.
106 // The commands pair and unlink go to Run as requests, and Run runs one at a
107 // time. A pairing runs in its own goroutine until its code expires, and a new
108 // pairing ends the one before. Its phone number and code go only to the answer
109 // of the client that asked. A logout from the phone comes as an event of the
110 // library, and Run deletes the session. The mutex protects the session, the
111 // client, and the state of the connection, which the event handler of the
112 // library changes from its own goroutines. The sessionMutex serializes session
113 // creation, close, removal, and snapshots. It is held before mutex, and stays
114 // held when removal releases mutex for client callbacks.
116 // ctx is the caller context until Run sets its context for library callbacks.
117 // The mutex protects it, so callbacks follow daemon cancellation.
119 // The inbox puts messages into batches and logs them, and groups holds the
120 // names of the groups. Both get a WhatsAppLink from link for each call, so they
121 // use the current device and client.
123 // The library logs nothing, because its log lines hold the ID of the own
124 // account, which is the phone number of the user. The own number and the own
125 // LID never go to the log of the agent or to the log of the daemon: redact
126 // removes them from the errors, and a batch shows the messages of the user as
127 // You and a mention of the user as @you. The IDs of the other chats go to the
128 // log, and those of private chats hold phone numbers, because the agent needs
130 type WhatsApp struct {
133 clock func() time.Time
135 newClient func(device *store.Device, handle func(event any) bool) WhatsAppClient
136 requests chan WhatsAppRequest
137 logouts chan struct{}
139 groups *WhatsAppGroups
143 sessionMutex sync.Mutex
145 container *sqlstore.Container
147 client WhatsAppClient
153 connectedAt time.Time
154 disconnectedAt time.Time
161 // WhatsAppLink is a copy of the link of the device at one time, for the parts
162 // of WhatsApp that do not own it: the groups, the batches, find, and send.
163 type WhatsAppLink struct {
166 client WhatsAppClient
171 // WhatsAppRequest is a pair or unlink command for Run, with the function that
172 // gives its answers to the client that asked. A pairing has the phone number.
173 type WhatsAppRequest struct {
176 answer func(text string)
179 // openWhatsApp opens the WhatsApp session in the data directory dir. Without
180 // the session file, WhatsApp is not linked. The session gives full access to
181 // the account, so a file that another user can read is an error. The clock
182 // gives the times of the status, logger gets the failures, and newClient makes
183 // the client of a device with its event handler.
184 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) {
187 dir: filepath.Join(dir, whatsappDirName),
190 newClient: newClient,
191 requests: make(chan WhatsAppRequest, whatsappRequests),
192 logouts: make(chan struct{}, 1),
193 groups: newWhatsAppGroups(logger),
198 w.state, err = openWhatsAppState(filepath.Join(w.dir, whatsappStateFileName))
203 w.inbox = newWhatsAppInbox(w.state, clock, w.groups, &ImageStore{dir: filepath.Join(dir, imageDirName)}, logger)
205 info, err := os.Stat(w.sessionPath())
206 if errors.Is(err, os.ErrNotExist) {
211 return nil, fmt.Errorf("read WhatsApp session: %w", err)
214 if info.Mode().Perm()&0o077 != 0 {
215 return nil, fmt.Errorf("%s holds the WhatsApp session, so its mode must be 0600", filepath.Join(whatsappDirName, whatsappSessionFileName))
218 err = w.openSession(ctx)
223 if w.device.ID != nil {
225 w.inbox.waitForCatchup()
227 w.own = whatsappOwnIDs(w.device.ID.User, w.device.LID.User)
233 func (w *WhatsApp) sessionPath() string {
234 return filepath.Join(w.dir, whatsappSessionFileName)
237 // openSession opens the session database and its first device. The caller holds
238 // the mutex, or no other goroutine uses w yet.
239 func (w *WhatsApp) openSession(ctx context.Context) error {
240 address, err := whatsappSessionAddress(w.sessionPath())
245 container, err := sqlstore.New(ctx, "sqlite", address, waLog.Noop)
247 return fmt.Errorf("open WhatsApp session: %w", err)
250 device, err := container.GetFirstDevice(ctx)
252 return errors.Join(fmt.Errorf("read WhatsApp device: %w", err), container.Close())
255 w.container = container
261 // Run connects a linked device, logs the closed batches in l, runs the pair and
262 // unlink commands, and handles a logout from the phone, until ctx ends. Its
263 // notes go to the log of l, and notify gets the notices for all clients. A
264 // failed first connection starts the same outage notice as a lost connection. A
265 // timer in the select retries it with a doubled wait from 2 seconds to 5
266 // minutes, without a retry goroutine or a wait that blocks commands or the
267 // inbox. A connection, unlink, logout, or ctx end stops retries. The library
268 // owns retries after a connection opens. Run returns the error of a write of
269 // the log or of the state file, which stops the daemon, because then messages
271 func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) error {
280 timer := time.NewTimer(whatsappRetryDelay)
284 var retry <-chan time.Time
286 delay := whatsappRetryDelay
293 delay = min(2*delay, whatsappRetryLimit)
297 stopRetry := func() {
308 var pairing sync.WaitGroup
323 err := w.inbox.deliver(ctx, l, w.link(), false)
331 case err = <-w.inbox.failures:
334 case <-w.inbox.due():
340 err = w.loggedOut(ctx, l)
344 case request := <-w.requests:
355 err = w.unlink(ctx, l, request.answer)
360 request.answer("WhatsApp is linked already. Run /whatsapp unlink first.")
362 pairCtx, cancelPair := context.WithTimeout(ctx, whatsappPairTimeout)
367 w.pair(pairCtx, ctx, l, request.phone, request.answer)
374 // connect tries the starting connection of the linked device with one client.
375 // It returns whether Run must retry. A failed call starts the shared outage
376 // state and logs its redacted cause once, because a returned ConnectContext
377 // error does not emit Disconnected. Connected logs recovery after
379 func (w *WhatsApp) connect(ctx context.Context) bool {
382 if ctx.Err() != nil || !w.linked || !w.retrying || w.closed {
389 w.client = w.newClient(w.device, w.handle)
395 err := client.ConnectContext(ctx)
399 if err == nil || ctx.Err() != nil || !w.retrying || !w.linked {
406 first := w.startOutage()
407 cause := whatsappRedact(w.own, err.Error())
411 w.logger.Warn("first connect failed, retrying", "err", cause)
417 // close ends the connection and the timer of the notice, and stops all notices.
418 func (w *WhatsApp) close() {
437 // link returns a copy of the link of the device: whether it is linked, the
438 // device, the client, whether it is connected, and the own IDs.
439 func (w *WhatsApp) link() WhatsAppLink {
441 defer w.mutex.Unlock()
447 connected: w.connected,
448 own: slices.Clone(w.own),
452 // loggedOut logs the open batches, deletes the session after a logout from the
453 // phone, notes it in the log of l, and tells all clients one time.
454 func (w *WhatsApp) loggedOut(ctx context.Context, l *TurnLoop) error {
459 w.logger.Error("logged out by the phone")
461 err := w.inbox.deliver(ctx, l, w.link(), true)
466 err = w.removeSession()
468 w.logger.Error("session delete failed", "err", err)
471 err = l.note("WhatsApp was unlinked from the phone.")
473 w.logger.Error("chat note failed", "err", err)
476 w.broadcast("WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again.")
481 // unlink logs the open batches, logs out the linked device, and deletes the
482 // session. When the logout call fails, as it does while WhatsApp is
483 // disconnected, the session is deleted all the same, and answer tells the user
484 // to remove the device in the phone. Its error is one of the log or the state
486 func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text string)) error {
494 answer("WhatsApp is not linked.")
499 err := w.inbox.deliver(ctx, l, w.link(), true)
507 ctx, cancel := context.WithTimeout(ctx, whatsappLogoutTimeout)
510 logoutErr = client.Logout(ctx)
513 err = w.removeSession()
515 answer("The unlink failed: " + w.redact(err.Error()))
520 w.logger.Info("unlinked")
522 err = l.note("WhatsApp is unlinked.")
524 w.logger.Error("chat note failed", "err", err)
527 if client == nil || logoutErr != nil {
528 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.")
533 answer("WhatsApp is unlinked.")
538 func (w *WhatsApp) isLinked() bool {
540 defer w.mutex.Unlock()
545 // pair links a new device to the account of phone with a pairing code. It gives
546 // the steps and the code to answer, and then the end of the pairing: the linked
547 // number, an expired code, or a failure. A pairing that a new one ends gives no
548 // answer. A link notes "WhatsApp is linked." in the log of l, with no number.
549 // The client of a pairing that does not link is disconnected.
551 // ctx limits the pairing, but the connection uses runCtx, the context of Run.
552 // The library keeps the context of the connection for its keep-alive, its event
553 // handler, and its reconnections, and after the link the server ends the stream
554 // and the library connects again. With ctx, the end of the pairing would stop
555 // that reconnection, and the linked device would never connect.
556 func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, answer func(text string)) {
557 device, err := w.newDevice(ctx)
559 answer("The pairing failed: " + err.Error())
564 client := w.newClient(device, w.handle)
572 qr, err := client.GetQRChannel(ctx)
574 err = client.ConnectContext(runCtx)
578 answer("The connection to WhatsApp failed: " + err.Error())
584 case item, open := <-qr:
585 if !open || item.Event != whatsmeow.QRChannelEventCode {
586 answer("WhatsApp did not start the pairing. Try again later.")
591 if errors.Is(ctx.Err(), context.DeadlineExceeded) {
592 answer("The connection to WhatsApp did not start the pairing in time. Try again later.")
598 // The server accepts only a display name of the form "Browser (OS)"
599 // with a common browser and OS, and refuses the pairing otherwise.
600 code, err := client.PairPhone(ctx, phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
602 answer("WhatsApp refused the pairing: " + err.Error() + ". Check the phone number and run /whatsapp pair PHONE again.")
607 answer(whatsappPairSteps)
611 var item whatsmeow.QRChannelItem
615 case item, open = <-qr:
617 if errors.Is(ctx.Err(), context.DeadlineExceeded) {
618 answer(whatsappPairExpired)
625 case !open || item.Event == whatsmeow.QRChannelTimeout.Event:
626 answer(whatsappPairExpired)
629 case item.Event == whatsmeow.QRChannelEventCode:
631 case item.Event == whatsmeow.QRChannelSuccess.Event:
635 w.logger.Info("linked")
637 err = l.note("WhatsApp is linked.")
639 w.logger.Error("chat note failed", "err", err)
642 answer("WhatsApp is linked to +" + phone + ".")
646 answer("The pairing failed: " + whatsappItemError(item) + ". Run /whatsapp pair PHONE again.")
653 // handle takes one event of the library and tells if it was handled. A message
654 // goes to the inbox. The names of the groups follow the events of the groups. A
655 // connection starts a catch-up of the inbox, which ends at OfflineSyncCompleted
656 // or after whatsappCatchupTimeout. A disconnection keeps the time of its start
657 // while Run or the library tries again. After whatsappOutageNoticeDelay of a
658 // disconnection, all clients get one notice, and they get one more when the
659 // connection comes back. A disconnection that the library does not try again
660 // gives a notice at once.
662 // The log of the daemon gets one line for each change of the connection of the
663 // linked device: connected, a failed first connection, a lost connection, and
664 // connected again with the length of the outage. The retries of the library and
665 // Run between them log nothing.
666 func (w *WhatsApp) handle(event any) bool {
672 switch event := event.(type) {
673 case *events.Message:
674 return w.inbox.receive(ctx, w.link(), event)
675 case *events.GroupInfo:
676 if event.Name != nil {
677 w.groups.set(event.JID.String(), event.Name.Name)
681 case *events.JoinedGroup:
682 w.groups.set(event.JID.String(), event.Name)
685 case *events.OfflineSyncCompleted:
689 case *events.Connected:
690 w.inbox.startCatchup()
693 var notice, line string
695 level := slog.LevelInfo
701 switch event := event.(type) {
702 case *events.PairSuccess:
703 w.own = whatsappOwnIDs(event.ID.User, event.LID.User)
704 case *events.Connected:
706 case !w.linked || w.connected:
707 case w.disconnectedAt.IsZero():
710 line = "connected again"
711 attrs = append(attrs, "after", w.clock().Sub(w.disconnectedAt))
716 w.connectedAt = w.clock()
723 notice = "WhatsApp is connected again."
725 case *events.Disconnected:
726 connected := w.connected
728 level = slog.LevelWarn
729 line = "disconnected, retrying"
731 line = "first connect failed, retrying"
734 case *events.LoggedOut:
737 case w.logouts <- struct{}{}:
740 case events.PermanentDisconnect:
745 notice = "WhatsApp is disconnected and does not connect again by itself: " + event.PermanentDisconnectDescription() + ". Restart the daemon to try again."
746 level = slog.LevelError
747 line = "disconnected for good, restart to retry"
748 attrs = append(attrs, "err", whatsappRedact(w.own, event.PermanentDisconnectDescription()))
759 w.logger.Log(ctx, level, line, attrs...)
769 // startOutage starts the shared outage time and notice timer once. The caller
770 // holds the mutex. Its result tells whether this is the first failure.
771 func (w *WhatsApp) startOutage() bool {
772 first := w.linked && (w.connected || w.disconnectedAt.IsZero())
774 w.disconnectedAt = w.clock()
776 disconnections := w.disconnections
777 w.timer = time.AfterFunc(whatsappOutageNoticeDelay, func() {
778 w.disconnectedLong(disconnections)
787 // removeSession ends the connection, closes the session database, and deletes
788 // its files. The session lock covers the cleanup after mutex is released, so a
789 // snapshot or a new session cannot open files that cleanup removes.
790 func (w *WhatsApp) removeSession() error {
791 w.sessionMutex.Lock()
792 defer w.sessionMutex.Unlock()
802 w.disconnectedAt = time.Time{}
808 container := w.container
813 w.inbox.stopCatchup()
821 if container != nil {
822 err = container.Close()
825 for _, suffix := range []string{"", "-wal", "-shm"} {
826 removeErr := os.Remove(w.sessionPath() + suffix)
827 if !errors.Is(removeErr, os.ErrNotExist) {
828 err = errors.Join(err, removeErr)
835 // broadcast gives text as a notice to all clients.
836 func (w *WhatsApp) broadcast(text string) {
843 if notify != nil && !closed {
851 // redact replaces the own number and the own LID in text with OWN.
852 func (w *WhatsApp) redact(text string) string {
854 defer w.mutex.Unlock()
856 return whatsappRedact(w.own, text)
859 // newDevice returns a new device in the session database. It makes the
860 // directory and the file of the session first, with their modes, when they do
862 func (w *WhatsApp) newDevice(ctx context.Context) (*store.Device, error) {
863 w.sessionMutex.Lock()
864 defer w.sessionMutex.Unlock()
867 defer w.mutex.Unlock()
869 if w.container == nil {
870 err := os.MkdirAll(w.dir, 0o700)
872 return nil, fmt.Errorf("create WhatsApp directory: %w", err)
875 file, err := os.OpenFile(w.sessionPath(), os.O_CREATE|os.O_RDWR, 0o600)
877 return nil, fmt.Errorf("create WhatsApp session: %w", err)
882 return nil, fmt.Errorf("create WhatsApp session: %w", err)
885 err = w.openSession(ctx)
891 w.device = w.container.NewDevice()
896 // linkedBy makes client the client of the linked device.
897 func (w *WhatsApp) linkedBy(client WhatsAppClient) {
899 defer w.mutex.Unlock()
905 // disconnectedLong gives the notice of a long disconnection, unless the
906 // disconnection with the number disconnections ended.
907 func (w *WhatsApp) disconnectedLong(disconnections int) {
910 long := !w.closed && w.linked && !w.connected && !w.noticed && w.disconnections == disconnections
915 since := w.disconnectedAt
919 w.broadcast("WhatsApp is disconnected since " + since.Format(whatsappTimeLayout) + ". OneChat tries again.")
923 // Command runs the WhatsApp command args, the text after /whatsapp, and gives
924 // each answer to answer. The answers go only to the client that sent the
926 func (w *WhatsApp) Command(args string, answer func(text string)) {
927 fields := strings.Fields(args)
928 request := WhatsAppRequest{
933 case len(fields) == 1 && fields[0] == "status":
937 case len(fields) == 1 && fields[0] == "unlink":
938 request.unlink = true
939 case len(fields) >= 2 && fields[0] == "pair":
940 phone, valid := whatsappPhone(strings.Join(fields[1:], ""))
942 answer("PHONE must be the number of your WhatsApp account with its country code: 7 to 15 digits, such as +15550001111.")
947 request.phone = phone
949 answer(whatsappUsage)
955 case w.requests <- request:
957 answer("Too many WhatsApp commands wait. Try again in a minute.")
961 // Status returns the state of the link: not linked, linked and connected, or
962 // linked and disconnected, with the times of the connection and the routes.
963 // Route source IDs can hold the phone numbers of other chats.
964 func (w *WhatsApp) Status() string {
966 defer w.mutex.Unlock()
970 listed := w.routes.List()
971 if listed != "No routes." {
972 routes = "\nWhatsApp routes:\n" + listed
978 return "WhatsApp is not linked. Run /whatsapp pair PHONE to link it." + routes
980 return "WhatsApp is linked and connected since " + w.connectedAt.Format(whatsappTimeLayout) + "." + routes
981 case w.disconnectedAt.IsZero():
982 return "WhatsApp is linked and not connected yet." + routes
983 case w.connectedAt.IsZero():
984 return "WhatsApp is linked and disconnected since " + w.disconnectedAt.Format(whatsappTimeLayout) + "." + routes
986 return "WhatsApp is linked and disconnected since " + w.disconnectedAt.Format(whatsappTimeLayout) + ". The last connection started at " + w.connectedAt.Format(whatsappTimeLayout) + "." + routes
990 // BackupSession writes a consistent SQLite snapshot to path while the session
991 // cannot be created, closed, or removed. The source opens without creation
992 // permission. No session file means no linked state to copy.
993 func (w *WhatsApp) BackupSession(ctx context.Context, path string) (err error) {
994 w.sessionMutex.Lock()
995 defer w.sessionMutex.Unlock()
998 defer w.mutex.Unlock()
1000 _, err = os.Stat(w.sessionPath())
1001 if errors.Is(err, os.ErrNotExist) {
1006 return fmt.Errorf("read session file: %w", err)
1009 err = os.MkdirAll(filepath.Dir(path), 0o700)
1011 return fmt.Errorf("create session snapshot directory: %w", err)
1014 address, err := whatsappSessionAddress(w.sessionPath())
1016 return fmt.Errorf("find session database: %w", err)
1019 database, err := sql.Open("sqlite", address+"&mode=rw")
1021 return fmt.Errorf("open session snapshot source: %w", err)
1025 err = errors.Join(err, database.Close())
1028 _, err = database.ExecContext(ctx, "VACUUM INTO ?", path)
1030 return fmt.Errorf("snapshot session database: %w", err)
1033 err = os.Chmod(path, 0o600)
1035 return fmt.Errorf("set session snapshot mode: %w", err)
1041 // Close closes the session database.
1042 func (w *WhatsApp) Close() error {
1043 w.sessionMutex.Lock()
1044 defer w.sessionMutex.Unlock()
1047 defer w.mutex.Unlock()
1049 if w.container == nil {
1053 err := w.container.Close()
1059 // Find returns the answer of whatsapp_find for name, as whatsappFind says.
1060 func (w *WhatsApp) Find(ctx context.Context, name string) (string, error) {
1061 return whatsappFind(ctx, w.link(), w.groups, name)
1064 // Send returns the answer of whatsapp_send for text and an optional stored
1065 // image to chat, with the durable send id that whatsappSend describes.
1066 func (w *WhatsApp) Send(ctx context.Context, id, chat, text, image string) (string, error) {
1067 return whatsappSend(ctx, w.link(), w.groups, w.state, w.inbox.images, w.logger, w.clock, id, chat, text, image)
1070 // whatsappOwnIDs returns the own number and the own LID that are not empty.
1071 func whatsappOwnIDs(ids ...string) []string {
1074 for _, id := range ids {
1076 own = append(own, id)
1083 // whatsappSessionAddress returns the SQLite address of the session database at
1084 // path, with foreign keys on, as the library needs, and a wait for a busy
1086 func whatsappSessionAddress(path string) (string, error) {
1087 path, err := filepath.Abs(path)
1089 return "", fmt.Errorf("find WhatsApp session: %w", err)
1095 RawQuery: "_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)",
1098 return address.String(), nil
1101 // whatsappRedact replaces each of the own IDs own in text with OWN.
1102 func whatsappRedact(own []string, text string) string {
1103 for _, id := range own {
1104 text = strings.ReplaceAll(text, id, "OWN")
1110 // whatsappItemError returns the error of a pairing item that is not a code, a
1111 // success, or a timeout.
1112 func whatsappItemError(item whatsmeow.QRChannelItem) string {
1113 if item.Error != nil {
1114 return item.Error.Error()
1120 // whatsappPhone returns the digits of phone, a number with its country code and
1121 // an optional + and dashes, and tells if it has the form of a number: 7 to 15
1122 // digits, with no 0 first.
1123 func whatsappPhone(phone string) (string, bool) {
1124 phone = strings.ReplaceAll(strings.TrimPrefix(phone, "+"), "-", "")
1125 valid := len(phone) >= 7 && len(phone) <= 15 && phone[0] != '0' && strings.Trim(phone, "0123456789") == ""
1130 // whatsappSetDeviceProps sets the device properties that the library sends at
1131 // the pairing. They are global values of the library, so the daemon sets them
1132 // once before any client exists. The phone shows the linked device with the
1133 // name OneChat and a desktop icon only when the platform type is known.
1134 func whatsappSetDeviceProps() {
1135 store.SetOSInfo("OneChat", [3]uint32{0, 1, 0})
1137 store.DeviceProps.PlatformType = waCompanionReg.DeviceProps_DESKTOP.Enum()
1140 // whatsappNewClient returns the client of the library for device, with handle
1141 // as its event handler. Run owns failed starting connections of linked devices,
1142 // so only an unlinked device enables the initial retry of the library for
1143 // pairing. The library still retries a lost connection. The client acknowledges
1144 // a message only after handle returns true, and downloads no old history.
1145 func whatsappNewClient(device *store.Device, handle func(event any) bool) WhatsAppClient {
1146 client := whatsmeow.NewClient(device, waLog.Noop)
1147 client.InitialAutoReconnect = device.ID == nil
1148 client.SynchronousAck = true
1149 client.ManualHistorySyncDownload = true
1150 client.AddEventHandlerWithSuccessStatus(handle)
1155 // whatsappContactName returns the name of the person jid: the full name, the
1156 // first name, the business name, or the push name of the contact in the session
1157 // of device, then push, the push name of a message, then the phone number. A
1158 // LID gets its phone number from the LID map of the session.
1159 func whatsappContactName(ctx context.Context, device *store.Device, jid types.JID, push string) string {
1161 candidates := []types.JID{jid}
1162 phone := types.EmptyJID
1164 case types.DefaultUserServer:
1166 case types.HiddenUserServer:
1167 mapped, err := device.LIDs.GetPNForLID(ctx, jid)
1168 if err == nil && !mapped.IsEmpty() {
1170 candidates = append(candidates, mapped)
1174 for _, candidate := range candidates {
1175 contact, err := device.Contacts.GetContact(ctx, candidate)
1176 if err != nil || !contact.Found {
1180 for _, name := range []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName} {
1190 case !phone.IsEmpty():
1191 return "+" + phone.User
1197 // whatsappChatHash returns a short hash of chat for the daemon log, so that a
1198 // log line can tell chats apart without a phone number.
1199 func whatsappChatHash(chat types.JID) string {
1200 digest := sha256.Sum256([]byte(chat.ToNonAD().String()))
1202 return fmt.Sprintf("%x", digest[:6])
1205 // whatsappCommandArgs returns the text after /whatsapp of a line of a WhatsApp
1206 // command, or false for any other line.
1207 func whatsappCommandArgs(line string) (string, bool) {
1208 line = strings.TrimSpace(line)
1209 if line == whatsappCommand {
1213 args, found := strings.CutPrefix(line, whatsappCommand+" ")
1218 return strings.TrimSpace(args), true