whatsapp-automations/internal/client.go

242 lines
5.9 KiB
Go

package internal
import (
"context"
"encoding/json"
"fmt"
"math/rand/v2"
"os"
"slices"
"strconv"
"time"
waautoresponder "git.sangeeth.dev/wa-autoresponder"
"github.com/mdp/qrterminal"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"google.golang.org/protobuf/proto"
)
const autoResponseTimeMapJsonFileName = "auto-response-time-map.json"
const allowlistFileName = "allowlist.json"
type Client struct {
WAClient *whatsmeow.Client
message string
autoResponseTimeMap map[string]string
allowlist []string
}
func NewClient(waClient *whatsmeow.Client) *Client {
autoResponseTimeMap := map[string]string{}
allowlist := []string{}
fileInfo, _ := os.Stat(autoResponseTimeMapJsonFileName)
if fileInfo != nil {
bytes, err := os.ReadFile(autoResponseTimeMapJsonFileName)
if err != nil {
panic(fmt.Errorf("error reading %s: %w", autoResponseTimeMapJsonFileName, err))
}
err = json.Unmarshal(bytes, &autoResponseTimeMap)
if err != nil {
panic(err)
}
}
fileInfo, _ = os.Stat(allowlistFileName)
if fileInfo != nil {
bytes, err := os.ReadFile(allowlistFileName)
if err != nil {
panic(fmt.Errorf("error reading %s: %w", allowlistFileName, err))
}
err = json.Unmarshal(bytes, &allowlist)
if err != nil {
panic(err)
}
}
return &Client{
WAClient: waClient,
message: waautoresponder.AutoResponderMessage,
autoResponseTimeMap: autoResponseTimeMap,
allowlist: allowlist,
}
}
func (client *Client) Register() {
client.WAClient.AddEventHandler(client.eventHandler)
}
func (myClient *Client) Connect() {
client := myClient.WAClient
if client.Store.ID == nil {
// No ID stored, new login
qrChan, _ := client.GetQRChannel(context.Background())
err := client.Connect()
if err != nil {
panic(err)
}
for evt := range qrChan {
if evt.Event == "code" {
// Render the QR code here
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
// or just manually `echo 2@... | qrencode -t ansiutf8` in a terminal
fmt.Println("QR code:", evt.Code)
} else {
fmt.Println("Login event:", evt.Event)
}
}
} else {
// Already logged in, just connect
err := client.Connect()
if err != nil {
panic(err)
}
}
}
func (client *Client) Disconnect() {
client.WAClient.Disconnect()
}
func (client *Client) hasAutoRespondedWithinSameDay(userId string) bool {
if rawLastResponseTime, exists := client.autoResponseTimeMap[userId]; exists {
parsedLastResponseTime, error := time.Parse(time.RFC3339, rawLastResponseTime)
if error != nil {
fmt.Fprintf(os.Stderr, "Map has time stored in invalid format, expected RFC3339. Raw value is %+v\n", rawLastResponseTime)
return false
}
// If we already responded today, not need to send the same spiel again
if AreSameDay(parsedLastResponseTime, time.Now()) {
fmt.Printf("Already responded to user %s, skipping\n", userId)
return true
}
}
return false
}
func (client *Client) updateAutoResponseTime(userId string) {
client.autoResponseTimeMap[userId] = time.Now().Format(time.RFC3339)
bytes, err := json.MarshalIndent(client.autoResponseTimeMap, "", " ")
if err != nil {
panic(err)
}
if err = os.WriteFile(autoResponseTimeMapJsonFileName, bytes, 0660); err != nil {
panic(err)
}
}
func (client *Client) eventHandler(evt interface{}) {
switch v := evt.(type) {
case *events.Message:
chatUserId := v.Info.Chat.User
if v.Info.IsFromMe {
return
}
if !slices.Contains(client.allowlist, chatUserId) {
if v.Info.IsGroup {
return
}
// Ignore business chats
if v.Info.VerifiedName != nil {
return
}
}
if client.hasAutoRespondedWithinSameDay(chatUserId) {
fmt.Printf("Already responded to user %s, skipping\n", chatUserId)
return
}
if time.Now().Sub(v.Info.Timestamp).Minutes() >= 5 {
fmt.Printf("Message from %s older than 5 minutes, skipping\n", chatUserId)
return
}
time.Sleep(2 * time.Duration(rand.IntN(3)) * time.Second)
client.WAClient.SendChatPresence(v.Info.Chat, types.ChatPresenceComposing, types.ChatPresenceMediaText)
time.Sleep(2 * time.Duration(rand.IntN(3)) * time.Second)
msg := proto.String(client.message + "\n\nIgnore this random number: `" + strconv.FormatInt(time.Now().Unix(), 10) + "`")
imageResp, err := client.WAClient.Upload(context.Background(), waautoresponder.Bernie, whatsmeow.MediaImage)
if err == nil {
client.WAClient.SendMessage(
context.Background(),
v.Info.Chat,
&waE2E.Message{
ImageMessage: &waE2E.ImageMessage{
Caption: msg,
Mimetype: proto.String("image/jpeg"),
URL: &imageResp.URL,
DirectPath: &imageResp.DirectPath,
MediaKey: imageResp.MediaKey,
FileEncSHA256: imageResp.FileEncSHA256,
FileSHA256: imageResp.FileSHA256,
FileLength: &imageResp.FileLength,
},
})
} else {
client.WAClient.SendMessage(
context.Background(),
v.Info.Chat,
&waE2E.Message{
Conversation: msg,
},
)
}
client.WAClient.SendChatPresence(v.Info.Chat, types.ChatPresencePaused, types.ChatPresenceMediaText)
fmt.Printf("Sent autoresponder message to user %s\n", chatUserId)
client.updateAutoResponseTime(chatUserId)
}
}
func (client *Client) SendTextMessage(chatId string, message string) {
chatJID, err := types.ParseJID(chatId)
if err != nil {
fmt.Println("Error parsing JID:", err)
return
}
time.Sleep(2 * time.Duration(rand.IntN(3)) * time.Second)
client.WAClient.SendChatPresence(chatJID, types.ChatPresenceComposing, types.ChatPresenceMediaText)
time.Sleep(2 * time.Duration(rand.IntN(3)) * time.Second)
client.WAClient.SendMessage(
context.Background(),
chatJID,
&waE2E.Message{
Conversation: proto.String(message),
},
)
client.WAClient.SendChatPresence(chatJID, types.ChatPresencePaused, types.ChatPresenceMediaText)
}