zap-go

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

commit 278d2e078e26cab65856c1e59a9fd3d80830be73
parent 1d596be5caaac14a7e652ecba1e544150b17a8f6
Author: Sivaldo <sivaldodavi@disroot.org>
Date:   Fri,  3 Apr 2026 19:54:51 -0300

Limpeza: sem grupo, sem status; adição: anexos e mídias

Diffstat:
Mgo.mod | 1+
Mgo.sum | 2++
Mmain.go | 205+++++++++++++++++++++++++++++++++----------------------------------------------
3 files changed, 88 insertions(+), 120 deletions(-)

diff --git a/go.mod b/go.mod @@ -4,6 +4,7 @@ go 1.25.5 require ( github.com/BurntSushi/toml v1.6.0 + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/mattn/go-sqlite3 v1.14.40 github.com/mdp/qrterminal/v3 v3.2.1 go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4 diff --git a/go.sum b/go.sum @@ -22,6 +22,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= diff --git a/main.go b/main.go @@ -15,6 +15,7 @@ import ( "time" "github.com/BurntSushi/toml" + "github.com/jordan-wright/email" "github.com/mdp/qrterminal/v3" _ "github.com/mattn/go-sqlite3" "go.mau.fi/whatsmeow" @@ -52,52 +53,37 @@ func loadConfig() Config { return cfg } -// --- Email --- +// --- Email com suporte a Anexo --- -func portugueseWeekday(d time.Weekday) string { - return [...]string{"Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab"}[d] -} +func sendEmailWithAttachment(cfg Config, subject, body, fileName string, fileData []byte) { + e := email.NewEmail() + e.From = fmt.Sprintf("Zap <%s>", cfg.SMTP.User) + e.To = []string{cfg.SMTP.User} + e.Subject = subject + e.Text = []byte(body) + + if len(fileData) > 0 { + e.Attach(strings.NewReader(string(fileData)), fileName, "application/octet-stream") + } -func sendEmailRaw(cfg Config, subject, body string) { addr := fmt.Sprintf("%s:%d", cfg.SMTP.Host, cfg.SMTP.Port) auth := smtp.PlainAuth("", cfg.SMTP.User, cfg.SMTP.Password, cfg.SMTP.Host) - msg := fmt.Sprintf( - "From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s", - cfg.SMTP.User, cfg.SMTP.User, subject, body, - ) - var err error if cfg.SMTP.Port == 465 { - conn, e := tls.Dial("tcp", addr, &tls.Config{ServerName: cfg.SMTP.Host}) - if e != nil { - fmt.Fprintln(os.Stderr, "erro tls:", e) - return - } - c, e := smtp.NewClient(conn, cfg.SMTP.Host) - if e != nil { - fmt.Fprintln(os.Stderr, "erro smtp:", e) - return - } - defer c.Close() - c.Auth(auth) - c.Mail(cfg.SMTP.User) - c.Rcpt(cfg.SMTP.User) - w, _ := c.Data() - fmt.Fprint(w, msg) - err = w.Close() + err = e.SendWithTLS(addr, auth, &tls.Config{ServerName: cfg.SMTP.Host}) } else { - err = smtp.SendMail(addr, auth, cfg.SMTP.User, []string{cfg.SMTP.User}, []byte(msg)) + err = e.Send(addr, auth) } if err != nil { fmt.Fprintln(os.Stderr, "erro ao enviar email:", err) } else { - fmt.Printf("[%s] email enviado: %s\n", time.Now().Format("15:04:05"), subject) + fmt.Printf("[%s] Email enviado: %s\n", time.Now().Format("15:04:05"), subject) } } -// --- WhatsApp --- +// --- WhatsApp Setup --- func newClient() *whatsmeow.Client { exePath, _ := os.Executable() @@ -105,10 +91,7 @@ func newClient() *whatsmeow.Client { dbPath := filepath.Join(filepath.Dir(realPath), "whatsapp.db") dbURI := fmt.Sprintf("file:%s?_foreign_keys=on", dbPath) - container, err := sqlstore.New( - context.Background(), "sqlite3", dbURI, - waLog.Stdout("Database", "ERROR", true), - ) + container, err := sqlstore.New(context.Background(), "sqlite3", dbURI, waLog.Stdout("Database", "ERROR", true)) if err != nil { fmt.Fprintln(os.Stderr, "erro db:", err) os.Exit(1) @@ -131,11 +114,10 @@ func connectClient(client *whatsmeow.Client) { } } -// --- Abook --- +// --- Abook & Helpers --- type Contact struct { - Name string - Mobile string + Name, Mobile string } func loadAbook() []Contact { @@ -160,24 +142,18 @@ func loadAbook() []Contact { } if k, v, ok := strings.Cut(line, "="); ok { switch strings.TrimSpace(k) { - case "name": - cur.Name = strings.TrimSpace(v) - case "mobile": - cur.Mobile = strings.TrimSpace(v) + case "name": cur.Name = strings.TrimSpace(v) + case "mobile": cur.Mobile = strings.TrimSpace(v) } } } - if cur.Name != "" && cur.Mobile != "" { - contacts = append(contacts, cur) - } + if cur.Name != "" && cur.Mobile != "" { contacts = append(contacts, cur) } return contacts } func cleanNumber(s string) string { return strings.Map(func(r rune) rune { - if r >= '0' && r <= '9' { - return r - } + if r >= '0' && r <= '9' { return r } return -1 }, s) } @@ -188,18 +164,17 @@ func findName(jidUser string, contacts []Contact, pushName string) string { suffix := clean[len(clean)-8:] for _, c := range contacts { s := cleanNumber(c.Mobile) - if len(s) >= 8 && s[len(s)-8:] == suffix { - return c.Name - } + if len(s) >= 8 && s[len(s)-8:] == suffix { return c.Name } } } - if pushName != "" { - return pushName - } - return jidUser + return (func() string { if pushName != "" { return pushName }; return jidUser })() +} + +func portugueseWeekday(d time.Weekday) string { + return [...]string{"Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab"}[d] } -// --- Comandos --- +// --- Daemon --- func runDaemon() { cfg := loadConfig() @@ -208,40 +183,58 @@ func runDaemon() { client.AddEventHandler(func(evt interface{}) { v, ok := evt.(*events.Message) - if !ok || v.Info.IsFromMe { - return + if !ok || v.Info.IsFromMe { return } + + // --- FILTROS DE SEGURANÇA --- + // 1. Bloqueia Mensagens de Grupos (@g.us) + if v.Info.Chat.Server == "g.us" { return } + + // 2. Bloqueia Mensagens de Status/Stories (status@broadcast) + if v.Info.Chat.User == "status" { return } + // ---------------------------- + + rawMsg := v.Message + if rawMsg.GetDeviceSentMessage() != nil { + rawMsg = rawMsg.GetDeviceSentMessage().GetMessage() } - // Tenta pegar o texto (conversa simples ou estendida) - txt := "" - if v.Message.Conversation != nil { - txt = *v.Message.Conversation - } else if v.Message.ExtendedTextMessage != nil { - txt = *v.Message.ExtendedTextMessage.Text + var txt, msgType, fileName string + var fileData []byte + var err error + ctx := context.Background() + + if m := rawMsg.GetConversation(); m != "" { + txt, msgType = m, "Texto" + } else if m := rawMsg.GetExtendedTextMessage(); m != nil { + txt, msgType = m.GetText(), "Texto" + } else if m := rawMsg.GetImageMessage(); m != nil { + msgType, txt, fileName = "Imagem", "Legenda: "+m.GetCaption(), "imagem.jpg" + fileData, err = client.Download(ctx, m) + } else if m := rawMsg.GetStickerMessage(); m != nil { + msgType, txt, fileName = "Figurinha", "[Figurinha]", "sticker.webp" + fileData, err = client.Download(ctx, m) + } else if m := rawMsg.GetAudioMessage(); m != nil { + msgType, txt, fileName = "Áudio", fmt.Sprintf("Duração: %ds", m.GetSeconds()), "audio.ogg" + fileData, err = client.Download(ctx, m) + } else if m := rawMsg.GetVideoMessage(); m != nil { + msgType, txt, fileName = "Vídeo", "Legenda: "+m.GetCaption(), "video.mp4" + fileData, err = client.Download(ctx, m) } - if txt == "" { - return - } + if msgType == "" { return } + if err != nil { fmt.Fprintln(os.Stderr, "Erro download:", err) } name := findName(v.Info.Sender.User, contacts, v.Info.PushName) - fmt.Printf("[%s] %s -> Enviando email...\n", time.Now().Format("15:04:05"), name) - - // Formata corpo do email - body := fmt.Sprintf("De: %s\nData: [%s %s]\n\nMensagem:\n%s", - name, - portugueseWeekday(v.Info.Timestamp.Weekday()), - v.Info.Timestamp.Format("02/01 15:04"), - txt, - ) - - subject := fmt.Sprintf("WhatsApp de %s", name) - sendEmailRaw(cfg, subject, body) + body := fmt.Sprintf("De: %s\nData: %s %s\nTipo: %s\n\n%s", + name, portugueseWeekday(v.Info.Timestamp.Weekday()), + v.Info.Timestamp.Format("02/01 15:04"), msgType, txt) + + sendEmailWithAttachment(cfg, "Zap de "+name+" ["+msgType+"]", body, fileName, fileData) }) connectClient(client) - fmt.Println("daemon rodando (modo imediato). Ctrl+C para parar.") - + fmt.Println("Daemon rodando (Privado apenas + Figurinha). Ctrl+C para parar.") + quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit @@ -251,62 +244,34 @@ func runDaemon() { func runSend() { contacts := loadAbook() var lines []string - for _, c := range contacts { - lines = append(lines, fmt.Sprintf("%s\t%s", c.Name, c.Mobile)) - } + 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 out, err := cmd.Output() - if err != nil { - return - } + if err != nil { return } + parts := strings.SplitN(strings.TrimSpace(string(out)), "\t", 2) - if len(parts) < 2 { - return - } - chosen := Contact{Name: parts[0], Mobile: parts[1]} - phone := cleanNumber(chosen.Mobile) - - jid, err := types.ParseJID(phone + "@s.whatsapp.net") - if err != nil { - fmt.Fprintln(os.Stderr, "numero invalido:", phone) - os.Exit(1) - } + jid, _ := types.ParseJID(cleanNumber(parts[1]) + "@s.whatsapp.net") client := newClient() connectClient(client) defer client.Disconnect() - fmt.Printf("para: %s (%s)\n", chosen.Name, chosen.Mobile) + fmt.Printf("Para: %s\n", parts[0]) scanner := bufio.NewScanner(os.Stdin) for { fmt.Print("> ") - if !scanner.Scan() { - break - } - text := strings.TrimSpace(scanner.Text()) - if text == "" { - break - } - client.SendMessage(context.Background(), jid, &waE2E.Message{ - Conversation: proto.String(text), - }) + if !scanner.Scan() { break } + t := strings.TrimSpace(scanner.Text()) + if t == "" { break } + client.SendMessage(context.Background(), jid, &waE2E.Message{Conversation: proto.String(t)}) } } func main() { - if len(os.Args) < 2 { - fmt.Println("uso: zap <daemon|send>") - os.Exit(1) - } + if len(os.Args) < 2 { fmt.Println("uso: zap <daemon|send>"); return } switch os.Args[1] { - case "daemon": - runDaemon() - case "send": - runSend() - default: - fmt.Println("uso: zap <daemon|send>") - os.Exit(1) + case "daemon": runDaemon() + case "send": runSend() } }