+ _, 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()
+
+ if w.container == nil {
+ return nil
+ }
+
+ err := w.container.Close()
+ w.container = nil
+
+ return err
+}
+
+// Find returns the answer of whatsapp_find for name, as whatsappFind says.
+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 text and an optional stored
+// image to chat, with the durable send id that whatsappSend describes.
+func (w *WhatsApp) Send(ctx context.Context, id, chat, text, image string) (string, error) {
+ return whatsappSend(ctx, w.link(), w.groups, w.state, w.inbox.images, w.logger, w.clock, id, chat, text, image)
+}
+
+// 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)
+ }
+ }
+
+ return own
+}
+
+// whatsappSessionAddress returns the SQLite address of the session database at
+// path, with foreign keys on, as the library needs, and a wait for a busy
+// database.
+func whatsappSessionAddress(path string) (string, error) {
+ path, err := filepath.Abs(path)
+ if err != nil {
+ return "", fmt.Errorf("find WhatsApp session: %w", err)
+ }
+
+ address := url.URL{
+ Scheme: "file",
+ Path: path,
+ RawQuery: "_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)",
+ }
+
+ return address.String(), nil
+}
+
+// whatsappRedact replaces each of the own IDs own in text with OWN.
+func whatsappRedact(own []string, text string) string {
+ for _, id := range own {