16 "go.mau.fi/whatsmeow/store"
17 "go.mau.fi/whatsmeow/store/sqlstore"
18 "go.mau.fi/whatsmeow/types/events"
19 waLog "go.mau.fi/whatsmeow/util/log"
21 // The session store of the library opens SQLite with this pure Go
23 _ "modernc.org/sqlite"
27 // whatsappDirName is the directory of the WhatsApp files in the data
29 whatsappDirName = "whatsapp"
31 // whatsappSessionFileName is the session database of the library in
33 whatsappSessionFileName = "session.db"
35 // whatsappCommand starts the input lines of the WhatsApp commands, such
36 // as "/whatsapp status".
37 whatsappCommand = "/whatsapp"
39 // whatsappUsage is the answer to a WhatsApp command that does not exist.
40 whatsappUsage = "Use /whatsapp status."
42 // whatsappTimeLayout is the layout of the times in the answers.
43 whatsappTimeLayout = "2006-01-02 15:04"
46 // WhatsAppClient is the part of the WhatsApp client that the daemon uses. The
47 // real one is a *whatsmeow.Client from whatsappNewClient, and the tests use a
49 type WhatsAppClient interface {
50 ConnectContext(ctx context.Context) error
54 // openWhatsApp opens the WhatsApp session in the data directory dir. Without
55 // the session file, WhatsApp is not linked. The session gives full access to
56 // the account, so a file that another user can read is an error. The clock
57 // gives the times of the status, logger gets the failures, and newClient makes
58 // the client of a linked device with its event handler.
59 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) {
61 dir: filepath.Join(dir, whatsappDirName),
67 path := filepath.Join(w.dir, whatsappSessionFileName)
68 info, err := os.Stat(path)
69 if errors.Is(err, os.ErrNotExist) {
74 return nil, fmt.Errorf("read WhatsApp session: %w", err)
77 if info.Mode().Perm()&0o077 != 0 {
78 return nil, fmt.Errorf("%s holds the WhatsApp session, so its mode must be 0600", filepath.Join(whatsappDirName, whatsappSessionFileName))
81 address, err := whatsappSessionAddress(path)
86 w.container, err = sqlstore.New(ctx, "sqlite", address, waLog.Noop)
88 return nil, fmt.Errorf("open WhatsApp session: %w", err)
91 w.device, err = w.container.GetFirstDevice(ctx)
93 return nil, errors.Join(fmt.Errorf("read WhatsApp device: %w", err), w.container.Close())
99 // WhatsApp is the link of the daemon to the WhatsApp account of the user, as
100 // a linked device. The session database of the library in dir holds the keys
101 // of the device. A device with an ID is linked, and Run keeps it connected.
102 // The library connects again by itself after a network failure. The mutex
103 // protects the state of the connection, which the event handler of the library
104 // changes from its own goroutines.
106 // The library logs nothing, because its log lines hold the ID of the own
107 // account, which is the phone number of the user.
108 type WhatsApp struct {
110 clock func() time.Time
112 newClient func(device *store.Device, handle func(event any) bool) WhatsAppClient
113 container *sqlstore.Container
115 client WhatsAppClient
119 connectedAt time.Time
120 disconnectedAt time.Time
123 // Run connects a linked device and keeps the connection until ctx ends. A
124 // failed first connection is logged, and the library tries again when the
125 // failure is one of the network.
126 func (w *WhatsApp) Run(ctx context.Context) {
132 w.client = w.newClient(w.device, w.handle)
133 err := w.client.ConnectContext(ctx)
135 w.logger.Printf("connect: %v", err)
139 w.client.Disconnect()
142 // Command runs the WhatsApp command args, the text after /whatsapp, and gives
143 // each answer to answer. The answers go only to the client that sent the
145 func (w *WhatsApp) Command(args string, answer func(text string)) {
146 switch strings.TrimSpace(args) {
150 answer(whatsappUsage)
154 // Status returns the state of the link: not linked, linked and connected, or
155 // linked and disconnected, with the times of the connection. It never holds
157 func (w *WhatsApp) Status() string {
159 defer w.mutex.Unlock()
163 return "WhatsApp is not linked."
165 return "WhatsApp is linked and connected since " + w.connectedAt.Format(whatsappTimeLayout) + "."
166 case w.disconnectedAt.IsZero():
167 return "WhatsApp is linked and not connected yet."
168 case w.connectedAt.IsZero():
169 return "WhatsApp is linked and disconnected since " + w.disconnectedAt.Format(whatsappTimeLayout) + "."
171 return "WhatsApp is linked and disconnected since " + w.disconnectedAt.Format(whatsappTimeLayout) + ". The last connection started at " + w.connectedAt.Format(whatsappTimeLayout) + "."
175 // Close closes the session database.
176 func (w *WhatsApp) Close() error {
177 if w.container == nil {
181 return w.container.Close()
184 // linked tells if the session holds a linked device.
185 func (w *WhatsApp) linked() bool {
186 return w.device != nil && w.device.ID != nil
189 // handle takes one event of the library and tells if it was handled. A
190 // disconnection keeps the time of its start while the library tries again.
191 func (w *WhatsApp) handle(event any) bool {
193 defer w.mutex.Unlock()
195 switch event.(type) {
196 case *events.Connected:
198 w.connectedAt = w.clock()
199 case *events.Disconnected:
200 if w.connected || w.disconnectedAt.IsZero() {
201 w.disconnectedAt = w.clock()
210 // whatsappNewClient returns the client of the library for device, with handle
211 // as its event handler. The client tries again after a failed first
212 // connection, acknowledges a message only after handle returns true, and
213 // downloads no old history.
214 func whatsappNewClient(device *store.Device, handle func(event any) bool) WhatsAppClient {
215 client := whatsmeow.NewClient(device, waLog.Noop)
216 client.InitialAutoReconnect = true
217 client.SynchronousAck = true
218 client.ManualHistorySyncDownload = true
219 client.AddEventHandlerWithSuccessStatus(handle)
224 // whatsappSessionAddress returns the SQLite address of the session database at
225 // path, with foreign keys on, as the library needs, and a wait for a busy
227 func whatsappSessionAddress(path string) (string, error) {
228 path, err := filepath.Abs(path)
230 return "", fmt.Errorf("find WhatsApp session: %w", err)
236 RawQuery: "_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)",
239 return address.String(), nil
242 // whatsappCommandArgs returns the text after /whatsapp of a line of a WhatsApp
243 // command, or false for any other line.
244 func whatsappCommandArgs(line string) (string, bool) {
245 line = strings.TrimSpace(line)
246 if line == whatsappCommand {
250 args, found := strings.CutPrefix(line, whatsappCommand+" ")
255 return strings.TrimSpace(args), true