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
324 err := w.inbox.deliver(ctx, l, w.link(), false)
332 case err = <-w.inbox.failures:
335 case <-w.inbox.due():
341 err = w.loggedOut(ctx, l)
345 case request := <-w.requests:
356 err = w.unlink(ctx, l, request.answer)
361 request.answer("WhatsApp is linked already. Run /whatsapp unlink first.")
363 pairCtx, cancelPair := context.WithTimeout(ctx, whatsappPairTimeout)
368 w.pair(pairCtx, ctx, l, request.phone, request.answer)
375 // connect tries the starting connection of the linked device with one client.
376 // It returns whether Run must retry. A failed call starts the shared outage
377 // state and logs its redacted cause once, because a returned ConnectContext
378 // error does not emit Disconnected. Connected logs recovery after
380 func (w *WhatsApp) connect(ctx context.Context) bool {
383 if ctx.Err() != nil || !w.linked || !w.retrying || w.closed {
390 w.client = w.newClient(w.device, w.handle)
396 err := client.ConnectContext(ctx)
400 if err == nil || ctx.Err() != nil || !w.retrying || !w.linked {
407 first := w.startOutage()
408 cause := whatsappRedact(w.own, err.Error())
412 w.logger.Warn("first connect failed, retrying", "err", cause)
418 // close ends the connection and the timer of the notice, and stops all notices.
419 func (w *WhatsApp) close() {
438 // link returns a copy of the link of the device: whether it is linked, the
439 // device, the client, whether it is connected, and the own IDs.
440 func (w *WhatsApp) link() WhatsAppLink {
442 defer w.mutex.Unlock()
448 connected: w.connected,
449 own: slices.Clone(w.own),
453 // loggedOut logs the open batches, deletes the session after a logout from the
454 // phone, notes it in the log of l, and tells all clients one time.
455 func (w *WhatsApp) loggedOut(ctx context.Context, l *TurnLoop) error {
460 w.logger.Error("logged out by the phone")
462 err := w.inbox.deliver(ctx, l, w.link(), true)
467 err = w.removeSession()
469 w.logger.Error("session delete failed", "err", err)
472 err = l.note("WhatsApp was unlinked from the phone.")
474 w.logger.Error("chat note failed", "err", err)
477 w.broadcast("WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again.")
482 // unlink logs the open batches, logs out the linked device, and deletes the
483 // session. When the logout call fails, as it does while WhatsApp is
484 // disconnected, the session is deleted all the same, and answer tells the user
485 // to remove the device in the phone. Its error is one of the log or the state
487 func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text string)) error {
495 answer("WhatsApp is not linked.")
500 err := w.inbox.deliver(ctx, l, w.link(), true)
508 ctx, cancel := context.WithTimeout(ctx, whatsappLogoutTimeout)
511 logoutErr = client.Logout(ctx)
514 err = w.removeSession()
516 answer("The unlink failed: " + w.redact(err.Error()))
521 w.logger.Info("unlinked")
523 err = l.note("WhatsApp is unlinked.")
525 w.logger.Error("chat note failed", "err", err)
528 if client == nil || logoutErr != nil {
529 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.")
534 answer("WhatsApp is unlinked.")
539 func (w *WhatsApp) isLinked() bool {
541 defer w.mutex.Unlock()
546 // pair links a new device to the account of phone with a pairing code. It gives
547 // the steps and the code to answer, and then the end of the pairing: the linked
548 // number, an expired code, or a failure. A pairing that a new one ends gives no
549 // answer. A link notes "WhatsApp is linked." in the log of l, with no number.
550 // The client of a pairing that does not link is disconnected.
552 // ctx limits the pairing, but the connection uses runCtx, the context of Run.
553 // The library keeps the context of the connection for its keep-alive, its event
554 // handler, and its reconnections, and after the link the server ends the stream
555 // and the library connects again. With ctx, the end of the pairing would stop
556 // that reconnection, and the linked device would never connect.
557 func (w *WhatsApp) pair(ctx, runCtx context.Context, l *TurnLoop, phone string, answer func(text string)) {
558 device, err := w.newDevice(ctx)
560 answer("The pairing failed: " + err.Error())
565 client := w.newClient(device, w.handle)
573 qr, err := client.GetQRChannel(ctx)
575 err = client.ConnectContext(runCtx)
579 answer("The connection to WhatsApp failed: " + err.Error())
585 case item, open := <-qr:
586 if !open || item.Event != whatsmeow.QRChannelEventCode {
587 answer("WhatsApp did not start the pairing. Try again later.")
592 if errors.Is(ctx.Err(), context.DeadlineExceeded) {
593 answer("The connection to WhatsApp did not start the pairing in time. Try again later.")
599 // The server accepts only a display name of the form "Browser (OS)"
600 // with a common browser and OS, and refuses the pairing otherwise.
601 code, err := client.PairPhone(ctx, phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
603 answer("WhatsApp refused the pairing: " + err.Error() + ". Check the phone number and run /whatsapp pair PHONE again.")
608 answer(whatsappPairSteps)
612 var item whatsmeow.QRChannelItem
616 case item, open = <-qr:
618 if errors.Is(ctx.Err(), context.DeadlineExceeded) {
619 answer(whatsappPairExpired)
626 case !open || item.Event == whatsmeow.QRChannelTimeout.Event:
627 answer(whatsappPairExpired)
630 case item.Event == whatsmeow.QRChannelEventCode:
632 case item.Event == whatsmeow.QRChannelSuccess.Event:
636 w.logger.Info("linked")
638 err = l.note("WhatsApp is linked.")
640 w.logger.Error("chat note failed", "err", err)
643 answer("WhatsApp is linked to +" + phone + ".")
647 answer("The pairing failed: " + whatsappItemError(item) + ". Run /whatsapp pair PHONE again.")
654 // handle takes one event of the library and tells if it was handled. A message
655 // goes to the inbox. The names of the groups follow the events of the groups. A
656 // connection starts a catch-up of the inbox, which ends at OfflineSyncCompleted
657 // or after whatsappCatchupTimeout. A disconnection keeps the time of its start
658 // while Run or the library tries again. After whatsappOutageNoticeDelay of a
659 // disconnection, all clients get one notice, and they get one more when the
660 // connection comes back. A disconnection that the library does not try again
661 // gives a notice at once.
663 // The log of the daemon gets one line for each change of the connection of the
664 // linked device: connected, a failed first connection, a lost connection, and
665 // connected again with the length of the outage. The retries of the library and
666 // Run between them log nothing.
667 func (w *WhatsApp) handle(event any) bool {
673 switch event := event.(type) {
674 case *events.Message:
675 return w.inbox.receive(ctx, w.link(), event)
676 case *events.GroupInfo:
677 if event.Name != nil {
678 w.groups.set(event.JID.String(), event.Name.Name)
682 case *events.JoinedGroup:
683 w.groups.set(event.JID.String(), event.Name)
686 case *events.OfflineSyncCompleted:
690 case *events.Connected:
691 w.inbox.startCatchup()
694 var notice, line string
696 level := slog.LevelInfo
702 switch event := event.(type) {
703 case *events.PairSuccess:
704 w.own = whatsappOwnIDs(event.ID.User, event.LID.User)
705 case *events.Connected:
707 case !w.linked || w.connected:
708 case w.disconnectedAt.IsZero():
711 line = "connected again"
712 attrs = append(attrs, "after", w.clock().Sub(w.disconnectedAt))
717 w.connectedAt = w.clock()
724 notice = "WhatsApp is connected again."
726 case *events.Disconnected:
727 connected := w.connected
729 level = slog.LevelWarn
730 line = "disconnected, retrying"
732 line = "first connect failed, retrying"
735 case *events.LoggedOut:
738 case w.logouts <- struct{}{}:
741 case events.PermanentDisconnect:
746 notice = "WhatsApp is disconnected and does not connect again by itself: " + event.PermanentDisconnectDescription() + ". Restart the daemon to try again."
747 level = slog.LevelError
748 line = "disconnected for good, restart to retry"
749 attrs = append(attrs, "err", whatsappRedact(w.own, event.PermanentDisconnectDescription()))
760 w.logger.Log(ctx, level, line, attrs...)
770 // startOutage starts the shared outage time and notice timer once. The caller
771 // holds the mutex. Its result tells whether this is the first failure.
772 func (w *WhatsApp) startOutage() bool {
773 first := w.linked && (w.connected || w.disconnectedAt.IsZero())
775 w.disconnectedAt = w.clock()
777 disconnections := w.disconnections
778 w.timer = time.AfterFunc(whatsappOutageNoticeDelay, func() {
779 w.disconnectedLong(disconnections)
788 // removeSession ends the connection, closes the session database, and deletes
789 // its files. The session lock covers the cleanup after mutex is released, so a
790 // snapshot or a new session cannot open files that cleanup removes.
791 func (w *WhatsApp) removeSession() error {
792 w.sessionMutex.Lock()
793 defer w.sessionMutex.Unlock()
803 w.disconnectedAt = time.Time{}
809 container := w.container
814 w.inbox.stopCatchup()
822 if container != nil {
823 err = container.Close()
826 for _, suffix := range []string{"", "-wal", "-shm"} {
827 removeErr := os.Remove(w.sessionPath() + suffix)
828 if !errors.Is(removeErr, os.ErrNotExist) {
829 err = errors.Join(err, removeErr)
836 // broadcast gives text as a notice to all clients.
837 func (w *WhatsApp) broadcast(text string) {
844 if notify != nil && !closed {
852 // redact replaces the own number and the own LID in text with OWN.
853 func (w *WhatsApp) redact(text string) string {
855 defer w.mutex.Unlock()
857 return whatsappRedact(w.own, text)
860 // newDevice returns a new device in the session database. It makes the
861 // directory and the file of the session first, with their modes, when they do
863 func (w *WhatsApp) newDevice(ctx context.Context) (*store.Device, error) {
864 w.sessionMutex.Lock()
865 defer w.sessionMutex.Unlock()
868 defer w.mutex.Unlock()
870 if w.container == nil {
871 err := os.MkdirAll(w.dir, 0o700)
873 return nil, fmt.Errorf("create WhatsApp directory: %w", err)
876 file, err := os.OpenFile(w.sessionPath(), os.O_CREATE|os.O_RDWR, 0o600)
878 return nil, fmt.Errorf("create WhatsApp session: %w", err)
883 return nil, fmt.Errorf("create WhatsApp session: %w", err)
886 err = w.openSession(ctx)
892 w.device = w.container.NewDevice()
897 // linkedBy makes client the client of the linked device.
898 func (w *WhatsApp) linkedBy(client WhatsAppClient) {
900 defer w.mutex.Unlock()
906 // disconnectedLong gives the notice of a long disconnection, unless the
907 // disconnection with the number disconnections ended.
908 func (w *WhatsApp) disconnectedLong(disconnections int) {
911 long := !w.closed && w.linked && !w.connected && !w.noticed && w.disconnections == disconnections
916 since := w.disconnectedAt
920 w.broadcast("WhatsApp is disconnected since " + since.Format(whatsappTimeLayout) + ". OneChat tries again.")
924 // Command runs the WhatsApp command args, the text after /whatsapp, and gives
925 // each answer to answer. The answers go only to the client that sent the
927 func (w *WhatsApp) Command(args string, answer func(text string)) {
928 fields := strings.Fields(args)
929 request := WhatsAppRequest{
934 case len(fields) == 1 && fields[0] == "status":
938 case len(fields) == 1 && fields[0] == "unlink":
939 request.unlink = true
940 case len(fields) >= 2 && fields[0] == "pair":
941 phone, valid := whatsappPhone(strings.Join(fields[1:], ""))
943 answer("PHONE must be the number of your WhatsApp account with its country code: 7 to 15 digits, such as +15550001111.")
948 request.phone = phone
950 answer(whatsappUsage)
956 case w.requests <- request:
958 answer("Too many WhatsApp commands wait. Try again in a minute.")
962 // Status returns the state of the link: not linked, linked and connected, or
963 // linked and disconnected, with the times of the connection and the routes.
964 // Route source IDs can hold the phone numbers of other chats.
965 func (w *WhatsApp) Status() string {
967 defer w.mutex.Unlock()
971 listed := w.routes.List()
972 if listed != "No routes." {
973 routes = "\nWhatsApp routes:\n" + listed
979 return "WhatsApp is not linked. Run /whatsapp pair PHONE to link it." + routes
981 return "WhatsApp is linked and connected since " + w.connectedAt.Format(whatsappTimeLayout) + "." + routes
982 case w.disconnectedAt.IsZero():
983 return "WhatsApp is linked and not connected yet." + routes
984 case w.connectedAt.IsZero():
985 return "WhatsApp is linked and disconnected since " + w.disconnectedAt.Format(whatsappTimeLayout) + "." + routes
987 return "WhatsApp is linked and disconnected since " + w.disconnectedAt.Format(whatsappTimeLayout) + ". The last connection started at " + w.connectedAt.Format(whatsappTimeLayout) + "." + routes
991 // BackupSession writes a consistent SQLite snapshot to path while the session
992 // cannot be created, closed, or removed. The source opens without creation
993 // permission. No session file means no linked state to copy.
994 func (w *WhatsApp) BackupSession(ctx context.Context, path string) (err error) {
995 w.sessionMutex.Lock()
996 defer w.sessionMutex.Unlock()
999 defer w.mutex.Unlock()
1001 _, err = os.Stat(w.sessionPath())
1002 if errors.Is(err, os.ErrNotExist) {
1007 return fmt.Errorf("read session file: %w", err)
1010 err = os.MkdirAll(filepath.Dir(path), 0o700)
1012 return fmt.Errorf("create session snapshot directory: %w", err)
1015 address, err := whatsappSessionAddress(w.sessionPath())
1017 return fmt.Errorf("find session database: %w", err)
1020 database, err := sql.Open("sqlite", address+"&mode=rw")
1022 return fmt.Errorf("open session snapshot source: %w", err)
1026 err = errors.Join(err, database.Close())
1029 _, err = database.ExecContext(ctx, "VACUUM INTO ?", path)
1031 return fmt.Errorf("snapshot session database: %w", err)
1034 err = os.Chmod(path, 0o600)
1036 return fmt.Errorf("set session snapshot mode: %w", err)
1042 // Close closes the session database.
1043 func (w *WhatsApp) Close() error {
1044 w.sessionMutex.Lock()
1045 defer w.sessionMutex.Unlock()
1048 defer w.mutex.Unlock()
1050 if w.container == nil {
1054 err := w.container.Close()
1060 // Find returns the answer of whatsapp_find for name, as whatsappFind says.
1061 func (w *WhatsApp) Find(ctx context.Context, name string) (string, error) {
1062 return whatsappFind(ctx, w.link(), w.groups, name)
1065 // Send returns the answer of whatsapp_send for text and an optional stored
1066 // image to chat, with the durable send id that whatsappSend describes.
1067 func (w *WhatsApp) Send(ctx context.Context, id, chat, text, image string) (string, error) {
1068 return whatsappSend(ctx, w.link(), w.groups, w.state, w.inbox.images, w.logger, w.clock, id, chat, text, image)
1071 // whatsappOwnIDs returns the own number and the own LID that are not empty.
1072 func whatsappOwnIDs(ids ...string) []string {
1075 for _, id := range ids {
1077 own = append(own, id)
1084 // whatsappSessionAddress returns the SQLite address of the session database at
1085 // path, with foreign keys on, as the library needs, and a wait for a busy
1087 func whatsappSessionAddress(path string) (string, error) {
1088 path, err := filepath.Abs(path)
1090 return "", fmt.Errorf("find WhatsApp session: %w", err)
1096 RawQuery: "_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)",
1099 return address.String(), nil
1102 // whatsappRedact replaces each of the own IDs own in text with OWN.
1103 func whatsappRedact(own []string, text string) string {
1104 for _, id := range own {
1105 text = strings.ReplaceAll(text, id, "OWN")
1111 // whatsappItemError returns the error of a pairing item that is not a code, a
1112 // success, or a timeout.
1113 func whatsappItemError(item whatsmeow.QRChannelItem) string {
1114 if item.Error != nil {
1115 return item.Error.Error()
1121 // whatsappPhone returns the digits of phone, a number with its country code and
1122 // an optional + and dashes, and tells if it has the form of a number: 7 to 15
1123 // digits, with no 0 first.
1124 func whatsappPhone(phone string) (string, bool) {
1125 phone = strings.ReplaceAll(strings.TrimPrefix(phone, "+"), "-", "")
1126 valid := len(phone) >= 7 && len(phone) <= 15 && phone[0] != '0' && strings.Trim(phone, "0123456789") == ""
1131 // whatsappSetDeviceProps sets the device properties that the library sends at
1132 // the pairing. They are global values of the library, so the daemon sets them
1133 // once before any client exists. The phone shows the linked device with the
1134 // name OneChat and a desktop icon only when the platform type is known.
1135 func whatsappSetDeviceProps() {
1136 store.SetOSInfo("OneChat", [3]uint32{0, 1, 0})
1138 store.DeviceProps.PlatformType = waCompanionReg.DeviceProps_DESKTOP.Enum()
1141 // whatsappNewClient returns the client of the library for device, with handle
1142 // as its event handler. Run owns failed starting connections of linked devices,
1143 // so only an unlinked device enables the initial retry of the library for
1144 // pairing. The library still retries a lost connection. The client acknowledges
1145 // a message only after handle returns true, and downloads no old history.
1146 func whatsappNewClient(device *store.Device, handle func(event any) bool) WhatsAppClient {
1147 client := whatsmeow.NewClient(device, waLog.Noop)
1148 client.InitialAutoReconnect = device.ID == nil
1149 client.SynchronousAck = true
1150 client.ManualHistorySyncDownload = true
1151 client.AddEventHandlerWithSuccessStatus(handle)
1156 // whatsappContactName returns the name of the person jid: the full name, the
1157 // first name, the business name, or the push name of the contact in the session
1158 // of device, then push, the push name of a message, then the phone number. A
1159 // LID gets its phone number from the LID map of the session.
1160 func whatsappContactName(ctx context.Context, device *store.Device, jid types.JID, push string) string {
1162 candidates := []types.JID{jid}
1163 phone := types.EmptyJID
1165 case types.DefaultUserServer:
1167 case types.HiddenUserServer:
1168 mapped, err := device.LIDs.GetPNForLID(ctx, jid)
1169 if err == nil && !mapped.IsEmpty() {
1171 candidates = append(candidates, mapped)
1175 for _, candidate := range candidates {
1176 contact, err := device.Contacts.GetContact(ctx, candidate)
1177 if err != nil || !contact.Found {
1181 for _, name := range []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName} {
1191 case !phone.IsEmpty():
1192 return "+" + phone.User
1198 // whatsappChatHash returns a short hash of chat for the daemon log, so that a
1199 // log line can tell chats apart without a phone number.
1200 func whatsappChatHash(chat types.JID) string {
1201 digest := sha256.Sum256([]byte(chat.ToNonAD().String()))
1203 return fmt.Sprintf("%x", digest[:6])
1206 // whatsappCommandArgs returns the text after /whatsapp of a line of a WhatsApp
1207 // command, or false for any other line.
1208 func whatsappCommandArgs(line string) (string, bool) {
1209 line = strings.TrimSpace(line)
1210 if line == whatsappCommand {
1214 args, found := strings.CutPrefix(line, whatsappCommand+" ")
1219 return strings.TrimSpace(args), true