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 | +- |
| M | main.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()