zap-go

Repositório zap-go
git clone git://git.sivaldodavi.com/zap-go.git
Log | Files | Refs | README

commit b7c83f8ecec8aa9f95f5c8d2e9d167f07deeb0fb
parent 0828da88ac5e15e48231e5163113ae807387935e
Author: Sivaldo <sivaldodavi@disroot.org>
Date:   Sat, 11 Apr 2026 12:15:31 -0300

Redução de linha de código

Diffstat:
M.gitignore | 2+-
Mmain.go | 506++++++++++++++++++++++++++-----------------------------------------------------
2 files changed, 166 insertions(+), 342 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,7 +1,7 @@ config.toml -zap.* +zap* # Dados sensíveis whatsapp.db diff --git a/main.go b/main.go @@ -32,33 +32,18 @@ import ( ) // --------------------------------------------------------------------------- -// Log em arquivo +// Globals // --------------------------------------------------------------------------- var ( + exeDir string logFile *os.File logMu sync.Mutex ) -func initLog(dir string) { - path := filepath.Join(dir, "zap.log") - rotateIfNeeded(path) - var err error - logFile, err = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - if err != nil { - fmt.Fprintln(os.Stderr, "não foi possível abrir log:", err) - } -} - -func rotateIfNeeded(path string) { - info, err := os.Stat(path) - if os.IsNotExist(err) || info == nil || info.Size() < 1<<20 { - return - } - if err := os.Rename(path, path+".1"); err != nil { - fmt.Fprintln(os.Stderr, "rotate log:", err) - } -} +// --------------------------------------------------------------------------- +// Log +// --------------------------------------------------------------------------- func logf(format string, args ...interface{}) { msg := fmt.Sprintf("[%s] %s\n", time.Now().Format("2006-01-02 15:04:05"), fmt.Sprintf(format, args...)) @@ -70,44 +55,43 @@ func logf(format string, args ...interface{}) { } } +func initLog() { + path := filepath.Join(exeDir, "zap.log") + if info, err := os.Stat(path); err == nil && info.Size() >= 1<<20 { + os.Rename(path, path+".1") + } + logFile, _ = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) +} + // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- -type SMTPConfig struct { - Host string `toml:"host"` - Port int `toml:"port"` - User string `toml:"user"` - Password string `toml:"password"` -} - type Config struct { - SMTP SMTPConfig `toml:"smtp"` + SMTP struct { + Host, User, Password string + Port int + } `toml:"smtp"` } -var exeDir string - -func loadConfig() Config { - cfgPath := filepath.Join(exeDir, "config.toml") - var cfg Config - if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil { - fmt.Fprintln(os.Stderr, "erro ao ler config.toml:", err) +func loadConfig() (cfg Config) { + if _, err := toml.DecodeFile(filepath.Join(exeDir, "config.toml"), &cfg); err != nil { + fmt.Fprintln(os.Stderr, "config.toml:", err) os.Exit(1) } - return cfg + return } // --------------------------------------------------------------------------- -// Lock file +// Lock // --------------------------------------------------------------------------- func acquireLock() (*os.File, bool) { - path := filepath.Join(exeDir, "zap.lock") - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + f, err := os.OpenFile(filepath.Join(exeDir, "zap.lock"), os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, false } - if err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { + if syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) != nil { f.Close() return nil, false } @@ -122,67 +106,27 @@ func releaseLock(f *os.File) { } // --------------------------------------------------------------------------- -// Abook & índice +// Abook // --------------------------------------------------------------------------- -type Contact struct { - Name, Mobile string -} - -type AbookIndex struct { - bySuffix map[string]string // últimos 8 dígitos → nome -} - -func buildIndex(contacts []Contact) AbookIndex { - idx := AbookIndex{bySuffix: make(map[string]string, len(contacts))} - for _, c := range contacts { - n := cleanNumber(c.Mobile) - if len(n) >= 8 { - idx.bySuffix[n[len(n)-8:]] = c.Name - } - } - return idx -} - -func (idx AbookIndex) Lookup(jidUser, pushName string) string { - clean := cleanNumber(jidUser) - variants := []string{clean} - if alt := removeNinthDigit(clean); alt != "" { - variants = append(variants, alt) - } - for _, v := range variants { - if len(v) >= 8 { - if name, ok := idx.bySuffix[v[len(v)-8:]]; ok { - return name - } - } - } - if pushName != "" { - return pushName - } - return jidUser -} +type Contact struct{ Name, Mobile string } func loadAbook() []Contact { - path := filepath.Join(os.Getenv("HOME"), ".local", "share", "abook", "addressbook") - f, err := os.Open(path) + f, err := os.Open(filepath.Join(os.Getenv("HOME"), ".local/share/abook/addressbook")) if err != nil { return nil } defer f.Close() var contacts []Contact var cur Contact - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) + for s := bufio.NewScanner(f); s.Scan(); { + line := strings.TrimSpace(s.Text()) if strings.HasPrefix(line, "[") && line != "[format]" { if cur.Name != "" && cur.Mobile != "" { contacts = append(contacts, cur) } cur = Contact{} - continue - } - if k, v, ok := strings.Cut(line, "="); ok { + } else if k, v, ok := strings.Cut(line, "="); ok { switch strings.TrimSpace(k) { case "name": cur.Name = strings.TrimSpace(v) @@ -197,7 +141,7 @@ func loadAbook() []Contact { return contacts } -func cleanNumber(s string) string { +func digitsOnly(s string) string { return strings.Map(func(r rune) rune { if r >= '0' && r <= '9' { return r @@ -206,15 +150,36 @@ func cleanNumber(s string) string { }, s) } -func removeNinthDigit(n string) string { - if len(n) == 13 && strings.HasPrefix(n, "55") { - return n[:4] + n[5:] +// Resolve nome: tenta sufixo de 8 dígitos, com/sem 9º dígito, fallback pushName. +func lookupName(jidUser, pushName string, idx map[string]string) string { + n := digitsOnly(jidUser) + for _, v := range []string{n, func() string { + if len(n) == 13 && strings.HasPrefix(n, "55") { + return n[:4] + n[5:] + } + return "" + }()} { + if len(v) >= 8 { + if name, ok := idx[v[len(v)-8:]]; ok { + return name + } + } } - return "" + if pushName != "" { + return pushName + } + return jidUser } -func portugueseWeekday(d time.Weekday) string { - return [...]string{"Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab"}[d] +func buildIndex(contacts []Contact) map[string]string { + idx := make(map[string]string, len(contacts)) + for _, c := range contacts { + n := digitsOnly(c.Mobile) + if len(n) >= 8 { + idx[n[len(n)-8:]] = c.Name + } + } + return idx } // --------------------------------------------------------------------------- @@ -223,17 +188,10 @@ func portugueseWeekday(d time.Weekday) string { const flushDelay = 2 * time.Minute -type FileAttachment struct { - Name string - Data []byte - Mime string -} - type PendingMsg struct { - msgType string - txt string - files []FileAttachment - ts time.Time + msgType, txt string + files []struct{ Name, Mime string; Data []byte } + ts time.Time } type Batcher struct { @@ -242,51 +200,37 @@ type Batcher struct { timers map[string]*time.Timer names map[string]string cfg Config - idx AbookIndex + idx map[string]string } -func newBatcher(cfg Config, idx AbookIndex) *Batcher { - return &Batcher{ - pending: make(map[string][]PendingMsg), - timers: make(map[string]*time.Timer), - names: make(map[string]string), - cfg: cfg, - idx: idx, +func newBatcher(cfg Config, idx map[string]string) *Batcher { + return &Batcher{cfg: cfg, idx: idx, + pending: map[string][]PendingMsg{}, + timers: map[string]*time.Timer{}, + names: map[string]string{}, } } func (b *Batcher) Add(jid, pushName string, msg PendingMsg) { b.mu.Lock() defer b.mu.Unlock() - b.pending[jid] = append(b.pending[jid], msg) - - if b.names[jid] == "" || b.names[jid] == jid { - b.names[jid] = b.idx.Lookup(jid, pushName) + if b.names[jid] == "" { + b.names[jid] = lookupName(jid, pushName, b.idx) } - - if t, ok := b.timers[jid]; ok { - if !t.Stop() { - select { - case <-t.C: - default: - } - } + if t := b.timers[jid]; t != nil { + t.Stop() } - - jidCopy := jid - b.timers[jid] = time.AfterFunc(flushDelay, func() { b.flush(jidCopy) }) + b.timers[jid] = time.AfterFunc(flushDelay, func() { b.flush(jid) }) } func (b *Batcher) flush(jid string) { b.mu.Lock() - msgs := b.pending[jid] - name := b.names[jid] + msgs, name := b.pending[jid], b.names[jid] delete(b.pending, jid) delete(b.timers, jid) delete(b.names, jid) b.mu.Unlock() - if len(msgs) == 0 { return } @@ -296,17 +240,12 @@ func (b *Batcher) flush(jid string) { e.To = []string{b.cfg.SMTP.User} e.Subject = fmt.Sprintf("Zap de %s [%d msg]", name, len(msgs)) + weekday := [...]string{"Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab"} var sb strings.Builder fmt.Fprintf(&sb, "De: %s\n%d mensagem(ns)\n\n", name, len(msgs)) - for i, m := range msgs { - fmt.Fprintf(&sb, "── %d [%s] %s %s ──\n", - i+1, m.msgType, - portugueseWeekday(m.ts.Weekday()), - m.ts.Format("02/01 15:04:05")) - if m.txt != "" { - fmt.Fprintf(&sb, "%s\n", m.txt) - } + fmt.Fprintf(&sb, "── %d [%s] %s %s ──\n%s\n\n", + i+1, m.msgType, weekday[m.ts.Weekday()], m.ts.Format("02/01 15:04:05"), m.txt) for _, fa := range m.files { if len(fa.Data) > 0 { mime := fa.Mime @@ -316,21 +255,17 @@ func (b *Batcher) flush(jid string) { e.Attach(bytes.NewReader(fa.Data), fa.Name, mime) } } - sb.WriteString("\n") } - e.Text = []byte(sb.String()) addr := fmt.Sprintf("%s:%d", b.cfg.SMTP.Host, b.cfg.SMTP.Port) auth := smtp.PlainAuth("", b.cfg.SMTP.User, b.cfg.SMTP.Password, b.cfg.SMTP.Host) - var err error if b.cfg.SMTP.Port == 465 { err = e.SendWithTLS(addr, auth, &tls.Config{ServerName: b.cfg.SMTP.Host}) } else { err = e.Send(addr, auth) } - if err != nil { logf("erro ao enviar email: %v", err) } else { @@ -342,28 +277,21 @@ func (b *Batcher) FlushAll() { b.mu.Lock() jids := make([]string, 0, len(b.pending)) for jid, t := range b.timers { - if !t.Stop() { - select { - case <-t.C: - default: - } - } + t.Stop() jids = append(jids, jid) } b.mu.Unlock() - for _, jid := range jids { b.flush(jid) } } // --------------------------------------------------------------------------- -// WhatsApp setup +// WhatsApp // --------------------------------------------------------------------------- func newClient() *whatsmeow.Client { - dbPath := filepath.Join(exeDir, "whatsapp.db") - dbURI := fmt.Sprintf("file:%s?_foreign_keys=on", dbPath) + dbURI := fmt.Sprintf("file:%s?_foreign_keys=on", filepath.Join(exeDir, "whatsapp.db")) container, err := sqlstore.New(context.Background(), "sqlite3", dbURI, waLog.Stdout("Database", "ERROR", true)) if err != nil { logf("erro db: %v", err) @@ -377,10 +305,10 @@ func connectClient(client *whatsmeow.Client) error { if client.Store.ID == nil { qrChan, err := client.GetQRChannel(context.Background()) if err != nil { - return fmt.Errorf("erro ao obter canal QR: %w", err) + return err } if err := client.Connect(); err != nil { - return fmt.Errorf("erro ao conectar: %w", err) + return err } for evt := range qrChan { if evt.Event == "code" { @@ -388,203 +316,127 @@ func connectClient(client *whatsmeow.Client) error { } } } else { - if err := client.Connect(); err != nil { - return fmt.Errorf("erro ao conectar: %w", err) - } + return client.Connect() } return nil } // --------------------------------------------------------------------------- -// Handler de mensagens recebidas +// Handler de mensagens // --------------------------------------------------------------------------- -type msgInfo struct { - msgType string - txt string - fileName string - mimeType string - media whatsmeow.DownloadableMessage -} - -func classifyMessage(raw *waE2E.Message) (msgInfo, bool) { - switch { - case raw.GetConversation() != "": - return msgInfo{msgType: "Texto", txt: raw.GetConversation()}, true - - case raw.GetExtendedTextMessage() != nil: - return msgInfo{msgType: "Texto", txt: raw.GetExtendedTextMessage().GetText()}, true - - case raw.GetImageMessage() != nil: - m := raw.GetImageMessage() - return msgInfo{ - msgType: "Imagem", - txt: m.GetCaption(), - fileName: "imagem.jpg", - mimeType: m.GetMimetype(), - media: m, - }, true - - case raw.GetStickerMessage() != nil: - return msgInfo{ - msgType: "Figurinha", - fileName: "sticker.webp", - mimeType: "image/webp", - media: raw.GetStickerMessage(), - }, true - - case raw.GetAudioMessage() != nil: - m := raw.GetAudioMessage() - return msgInfo{ - msgType: "Áudio", - txt: fmt.Sprintf("Duração: %ds", m.GetSeconds()), - fileName: "audio.ogg", - mimeType: m.GetMimetype(), - media: m, - }, true - - case raw.GetVideoMessage() != nil: - m := raw.GetVideoMessage() - return msgInfo{ - msgType: "Vídeo", - txt: m.GetCaption(), - fileName: "video.mp4", - mimeType: m.GetMimetype(), - media: m, - }, true - - case raw.GetDocumentMessage() != nil: - m := raw.GetDocumentMessage() - name := m.GetFileName() - if name == "" { - name = "arquivo_recebido" +func buildHandler(client *whatsmeow.Client, batcher *Batcher) func(interface{}) { + type msgSpec struct { + msgType, txt, fileName, mime string + media whatsmeow.DownloadableMessage + } + + classify := func(r *waE2E.Message) (msgSpec, bool) { + switch { + case r.GetConversation() != "": + return msgSpec{msgType: "Texto", txt: r.GetConversation()}, true + case r.GetExtendedTextMessage() != nil: + return msgSpec{msgType: "Texto", txt: r.GetExtendedTextMessage().GetText()}, true + case r.GetImageMessage() != nil: + m := r.GetImageMessage() + return msgSpec{"Imagem", m.GetCaption(), "imagem.jpg", m.GetMimetype(), m}, true + case r.GetStickerMessage() != nil: + return msgSpec{"Figurinha", "", "sticker.webp", "image/webp", r.GetStickerMessage()}, true + case r.GetAudioMessage() != nil: + m := r.GetAudioMessage() + return msgSpec{"Áudio", fmt.Sprintf("Duração: %ds", m.GetSeconds()), "audio.ogg", m.GetMimetype(), m}, true + case r.GetVideoMessage() != nil: + m := r.GetVideoMessage() + return msgSpec{"Vídeo", m.GetCaption(), "video.mp4", m.GetMimetype(), m}, true + case r.GetDocumentMessage() != nil: + m := r.GetDocumentMessage() + name := m.GetFileName() + if name == "" { + name = "arquivo_recebido" + } + return msgSpec{"Documento", name, name, m.GetMimetype(), m}, true } - return msgInfo{ - msgType: "Documento", - txt: name, - fileName: name, - mimeType: m.GetMimetype(), - media: m, - }, true + return msgSpec{}, false } - return msgInfo{}, false -} - -func buildHandler(client *whatsmeow.Client, batcher *Batcher) func(interface{}) { return func(evt interface{}) { v, ok := evt.(*events.Message) - if !ok || v.Info.IsFromMe { + if !ok || v.Info.IsFromMe || v.Info.Chat.Server == "g.us" || v.Info.Chat.User == "status" { return } - if v.Info.Chat.Server == "g.us" || v.Info.Chat.User == "status" { - return - } - - rawMsg := v.Message - if rawMsg.GetDeviceSentMessage() != nil { - rawMsg = rawMsg.GetDeviceSentMessage().GetMessage() + raw := v.Message + if raw.GetDeviceSentMessage() != nil { + raw = raw.GetDeviceSentMessage().GetMessage() } - - info, ok := classifyMessage(rawMsg) + spec, ok := classify(raw) if !ok { return } - - msg := PendingMsg{ - msgType: info.msgType, - txt: info.txt, - ts: v.Info.Timestamp, - } - - if info.media != nil { + msg := PendingMsg{msgType: spec.msgType, txt: spec.txt, ts: v.Info.Timestamp} + if spec.media != nil { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - data, err := client.Download(ctx, info.media) - if err != nil { + if data, err := client.Download(ctx, spec.media); err == nil && len(data) > 0 { + msg.files = []struct{ Name, Mime string; Data []byte }{{spec.fileName, spec.mime, data}} + } else if err != nil { logf("erro download: %v", err) - } else if len(data) > 0 { - msg.files = []FileAttachment{{ - Name: info.fileName, - Data: data, - Mime: info.mimeType, - }} } } - batcher.Add(v.Info.Sender.User, v.Info.PushName, msg) } } // --------------------------------------------------------------------------- -// Envio de arquivos +// Send file // --------------------------------------------------------------------------- func sendFile(client *whatsmeow.Client, jid types.JID, path, caption string) error { data, err := os.ReadFile(path) if err != nil { - return fmt.Errorf("leitura: %w", err) + return err } - mime := mimetype.Detect(data).String() ctx := context.Background() + base := filepath.Base(path) switch { case strings.HasPrefix(mime, "image/"): - uploaded, err := client.Upload(ctx, data, whatsmeow.MediaImage) + up, err := client.Upload(ctx, data, whatsmeow.MediaImage) if err != nil { - return fmt.Errorf("upload: %w", err) + return err } - _, err = client.SendMessage(ctx, jid, &waE2E.Message{ - ImageMessage: &waE2E.ImageMessage{ - Caption: proto.String(caption), - Mimetype: proto.String(mime), - URL: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - FileEncSHA256: uploaded.FileEncSHA256, - FileSHA256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(data))), - }, - }) + _, err = client.SendMessage(ctx, jid, &waE2E.Message{ImageMessage: &waE2E.ImageMessage{ + Caption: proto.String(caption), Mimetype: proto.String(mime), + URL: proto.String(up.URL), DirectPath: proto.String(up.DirectPath), + MediaKey: up.MediaKey, FileEncSHA256: up.FileEncSHA256, + FileSHA256: up.FileSHA256, FileLength: proto.Uint64(uint64(len(data))), + }}) return err case strings.HasPrefix(mime, "video/"): - uploaded, err := client.Upload(ctx, data, whatsmeow.MediaVideo) + up, err := client.Upload(ctx, data, whatsmeow.MediaVideo) if err != nil { - return fmt.Errorf("upload: %w", err) + return err } - _, err = client.SendMessage(ctx, jid, &waE2E.Message{ - VideoMessage: &waE2E.VideoMessage{ - Caption: proto.String(caption), - Mimetype: proto.String(mime), - URL: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - FileEncSHA256: uploaded.FileEncSHA256, - FileSHA256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(data))), - }, - }) + _, err = client.SendMessage(ctx, jid, &waE2E.Message{VideoMessage: &waE2E.VideoMessage{ + Caption: proto.String(caption), Mimetype: proto.String(mime), + URL: proto.String(up.URL), DirectPath: proto.String(up.DirectPath), + MediaKey: up.MediaKey, FileEncSHA256: up.FileEncSHA256, + FileSHA256: up.FileSHA256, FileLength: proto.Uint64(uint64(len(data))), + }}) return err - default: // PDF, DOCX e qualquer outro documento - uploaded, err := client.Upload(ctx, data, whatsmeow.MediaDocument) + default: + up, err := client.Upload(ctx, data, whatsmeow.MediaDocument) if err != nil { - return fmt.Errorf("upload: %w", err) + return err } - _, err = client.SendMessage(ctx, jid, &waE2E.Message{ - DocumentMessage: &waE2E.DocumentMessage{ - FileName: proto.String(filepath.Base(path)), - Mimetype: proto.String(mime), - URL: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - FileEncSHA256: uploaded.FileEncSHA256, - FileSHA256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(data))), - }, - }) + _, err = client.SendMessage(ctx, jid, &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{ + FileName: proto.String(base), Mimetype: proto.String(mime), + URL: proto.String(up.URL), DirectPath: proto.String(up.DirectPath), + MediaKey: up.MediaKey, FileEncSHA256: up.FileEncSHA256, + FileSHA256: up.FileSHA256, FileLength: proto.Uint64(uint64(len(data))), + }}) return err } } @@ -602,31 +454,21 @@ func runDaemon() { defer releaseLock(lock) cfg := loadConfig() - initLog(exeDir) - contacts := loadAbook() - idx := buildIndex(contacts) + initLog() + idx := buildIndex(loadAbook()) client := newClient() batcher := newBatcher(cfg, idx) - client.AddEventHandler(buildHandler(client, batcher)) - client.AddEventHandler(func(evt interface{}) { if _, ok := evt.(*events.Disconnected); !ok { return } logf("desconectado — tentando reconectar...") - backoff := 5 * time.Second - for { - jitter := time.Duration(rand.Intn(3000)) * time.Millisecond - time.Sleep(backoff + jitter) + for backoff := 5 * time.Second; ; backoff = min(backoff*2, 5*time.Minute) { + time.Sleep(backoff + time.Duration(rand.Intn(3000))*time.Millisecond) if err := connectClient(client); err == nil { - logf("reconectado com sucesso") + logf("reconectado") return - } else { - logf("reconexão falhou: %v — tentando em %s", err, backoff) - } - if backoff < 5*time.Minute { - backoff *= 2 } } }) @@ -635,14 +477,13 @@ func runDaemon() { logf("erro ao conectar: %v", err) os.Exit(1) } - logf("daemon iniciado") quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - logf("encerrando — enviando lotes pendentes...") + logf("encerrando...") batcher.FlushAll() client.Disconnect() logf("encerrado") @@ -650,12 +491,6 @@ func runDaemon() { // --------------------------------------------------------------------------- // Send -// -// Uso: -// zap send → escolhe contato via fzf, loop de texto -// zap send foto.jpg → envia imagem -// zap send doc.pdf "legenda" → envia documento com legenda/caption -// zap send video.mp4 "legenda" → envia vídeo com legenda // --------------------------------------------------------------------------- func runSend() { @@ -664,7 +499,6 @@ func runSend() { for _, c := range contacts { lines = append(lines, fmt.Sprintf("%s\t%s", c.Name, c.Mobile)) } - cmd := exec.Command("fzf", "--prompt=para: ", "--delimiter=\t", "--with-nth=1") cmd.Stdin = strings.NewReader(strings.Join(lines, "\n")) cmd.Stderr = os.Stderr @@ -672,50 +506,41 @@ func runSend() { if err != nil { return } - parts := strings.SplitN(strings.TrimSpace(string(out)), "\t", 2) if len(parts) < 2 { fmt.Fprintln(os.Stderr, "contato inválido") return } - - jid, _ := types.ParseJID(cleanNumber(parts[1]) + "@s.whatsapp.net") + jid, _ := types.ParseJID(digitsOnly(parts[1]) + "@s.whatsapp.net") client := newClient() if err := connectClient(client); err != nil { fmt.Fprintln(os.Stderr, "erro ao conectar:", err) return } defer client.Disconnect() - fmt.Printf("Para: %s\n", parts[0]) - // Envio de arquivo passado como argumento if len(os.Args) >= 3 { - filePath := os.Args[2] caption := "" if len(os.Args) >= 4 { caption = os.Args[3] } - if err := sendFile(client, jid, filePath, caption); err != nil { + if err := sendFile(client, jid, os.Args[2], caption); err != nil { fmt.Fprintln(os.Stderr, "erro ao enviar arquivo:", err) } return } - // Loop interativo de texto - scanner := bufio.NewScanner(os.Stdin) - for { + for s := bufio.NewScanner(os.Stdin); ; { fmt.Print("> ") - if !scanner.Scan() { + if !s.Scan() { break } - t := strings.TrimSpace(scanner.Text()) - if t == "" { + if t := strings.TrimSpace(s.Text()); t != "" { + client.SendMessage(context.Background(), jid, &waE2E.Message{Conversation: proto.String(t)}) + } else { break } - client.SendMessage(context.Background(), jid, &waE2E.Message{ - Conversation: proto.String(t), - }) } } @@ -732,7 +557,6 @@ func main() { fmt.Println("uso: zap <daemon|send> [arquivo] [legenda]") return } - switch os.Args[1] { case "daemon": runDaemon()