Before we can start using Whatsapp API, be sure to opt-in to WhatsApp Multi-device Beta. This is located on the top-right menu (you should have the Linked devices option)

join multi-device beta

Getting started with WhatsApp

whatsmeow is a Go library for the WhatsApp web multidevice API. To install, run

go get -u go.mau.fi/whatsmeow

The full client example

package main

import (
	"context"
	"fmt"
	"os"

	_ "github.com/mattn/go-sqlite3"
	"github.com/mdp/qrterminal"
	"go.mau.fi/whatsmeow"
	"go.mau.fi/whatsmeow/store/sqlstore"
	waLog "go.mau.fi/whatsmeow/util/log"
)

func WAConnect() (*whatsmeow.Client, error) {
	container, err := sqlstore.New("sqlite3", "file:wapp.db?_foreign_keys=on", waLog.Noop)
	if err != nil {
		return nil, err
	}
	deviceStore, err := container.GetFirstDevice()
	if err != nil {
		panic(err)
	}
	client := whatsmeow.NewClient(deviceStore, waLog.Noop)
	if client.Store.ID == nil {
		// No ID stored, new login
		qrChan, _ := client.GetQRChannel(context.Background())
		err = client.Connect()
		if err != nil {
			return nil, err
		}
		for evt := range qrChan {
			if evt.Event == "code" {
				qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
			} else {
				fmt.Println("Login event:", evt.Event)
			}
		}
	} else {
		err := client.Connect()
		if err != nil {
			return nil, err
		}
	}
	return client, nil
}

whatsmeow requires a store interface to save/delete state information. If you ever used the WhatsApp web interface, in the first initial step, you need to scan QrCode. If client.Store.ID == nil, we don’t have a token, so the client will return this via QRChannel.

The qrterminal library, prints a full QRCode in the terminal so it’s convenient for a first-time setup. After we scan the QrCode and accept it on a mobile device, the token will be stored in SQLite databases (file wapp.db).

scan code

We are now ready to send WhatsApp messages :)

No API? Scrape…

I’m not surprised that kindergarten does not have an API so I default to scraping web content. For this, I really like goquery library which is a jquery-like element finder.

go get -u github.com/PuerkitoBio/goquery

So, for every day, I append to a slice of dayMenu and get the current day with time.Now().Weekday(). In my slice, it would be dayMenu[weekday - 1]

doc, err := goquery.NewDocument(uri)
if err != nil {
	fmt.Println(err)
	return
}

var dayMenu []Day
doc.Find("#right_content").Children().Each(func(i int, s *goquery.Selection) {
	txt := strings.TrimSpace(s.Text())
	if strings.Contains(txt, "JELOVNIK") {
		dayMenu = parseMenu(txt)
    }
})

To build a text message that we can send, I use text/template. An example of it looks like this

func buildMessage(day *Day, nextDay *Day) string {
	t := struct {
		Day  *Day
		Next *Day
	}{day, nextDay}

	tt, err := template.New("txt").Parse(templateDay)
	if err != nil {
		panic(err)
	}
	var buff bytes.Buffer
	tt.Execute(&buff, t)
	return strings.TrimSpace(buff.String())
}

Connect the dots

So far we have:

  • WhatsApp client set
  • Food menu scraped
  • Transferring our model to text message

Now we are ready to send message.

wac, err := WAConnect()
if err != nil {
	fmt.Println(err)
	return
}
defer wac.Disconnect()

_, err = wac.SendMessage(types.JID{
	User:   "3859XXXXXXXX",
	Server: types.DefaultUserServer,
}, "", &waProto.Message{
	Conversation: proto.String(msg),
})

The result

msg

I pushed the binary to my local server, set the crontab to run daily from Mon-Fri at 7:45 am

# Cron to run at 7:45 am from Mon-Fri
45 07 * * 1-5