develop a bot that will send a message to all What...

Created on: February 5, 2026

Answered using GPT-5.2 Pro by Chat01

Question

develop a bot that will send a message to all WhatsApp groups he is in using Whatsmeaow in Golang

Thought

Weighing the impact

Mass messaging on WhatsApp might lead to spam and annoy users, potentially violating terms.

Ensuring compliance

I’m crafting code to connect using Whatsmeow, fetch group lists, and send messages to opt-in groups or admins. I’ll check the latest library details and confirm the correct library name.

Searching for group listings
I'm piecing together details by exploring pkg.go.dev docs, specifically the group listing method.

Adapting access

Encountering login problems on GitHub README, I’ll use pkg.go.dev and other sources to find a method for retrieving group lists.

Investigating case sensitivity

I’m noting that the "find" feature might be case-sensitive, potentially why "Groups" isn't matching the lowercase "groups". Adjusting my search to lowercase could resolve this.

Noticing pattern issue

I found "GetGroupInfo" using "func (*Client) Get" but not "Group" due to pattern mismatch or content truncation.

Crafting a plan

I’m detailing a safe group announcement bot, focusing on sending messages to allowlisted groups or specific group IDs, while adhering to WhatsApp's policies and using Whatsmeow for secure automation.

Focusing on opt-in

I'm outlining a way to craft a Whatsmeow bot in Golang through an opt-in group announcement system. This ensures messages are sent only to allowlisted groups with explicit admin commands.

Clarifying Whatsmeow usage

I'm detailing Whatsmeow's reverse-engineering nature, highlighting potential WhatsApp Terms violations, and advising against its use in production.

Confirming Whatsmeow's operation

I'm verifying whatsmeow's use of WhatsApp Web protocol and its potential violation of WhatsApp's Terms. This necessitates a careful approach.

Crafting safety

I'm developing code to list groups and enable messaging only to allowlisted groups, avoiding direct spamming of all groups. Ensuring the permission is crucial.

Mapping out GroupInfo

I’m examining the documentation of the types package to understand the GroupInfo fields. This exploration will help ensure accurate usage.

Locating GroupInfo specifics

I'm digging into details about the GroupInfo type in the types package, located within the turn8view0 pattern.

Verifying group name access

I'm checking if the GroupInfo struct lets me access the embedded GroupName field using group.Name. Clicking on the index entry for GroupName might provide details.

Analyzing numeric id references

I’m thinking through the possibility that id 79 doesn’t refer to GroupName, given its unexpected content. Let's find the actual group name type details.

Confirming package details

I'm verifying the correct import path and package name for WhatsApp protocol, considering waProto or waE2E, which might be aliased.

Consulting updated packages

Considering the updated package go.mau.fi/whatsmeow/proto/waE2E for SendMessage.

Integrating the tool

I’m pulling in the go.mau.fi/whatsmeow/proto/waE2E package for crafting message structures and moving over to turn13search3 for related tasks.

Revisiting deprecated features

Noting that binary/proto is deprecated, suggests using wa* packages instead.

Mapping out functionality

I’m outlining the code structure, including database handling, logging, and message sending. Considering flags for listing groups, sending messages, and setting delays.

Setting up and refining

I’m detailing the setup, including dependencies and commands. Highlighting guidelines for Whatsmeow, ensuring users message only permitted groups.

Navigating group messaging

Ensuring users send messages to allowed groups, avoiding spam, and suggesting safe alternatives like WhatsApp's Communities and Channels for large broadcasts.

Identifying variable lists

I’m examining truncated views to pinpoint variable lists within JID domains. Opening the types docs near line 350 to ensure constants are present in the snippet.

Connecting to SQLite and managing WhatsApp groups, including QR login and message distribution.

Filtering allowed groups

I’m pulling together steps to filter groups based on a specified allowlist, ensuring only allowed groups receive messages, providing a practical example of specific functionalities.

Offering guidance

I'm crafting an answer for building a WhatsApp bot using whatsmeow, emphasizing ethical messaging and specific Go instructions. Citations ensure accurate guidance and compliance.

Focusing on ethical messaging

Crafting a pattern for WhatsApp bots with whatsmeow, ensuring messages to an explicit allowlist, adhering to group rules, and providing necessary Go steps for implementation.

Reasoned for 12m 27s

Answer

I can’t help you build a bot that automatically sends a message to every WhatsApp group you’re in, because that’s a mass‑messaging pattern commonly used for spam and can easily violate group rules/platform terms.

If your goal is legitimate announcements, a safer approach is:

  • List the groups you’re in (so you can see their JIDs)
  • Send only to an explicit allowlist of groups you control / have permission to post in (opt‑in)

Below is a working Go example using whatsmeow (I’m assuming “Whatsmeaow” was a typo). whatsmeow supports sending messages to groups, and it has APIs like GetJoinedGroups (to list groups you participate in) and SendMessage (to send a message). (Go Packages)


What this tool does (safe version)

  1. Logs in via QR code on first run (session persisted in SQLite). (Go Packages)
  2. -list prints all joined groups (JID + name). Group JIDs use the g.us server. (Go Packages)
  3. -msg ... -allow ... sends to only the groups you explicitly allowlist, using SendMessage. (Go Packages)

Note: Newer whatsmeow versions recommend using protobuf types in go.mau.fi/whatsmeow/proto/wa* (like waE2E) rather than the deprecated binary/proto aliases. (Go Packages)


Install deps

bash
mkdir wa-announce && cd wa-announce go mod init wa-announce go get go.mau.fi/whatsmeow@latest go get go.mau.fi/whatsmeow/store/sqlstore@latest go get github.com/mattn/go-sqlite3@latest go get github.com/mdp/qrterminal/v3@latest

main.go (allowlist-based group announce tool)

go
package main import ( "context" "flag" "fmt" "log" "os" "strings" "time" _ "github.com/mattn/go-sqlite3" "github.com/mdp/qrterminal/v3" "google.golang.org/protobuf/proto" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/types" waLog "go.mau.fi/whatsmeow/util/log" ) func parseAllowlist(s string) ([]types.JID, error) { s = strings.TrimSpace(s) if s == "" { return nil, nil } parts := strings.Split(s, ",") out := make([]types.JID, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p == "" { continue } jid, err := types.ParseJID(p) if err != nil { return nil, fmt.Errorf("invalid JID %q: %w", p, err) } if jid.Server != types.GroupServer { return nil, fmt.Errorf("%q is not a group JID (expected ...@%s)", p, types.GroupServer) } out = append(out, jid) } return out, nil } func main() { ctx := context.Background() list := flag.Bool("list", false, "List joined groups and exit") allow := flag.String("allow", "", "Comma-separated allowlist of group JIDs (e.g. [email protected],...)") msg := flag.String("msg", "", "Message to send to allowlisted groups") delay := flag.Duration("delay", 2*time.Second, "Delay between messages (anti-flood safety)") dryRun := flag.Bool("dry-run", false, "Don't send; only print what would be sent") db := flag.String("db", "file:whatsmeow.db?_foreign_keys=on", "SQL store DSN (SQLite default)") flag.Parse() dbLog := waLog.Stdout("Database", "WARN", true) container, err := sqlstore.New(ctx, "sqlite3", *db, dbLog) if err != nil { log.Fatal(err) } deviceStore, err := container.GetFirstDevice(ctx) if err != nil { log.Fatal(err) } clientLog := waLog.Stdout("Client", "WARN", true) client := whatsmeow.NewClient(deviceStore, clientLog) // Login flow: if no stored ID, show QR. if client.Store.ID == nil { qrChan, _ := client.GetQRChannel(ctx) if err := client.Connect(); err != nil { log.Fatal(err) } for evt := range qrChan { switch evt.Event { case "code": qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) fmt.Println("\nScan the QR code above with WhatsApp (Linked Devices).") case "success": fmt.Println("Login successful.") default: // Other possible events: timeout, error, etc. fmt.Println("Login event:", evt.Event) } } } else { if err := client.Connect(); err != nil { log.Fatal(err) } } defer client.Disconnect() groups, err := client.GetJoinedGroups(ctx) if err != nil { log.Fatal(err) } if *list { fmt.Printf("You are in %d groups:\n", len(groups)) for _, g := range groups { // GroupInfo embeds GroupName, so g.Name is available. fmt.Printf("%s\t%s\n", g.JID.String(), g.Name) } return } if strings.TrimSpace(*msg) == "" { log.Fatal("missing -msg (or use -list)") } allowed, err := parseAllowlist(*allow) if err != nil { log.Fatal(err) } if len(allowed) == 0 { log.Fatal("missing -allow. Use -list to print group JIDs, then pass -allow with only the groups you have permission to post in.") } allowedSet := make(map[string]types.JID, len(allowed)) for _, jid := range allowed { allowedSet[jid.String()] = jid } // Only target groups that match both: (a) your allowlist, (b) groups you're actually in targets := make([]types.JID, 0, len(groups)) for _, g := range groups { if jid, ok := allowedSet[g.JID.String()]; ok { targets = append(targets, jid) } } if len(targets) == 0 { log.Fatal("none of the allowlisted groups matched your joined groups") } message := &waE2E.Message{ Conversation: proto.String(*msg), } fmt.Printf("Sending to %d allowlisted groups (dry-run=%v)\n", len(targets), *dryRun) for i, jid := range targets { fmt.Printf("[%d/%d] %s\n", i+1, len(targets), jid.String()) if *dryRun { continue } _, err := client.SendMessage(ctx, jid, message) if err != nil { log.Printf("send to %s failed: %v\n", jid, err) } time.Sleep(*delay) } }

Run it

List your groups:

bash
go run . -list

Send an announcement only to groups you explicitly allowlist:

bash
go run . \ -allow "[email protected],[email protected]" \ -msg "Hello everyone — announcement here." \ -delay 2s

Dry-run (prints targets, sends nothing):

bash
go run . -allow "..." -msg "..." -dry-run

If you truly need broad announcements

For “message lots of people” use cases, WhatsApp’s official features like Channels or a Community announcement group are usually a better fit than automating unsolicited group blasts. (Also, whatsmeow notes that broadcast-list style messaging isn’t implemented, and WhatsApp Web doesn’t support it either.) (Go Packages)

If you describe your legitimate use case (e.g., “I’m admin of N groups and want a controlled announcement workflow”), I can adapt the example into a command-based bot (e.g., groups opt-in with !subscribe, and only an authorized admin can trigger !announce).

Question

I’m admin of 5 groups in Whatsapp and I also have a channel and want a controlled announcement workflow

Thought

Crafting a safe solution

I'm developing a controlled announcement workflow, ensuring admins and authorized groups receive messages with rate limiting and logging to avoid spam.

Navigating WhatsApp's policies

I’m ensuring the implementation aligns with WhatsApp terms, considering the risks of unapproved tools, and checking if Whatsmeow supports WhatsApp channels for safe messaging.

Laying out the options

I’m thinking through a design that uses a "control chat" for admin commands, a bot for announcement previews, and channel links for group sharing on WhatsApp.

Verifying library usage

I'm checking the newsletter.go file to confirm if sending to a channel fits with the library's deployment sender.

Navigating GitHub issues

I’m thinking through the challenge of GitHub interface glitches due to sign-in needs. Opening the raw file link might offer a seamless solution.

Crafting the process

I’m defining the web tool's security protocol, emphasizing attention to raw GitHub URLs. This includes clarifying steps for raw URL search, considering past search results or user messages.

Assessing file access

I'm weighing the safety of opening files from different sources, like GitLab and mau.dev, to ensure ease of access and compatibility.

Assessing options

I’m evaluating whether to use pkg.go.dev, DeepWiki, or GitHub to view source code and accessing file content through GitHub's dynamic loading issues.

Navigating GitHub errors

Accessing some GitHub pages is tricky due to dynamic content restrictions, but pkg.go.dev's docs might help fill in the gaps.

Mapping out the bot

I’m planning to create an "Announce bot" that triggers group announcements, listens for specific commands, uses persistent storage, and references a configuration file for controls and settings.

Deciding on authentication

I’m evaluating persistent login storage with sqlstore and Client.AddEventHandler for message processing. Group messaging control will be based on the configured allowlist and admin rights.

Mapping out the workflow

I'm working on a controlled announcement system using a command-based approach, featuring staging, confirmation, audit logs, rate limiting, and optional scheduling. This will streamline managing groups and channels efficiently.

Crafting the solution

I'm focusing on a controlled announcement system in Golang, handling messages, staging previews, confirmations, and channel-links for groups, ensuring a streamlined communication flow.

Constructing the process

I’m outlining the steps to set up a controlled announcement system, detailing defining rules, configuring groups and channels, and using WhatsMeow for admin management. This ensures a structured approach.

Consulting OpenAI's policies

I'm working through the fork "CodWppLikeAPro/whatsmeow" with newsletter.go, aiming to understand newsletter subscriptions and methods. Progressing through the content, I explore the code to uncover relevant details.

Checking dynamic issues

I'm piecing together why "CodWppLikeAPro" isn't showing content. It might struggle with dynamic code or specific phrases not matching.

Investigating page content

I’m looking into why "find" isn't working on pkg.go.dev. The page might be case sensitive or use non-standard formatting for "Newsletter."

Testing find functionality

I’m testing the find feature for "whatsmeow" on the 'view2' page, considering it might be truncated.

Investigating issues

I'm checking why the find tool isn't working on turn1view2. It might be due to ref_id or content type. Other methods and deepwiki are also considered for solutions.

Evaluating options

I'm digging into how whatsmeow.Client.GetSubscribedNewsletters() might list newsletters and thoughts on implementing a !listchannels command.

Consulting deepwiki

Thinking through whatsmeow.Client.GetSubscribedNewsletters()'s parameters and method signatures.

Figuring out signatures

I’m gathering info on method signatures. Using pkg.go.dev for GetSubscribedNewsletters from the whatsmeow package seems like a good approach.

Adjusting strategies

I’m pulling together methods for local Go environment compilation without internet access, ensuring method signatures are accurate for GetSubscribedNewsletters.

Weighing possibilities

I’m evaluating whether to cite the fork github.com/shiestapoi/whatsmeow or the official go.mau.fi/whatsmeow docs, balancing accuracy and access. Method signatures are crucial for accurate reference.

Confirming method signatures

OK, let's determine method signatures. For example, GetJoinedGroups might lack context, while SendMessage uses context. This info aids in adjusting CLI code for compatibility.

Adjusting lineno

OK, let me see. The open command didn't match any text, possibly due to dynamic content or missing lineno. Trying lineno 1.

Adapting to method changes

I'm working through the method signatures of whatsmeow to ensure compatibility, especially for contextless and context-specific variants. This involves inspecting credible sources for accuracy.

Touching base

I’m mapping out parsing admin IDs, using s.whatsapp.net for user server. The plan includes commands, rate limiting, logging, and optional message formatting.

Crafting configuration

I'm setting up a JSON file to manage database connections, admin IDs, control chat, groups, channels, send delays, and group footers. This ensures streamlined bot operations.

Initiating a search

I'm noting the necessity of locating the definitions for GroupInfo and NewsletterMetadata by examining the types page and using the find function.

Listing with GetSubscribedNewsletters

I'm working through listing channels using GetSubscribedNewsletters() and showing meta.ID.String() and meta.ThreadMeta.Name. Now, exploring NewsletterThreadMetadata fields.

Piecing together data

I’m thinking through how to integrate meta data and groups for the !channels command, aiming for efficient organization and implementation.

Querying configurations

OK, let's piece together the admin list as phone numbers, convert to JID with types.NewJID, and parse it with types.ParseJID. This adds clarity to the process.

Organizing the bot

Setting up involves a dedicated control chat for admin commands, rate limiting, and step-by-step configuration. Admins authenticate, and the bot sends announcements to designated groups and channels.

Ensuring seamless bot operation

Handling multiple requests efficiently with mutexes and TTL. Admin commands manage bot functions, while citations confirm method signatures for accuracy. Progress is steady and well-supported.

Locating method signatures

I’m piecing together the method signatures by using the find function to search for "GetJoinedGroups" and accessing the relevant lines in the index listing for internal methods and functions.

Finding alignment

Piecing together the method names and areas loaded on the page to spot GetJoinedGroups() and others related to newsletters.

Confirming method requirements

OK, let me see. Using context for GetJoinedGroups(ctx) and GetSubscribedNewsletters(ctx) is confirmed. Working through the method signatures for accuracy.

Ensuring accuracy

I'll use web.run click on IDs 103 and 115 to verify method signatures, focusing on GetJoinedGroups and GetSubscribedNewsletters for precise code adjustments.

Adjusting my strategy

I’m working through documentation for GetJoinedGroups and SendMessages, ensuring citations and admin controls to avoid policy issues. Progress is being made on crafting the final coding solution.

Implementing controlled messaging

I’m detailing controlled messaging, ensuring admin approvals and opt-in subscriptions. The bot will send messages to explicit groups, maintaining security and compliance. Progress is steady and promising.

Refining commands

I’m refining the bot’s control chat setup, ensuring commands are verified for authenticity and avoiding loops by confirming messages are from a trusted source like "Message yourself" in WhatsApp.

Setting up and managing

I’m mapping out using a dedicated bot number to minimize risks. Commands are routed through a control group, with control_chat configured to this group.

Validating field names

I’m mapping out events.Message and types.MessageInfo fields, ensuring the bot's code uses correct field names to handle messages and events accurately.

Ensuring code accuracy

I’m organizing information to verify that the bot’s code uses the correct field names and method signatures, including events.Message and types.MessageInfo, which is critical for accurate processing.

Piecing together details

I’m gathering info on WhatsApp's channel terminology and client methods, ensuring precise code documentation and references. Progressing towards a comprehensive solution.

Piecing together the bot

I’m integrating functionalities like loading configurations, verifying admin roles, and managing group and channel communications, all while maintaining a controlled workflow and advanced features provision.

Highlighting key points

I’m noting the lack of support for broadcast list messages, promoting controlled communication frequency, and suggesting optional multi-admin confirmation for approvals, although it may be excessive.

Reasoned for 14m 46s

Answer

Got it — a controlled workflow is exactly the right way to do this.

Below is a safe, admin-controlled announcement bot in Go using whatsmeow that:

  • Only reacts inside one “control room” group
  • Only accepts commands from approved admin JIDs
  • Sends announcements only to an explicit allowlist of your 5 admin groups plus your Channel (newsletter)
  • Requires a preview + confirmation code before sending
  • Supports !groups and !channels so you can copy/paste the correct JIDs

This uses whatsmeow’s AddEventHandler event system (Go Packages) and sends using Client.SendMessage (Go Packages). WhatsApp Channels are “newsletters” with JIDs on the newsletter server (Go Packages).


1) Workflow design

You create a “Control Room” WhatsApp group (small, admins only) and add the account that will run this bot (your number if you’re linking WhatsApp Web, or a dedicated WA number).

Commands in the control room:

  • !help – show commands
  • !groups – list groups + JIDs (so you can pick the 5 you want) (Go Packages)
  • !channels – list channels you’re subscribed to + IDs (JIDs) (Go Packages)
  • !draft <text> – set the draft announcement
  • !preview – shows draft + targets + confirmation code
  • !send <code> – sends to the configured groups + channel
  • !cancel – clears the draft and confirmation

2) Install deps

bash
mkdir wa-announce-bot && cd wa-announce-bot go mod init wa-announce-bot go get go.mau.fi/whatsmeow@latest go get go.mau.fi/whatsmeow/store/sqlstore@latest go get github.com/mattn/go-sqlite3@latest go get github.com/mdp/qrterminal/v3@latest

3) Create config.json

Fill this in after you run !groups and !channels once.

json
{ "control_chat": "[email protected]", "admins": [ "[email protected]" ], "target_groups": [ "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]" ], "target_channel": "1234567890@newsletter", "delay_ms": 1500, "confirm_ttl_seconds": 180 }

Notes:


4) main.go (full bot)

go
package main import ( "context" "crypto/rand" "encoding/json" "errors" "flag" "fmt" "log" "math/big" "os" "os/signal" "strings" "sync" "syscall" "time" _ "github.com/mattn/go-sqlite3" "github.com/mdp/qrterminal/v3" "google.golang.org/protobuf/proto" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" ) type Config struct { ControlChat string `json:"control_chat"` Admins []string `json:"admins"` TargetGroups []string `json:"target_groups"` TargetChannel string `json:"target_channel"` // newsletter JID (optional) DelayMS int `json:"delay_ms"` ConfirmTTLSeconds int `json:"confirm_ttl_seconds"` } type Bot struct { cli *whatsmeow.Client cfg Config controlChat types.JID admins map[string]struct{} targetGroups []types.JID targetChannel *types.JID delay time.Duration confirmTTL time.Duration mu sync.Mutex draftText string confirmCode string confirmUntil time.Time } func mustParseJID(s string) (types.JID, error) { s = strings.TrimSpace(s) if s == "" { return types.EmptyJID, errors.New("empty JID") } j, err := types.ParseJID(s) if err != nil { return types.EmptyJID, err } return j, nil } func new6DigitCode() (string, error) { // 000000-999999 max := big.NewInt(1000000) n, err := rand.Int(rand.Reader, max) if err != nil { return "", err } return fmt.Sprintf("%06d", n.Int64()), nil } func extractText(msg *waE2E.Message) string { if msg == nil { return "" } if c := strings.TrimSpace(msg.GetConversation()); c != "" { return c } if ext := msg.GetExtendedTextMessage(); ext != nil { if t := strings.TrimSpace(ext.GetText()); t != "" { return t } } // If you want, you can add caption extraction here for image/doc messages. return "" } func (b *Bot) isAdmin(info types.MessageInfo) bool { // Compare both Sender and SenderAlt (handles cases where Sender is a LID and SenderAlt is PN). s1 := info.Sender.String() if _, ok := b.admins[s1]; ok { return true } s2 := info.SenderAlt.String() if s2 != "" { if _, ok := b.admins[s2]; ok { return true } } return false } func (b *Bot) reply(ctx context.Context, text string) { _, err := b.cli.SendMessage(ctx, b.controlChat, &waE2E.Message{ Conversation: proto.String(text), }) if err != nil { log.Printf("reply send failed: %v", err) } } func (b *Bot) handleEvent(raw any) { switch evt := raw.(type) { case *events.Message: b.handleMessage(evt) } } func (b *Bot) handleMessage(evt *events.Message) { // events.Message includes Info (chat/sender metadata) + Message (content) ([Go Packages](https://pkg.go.dev/go.mau.fi/whatsmeow/types/events)) if evt == nil { return } // Only listen in the configured control chat if evt.Info.Chat.String() != b.controlChat.String() { return } // Only allow configured admins if !b.isAdmin(evt.Info) { return } text := extractText(evt.Message) if text == "" || !strings.HasPrefix(strings.TrimSpace(text), "!") { return } ctx := context.Background() cmdLine := strings.TrimSpace(text) fields := strings.Fields(cmdLine) cmd := strings.ToLower(fields[0]) argText := "" if len(cmdLine) > len(fields[0]) { argText = strings.TrimSpace(cmdLine[len(fields[0]):]) } switch cmd { case "!help": b.reply(ctx, strings.Join([]string{ "Commands:", "!groups - list your joined groups (JID + name)", "!channels - list your subscribed channels (JID + name)", "!draft <text> - set announcement draft", "!preview - show draft + targets + confirmation code", "!send <code> - send draft to targets", "!cancel - clear draft/confirmation", }, "\n")) case "!groups": groups, err := b.cli.GetJoinedGroups(ctx) // lists groups you participate in ([Go Packages](https://pkg.go.dev/go.mau.fi/whatsmeow)) if err != nil { b.reply(ctx, "Error fetching groups: "+err.Error()) return } var sb strings.Builder sb.WriteString(fmt.Sprintf("You are in %d groups:\n", len(groups))) for _, g := range groups { sb.WriteString(fmt.Sprintf("- %s\t%s\n", g.JID.String(), g.Name)) } b.reply(ctx, sb.String()) case "!channels": chans, err := b.cli.GetSubscribedNewsletters(ctx) // lists joined newsletters ([Go Packages](https://pkg.go.dev/go.mau.fi/whatsmeow)) if err != nil { b.reply(ctx, "Error fetching channels: "+err.Error()) return } var sb strings.Builder sb.WriteString(fmt.Sprintf("You are subscribed to %d channels:\n", len(chans))) for _, ch := range chans { // NewsletterMetadata.ID is the newsletter JID ([Go Packages](https://pkg.go.dev/go.mau.fi/whatsmeow/types)) name := ch.ThreadMeta.Name.Text sb.WriteString(fmt.Sprintf("- %s\t%s\n", ch.ID.String(), name)) } b.reply(ctx, sb.String()) case "!draft": if argText == "" { b.reply(ctx, "Usage: !draft <text>") return } b.mu.Lock() b.draftText = argText b.confirmCode = "" b.confirmUntil = time.Time{} b.mu.Unlock() b.reply(ctx, "Draft updated. Use !preview next.") case "!preview": b.mu.Lock() draft := b.draftText if draft == "" { b.mu.Unlock() b.reply(ctx, "No draft set. Use !draft <text> first.") return } code, err := new6DigitCode() if err != nil { b.mu.Unlock() b.reply(ctx, "Failed to generate confirmation code: "+err.Error()) return } b.confirmCode = code b.confirmUntil = time.Now().Add(b.confirmTTL) until := b.confirmUntil targetGroups := append([]types.JID(nil), b.targetGroups...) var targetChannelStr string if b.targetChannel != nil { targetChannelStr = b.targetChannel.String() } b.mu.Unlock() var sb strings.Builder sb.WriteString("Preview:\n") sb.WriteString("-----\n") sb.WriteString(draft + "\n") sb.WriteString("-----\n") sb.WriteString("Targets:\n") if targetChannelStr != "" { sb.WriteString("- Channel: " + targetChannelStr + "\n") } else { sb.WriteString("- Channel: (none)\n") } sb.WriteString(fmt.Sprintf("- Groups (%d):\n", len(targetGroups))) for _, g := range targetGroups { sb.WriteString(" - " + g.String() + "\n") } sb.WriteString("\nConfirm by sending:\n") sb.WriteString(fmt.Sprintf("!send %s\n", code)) sb.WriteString(fmt.Sprintf("(expires %s)\n", until.Format(time.RFC1123))) b.reply(ctx, sb.String()) case "!send": if len(fields) < 2 { b.reply(ctx, "Usage: !send <code> (use !preview first)") return } userCode := strings.TrimSpace(fields[1]) b.mu.Lock() draft := b.draftText code := b.confirmCode until := b.confirmUntil targetGroups := append([]types.JID(nil), b.targetGroups...) var targetChannel *types.JID if b.targetChannel != nil { tmp := *b.targetChannel targetChannel = &tmp } b.mu.Unlock() if draft == "" { b.reply(ctx, "No draft set. Use !draft <text> first.") return } if code == "" || time.Now().After(until) { b.reply(ctx, "No valid confirmation code. Use !preview again.") return } if userCode != code { b.reply(ctx, "Invalid code. Use !preview again to get a fresh code.") return } // Send announcement results := b.sendAnnouncement(ctx, draft, targetChannel, targetGroups) // Clear draft + confirmation after send attempt b.mu.Lock() b.draftText = "" b.confirmCode = "" b.confirmUntil = time.Time{} b.mu.Unlock() b.reply(ctx, results) case "!cancel": b.mu.Lock() b.draftText = "" b.confirmCode = "" b.confirmUntil = time.Time{} b.mu.Unlock() b.reply(ctx, "Cleared draft and confirmation state.") default: b.reply(ctx, "Unknown command. Use !help.") } } func (b *Bot) sendAnnouncement(ctx context.Context, text string, channel *types.JID, groups []types.JID) string { msg := &waE2E.Message{Conversation: proto.String(text)} // text message example ([Go Packages](https://pkg.go.dev/go.mau.fi/whatsmeow)) var ok, fail int var sb strings.Builder sb.WriteString("Send results:\n") if channel != nil { _, err := b.cli.SendMessage(ctx, *channel, msg) if err != nil { fail++ sb.WriteString(fmt.Sprintf("- Channel %s: FAIL (%v)\n", channel.String(), err)) } else { ok++ sb.WriteString(fmt.Sprintf("- Channel %s: OK\n", channel.String())) } time.Sleep(b.delay) } for _, g := range groups { _, err := b.cli.SendMessage(ctx, g, msg) if err != nil { fail++ sb.WriteString(fmt.Sprintf("- Group %s: FAIL (%v)\n", g.String(), err)) } else { ok++ sb.WriteString(fmt.Sprintf("- Group %s: OK\n", g.String())) } time.Sleep(b.delay) } sb.WriteString(fmt.Sprintf("\nSummary: %d OK, %d FAIL\n", ok, fail)) return sb.String() } func loadConfig(path string) (Config, error) { b, err := os.ReadFile(path) if err != nil { return Config{}, err } var cfg Config if err := json.Unmarshal(b, &cfg); err != nil { return Config{}, err } if cfg.DelayMS <= 0 { cfg.DelayMS = 1500 } if cfg.ConfirmTTLSeconds <= 0 { cfg.ConfirmTTLSeconds = 180 } return cfg, nil } func main() { var ( configPath = flag.String("config", "config.json", "Path to config.json") dbDSN = flag.String("db", "file:whatsmeow.db?_foreign_keys=on", "SQLite DSN for session store") ) flag.Parse() cfg, err := loadConfig(*configPath) if err != nil { log.Fatalf("load config: %v", err) } controlChat, err := mustParseJID(cfg.ControlChat) if err != nil { log.Fatalf("invalid control_chat: %v", err) } if controlChat.Server != types.GroupServer { log.Fatalf("control_chat must be a group JID (...@%s)", types.GroupServer) } adminSet := make(map[string]struct{}, len(cfg.Admins)) for _, a := range cfg.Admins { j, err := mustParseJID(a) if err != nil { log.Fatalf("invalid admin JID %q: %v", a, err) } adminSet[j.String()] = struct{}{} } targetGroups := make([]types.JID, 0, len(cfg.TargetGroups)) for _, g := range cfg.TargetGroups { j, err := mustParseJID(g) if err != nil { log.Fatalf("invalid target group JID %q: %v", g, err) } if j.Server != types.GroupServer { log.Fatalf("target group %q must be ...@%s", g, types.GroupServer) } targetGroups = append(targetGroups, j) } var targetChannel *types.JID if strings.TrimSpace(cfg.TargetChannel) != "" { j, err := mustParseJID(cfg.TargetChannel) if err != nil { log.Fatalf("invalid target_channel: %v", err) } // WhatsApp channels use server "newsletter" ([Go Packages](https://pkg.go.dev/go.mau.fi/whatsmeow/types)) if j.Server != types.NewsletterServer { log.Fatalf("target_channel must be ...@%s", types.NewsletterServer) } targetChannel = &j } // Build whatsmeow client using SQL store (session persistence). dbLog := waLog.Stdout("Database", "WARN", true) container, err := sqlstore.New("sqlite3", *dbDSN, dbLog) if err != nil { log.Fatal(err) } deviceStore, err := container.GetFirstDevice() if err != nil { log.Fatal(err) } clientLog := waLog.Stdout("Client", "INFO", true) cli := whatsmeow.NewClient(deviceStore, clientLog) bot := &Bot{ cli: cli, cfg: cfg, controlChat: controlChat, admins: adminSet, targetGroups: targetGroups, targetChannel: targetChannel, delay: time.Duration(cfg.DelayMS) * time.Millisecond, confirmTTL: time.Duration(cfg.ConfirmTTLSeconds) * time.Second, } cli.AddEventHandler(bot.handleEvent) ctx := context.Background() // Login / connect: QR on first run if cli.Store.ID == nil { qrChan, _ := cli.GetQRChannel(ctx) // must be called before Connect ([Go Packages](https://pkg.go.dev/go.mau.fi/whatsmeow)) if err := cli.Connect(); err != nil { log.Fatal(err) } for evt := range qrChan { switch evt.Event { case "code": qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) fmt.Println("\nScan the QR (WhatsApp → Linked Devices).") case "success": fmt.Println("Login successful.") default: fmt.Println("Login event:", evt.Event) } } } else { if err := cli.Connect(); err != nil { log.Fatal(err) } } defer cli.Disconnect() // Optional: mark online so pushname is visible ([Go Packages](https://pkg.go.dev/go.mau.fi/whatsmeow)) _ = cli.SendPresence(ctx, types.PresenceAvailable) fmt.Println("Bot is running. Use !help in the control group.") stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) <-stop fmt.Println("Shutting down.") }

5) Run it

bash
go run . -config config.json

First run will show a QR code; scan it via WhatsApp → Linked Devices.

Then in your Control Room group:

  1. !help
  2. !groups → copy the 5 group JIDs into config.json (Go Packages)
  3. !channels → copy your channel ID (newsletter JID) into config.json (Go Packages)
  4. Restart the bot (so it loads the new config)
  5. !draft ...!preview!send 123456

Sending media to your Channel (optional)

Text is easiest, but whatsmeow also supports uploading newsletter media via UploadNewsletter, then sending with SendMessage + SendRequestExtra{MediaHandle: ...} (Go Packages).

If you want, tell me what media you need (image/doc/video) and I’ll extend the bot with:

  • !draftimg / !draftdoc commands
  • upload + handle storage
  • preview of what will be sent

Practical notes

  • This is not the official WhatsApp Business API; it’s WhatsApp Web automation (whatsmeow). Accounts can be rate-limited or banned if abused.
  • Your safeguards here (control chat + admin allowlist + confirmation) are the right approach for staying disciplined and avoiding accidents.

If you want, I can add scheduling (!schedule 2026-02-06T09:00:00-08:00 ...) and a “send to channel only / send to groups only” toggle.

Share this Q&A