A few days ago, my fiancée asked me if it's possible to create an application to read the number of messages sent on WhatsApp in a given day and input this data into a spreadsheet. She was also interested in chat history from the last few days. I wasn't entirely sure how much the Meta Graph API allowed for this, but I decided to find out and help her.
And so it began.
It turned out that Meta didn't provide extensive options for fetching messages, and for real-time synchronization, only webhooks could be used (which sounded good), but supporting them required business activity confirmation and a lot of paperwork.
I decided to look for alternatives and quickly stumbled upon the library whatsapp-web.js written in Node.js, which supports real-time tracking of sent and received messages and chat history browsing. The library works based on Puppeteer and simulates a WhatsApp session from a browser.
At first glance, the architecture for the project seemed relatively straightforward, but assembling all the parts took me a while. Node.js, whatsapp-web.js, Sequelize for communication with AWS RDS (I chose a PostgreSQL database), and GCP Sheets SDK for updating the spreadsheet. I used a service account for GCP authentication and Access Key ID and Access Token for AWS authentication.
The application's entry point is:
import whatsapp from 'whatsapp-web.js';
import {syncMessage} from "../db.mjs"
import whatsappConfig from "../whatsapp.config.mjs"
const whatsappClient = new whatsapp.Client(whatsappConfig)
whatsappClient.on('message', message => syncMessage(message))
whatsappClient.on('message_create', message => syncMessage(message))
whatsappClient.initialize()
Listen for new sent or received messages and save them to the database.
At the beginning, there were several questions that I was aware of and wanted to find answers to:
FROM node:18
RUN apt-get update
# https://wwebjs.dev/guide/#installation-on-no-gui-systems
RUN apt install -y gconf-service libgbm-dev libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
# https://stackoverflow.com/a/25873952
RUN install -d -m 0755 -o 1000 -g 1000 /app
USER 1000
WORKDIR /app
COPY package.json .
COPY package-lock.json .
COPY /src ./src
COPY /ssl ./ssl
RUN npm install
CMD ["node", "src/cli/whatsapp.listen.mjs"]
aws ecr get-login-password --region eu-central-1 | docker login --username AWS --password-stdin <ACCOUNTID>.dkr.ecr.eu-central-1.amazonaws.com
After an initial attempt with direct interaction with pg, I decided to use the abstraction provided by the sequelize library.
db.config.mjs
:
import fs from "fs"
import {configDotenv} from "dotenv";
configDotenv()
import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
const secretsManagerClient = new SecretsManagerClient({
region: process.env.AWS_DEFAULT_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
}
})
let response;
try {
response = await secretsManagerClient.send(
new GetSecretValueCommand({
SecretId: process.env.AWS_RDS_SECRET_NAME,
VersionStage: "AWSCURRENT",
})
)
} catch (error) {
throw error
}
const secret = JSON.parse(response.SecretString)
export default {
host: process.env.AWS_RDS_HOST,
port: 5432,
user: secret['username'],
password: secret['password'],
database: process.env.AWS_RDS_DATABASE,
ssl: {
rejectUnauthorized: false,
ca: fs.readFileSync("./ssl/eu-central-1-bundle.pem").toString(),
}
}
db.mjs
:
import {Sequelize} from 'sequelize'
import config from "./db.config.mjs"
const sequelize = new Sequelize(
config.database,
config.user,
config.password,
{
dialect: "postgres",
host: config.host,
port: config.port,
dialectOptions: {
ssl: config.ssl,
}
}
)
Google Cloud Platform Service Account JSON.
I also had to grant access to the email from the JSON file to a spreadsheet as <service-account-name>@<project-name>.iam.gserviceaccount.com
.
The initial version I used (27.08.2023) was this:
https://gist.github.com/piotr-rusin/cfa37319513c5b343227ceae59afc5dc
Several more challenges arose during work:
I downloaded and configured AWS CLI on the control plane:
aws configure
And added a secret of type docker-registry:
kubectl create secret docker-registry aws-ecr \
--docker-server=https://<ACCOUNTID>.dkr.ecr.eu-central-1.amazonaws.com \
--docker-username=AWS \
--docker-password=$(aws ecr get-login-password)
And passed it in the deployment's YAML:
imagePullSecrets:
- name: "aws-ecr"
I used hostPath:
volumeMounts:
- name: wwebjs-auth
mountPath: /app/.wwebjs_auth
- name: wwebjs-cache
mountPath: /app/.wwebjs_cache
volumes:
- name: wwebjs-auth
hostPath:
path: /home/pokemon/whataspp-metrics/.wwebjs_auth
type: DirectoryOrCreate
- name: wwebjs-cache
hostPath:
path: /home/pokemon/whataspp-metrics/.wwebjs_cache
type: DirectoryOrCreate
After overcoming few difficulties, the deployment was successful, and the first application started on my cluster.
pikachu
The second part of the application remains to be written - synchronizing data from PostgreSQL into Google Sheets.
I also want to become familiar with Helm and convert the "raw" YAML deployment to a dedicated Helm chart, which I will keep in a GitHub repository. After a push, I'd like to automatically publish it to the cluster using ArgoCD.
NixOS also sounds tempting as a replacement for the current Dockerfile.