matterbridge/vendor/go.mau.fi/whatsmeow/mdtest/main.go
2024-05-21 20:08:12 +03:00

1166 lines
31 KiB
Go

// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"bufio"
"context"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"mime"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"sync/atomic"
"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/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
)
var cli *whatsmeow.Client
var log waLog.Logger
var logLevel = "INFO"
var debugLogs = flag.Bool("debug", false, "Enable debug logs?")
var dbDialect = flag.String("db-dialect", "sqlite3", "Database dialect (sqlite3 or postgres)")
var dbAddress = flag.String("db-address", "file:mdtest.db?_foreign_keys=on", "Database address")
var requestFullSync = flag.Bool("request-full-sync", false, "Request full (1 year) history sync when logging in?")
var pairRejectChan = make(chan bool, 1)
func main() {
waBinary.IndentXML = true
flag.Parse()
if *debugLogs {
logLevel = "DEBUG"
}
if *requestFullSync {
store.DeviceProps.RequireFullSync = proto.Bool(true)
store.DeviceProps.HistorySyncConfig = &waProto.DeviceProps_HistorySyncConfig{
FullSyncDaysLimit: proto.Uint32(3650),
FullSyncSizeMbLimit: proto.Uint32(102400),
StorageQuotaMb: proto.Uint32(102400),
}
}
log = waLog.Stdout("Main", logLevel, true)
dbLog := waLog.Stdout("Database", logLevel, true)
storeContainer, err := sqlstore.New(*dbDialect, *dbAddress, dbLog)
if err != nil {
log.Errorf("Failed to connect to database: %v", err)
return
}
device, err := storeContainer.GetFirstDevice()
if err != nil {
log.Errorf("Failed to get device: %v", err)
return
}
cli = whatsmeow.NewClient(device, waLog.Stdout("Client", logLevel, true))
var isWaitingForPair atomic.Bool
cli.PrePairCallback = func(jid types.JID, platform, businessName string) bool {
isWaitingForPair.Store(true)
defer isWaitingForPair.Store(false)
log.Infof("Pairing %s (platform: %q, business name: %q). Type r within 3 seconds to reject pair", jid, platform, businessName)
select {
case reject := <-pairRejectChan:
if reject {
log.Infof("Rejecting pair")
return false
}
case <-time.After(3 * time.Second):
}
log.Infof("Accepting pair")
return true
}
ch, err := cli.GetQRChannel(context.Background())
if err != nil {
// This error means that we're already logged in, so ignore it.
if !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
log.Errorf("Failed to get QR channel: %v", err)
}
} else {
go func() {
for evt := range ch {
if evt.Event == "code" {
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
} else {
log.Infof("QR channel result: %s", evt.Event)
}
}
}()
}
cli.AddEventHandler(handler)
err = cli.Connect()
if err != nil {
log.Errorf("Failed to connect: %v", err)
return
}
c := make(chan os.Signal, 1)
input := make(chan string)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
defer close(input)
scan := bufio.NewScanner(os.Stdin)
for scan.Scan() {
line := strings.TrimSpace(scan.Text())
if len(line) > 0 {
input <- line
}
}
}()
for {
select {
case <-c:
log.Infof("Interrupt received, exiting")
cli.Disconnect()
return
case cmd := <-input:
if len(cmd) == 0 {
log.Infof("Stdin closed, exiting")
cli.Disconnect()
return
}
if isWaitingForPair.Load() {
if cmd == "r" {
pairRejectChan <- true
} else if cmd == "a" {
pairRejectChan <- false
}
continue
}
args := strings.Fields(cmd)
cmd = args[0]
args = args[1:]
go handleCmd(strings.ToLower(cmd), args)
}
}
}
func parseJID(arg string) (types.JID, bool) {
if arg[0] == '+' {
arg = arg[1:]
}
if !strings.ContainsRune(arg, '@') {
return types.NewJID(arg, types.DefaultUserServer), true
} else {
recipient, err := types.ParseJID(arg)
if err != nil {
log.Errorf("Invalid JID %s: %v", arg, err)
return recipient, false
} else if recipient.User == "" {
log.Errorf("Invalid JID %s: no server specified", arg)
return recipient, false
}
return recipient, true
}
}
func handleCmd(cmd string, args []string) {
switch cmd {
case "pair-phone":
if len(args) < 1 {
log.Errorf("Usage: pair-phone <number>")
return
}
linkingCode, err := cli.PairPhone(args[0], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
if err != nil {
panic(err)
}
fmt.Println("Linking code:", linkingCode)
case "reconnect":
cli.Disconnect()
err := cli.Connect()
if err != nil {
log.Errorf("Failed to connect: %v", err)
}
case "logout":
err := cli.Logout()
if err != nil {
log.Errorf("Error logging out: %v", err)
} else {
log.Infof("Successfully logged out")
}
case "appstate":
if len(args) < 1 {
log.Errorf("Usage: appstate <types...>")
return
}
names := []appstate.WAPatchName{appstate.WAPatchName(args[0])}
if args[0] == "all" {
names = []appstate.WAPatchName{appstate.WAPatchRegular, appstate.WAPatchRegularHigh, appstate.WAPatchRegularLow, appstate.WAPatchCriticalUnblockLow, appstate.WAPatchCriticalBlock}
}
resync := len(args) > 1 && args[1] == "resync"
for _, name := range names {
err := cli.FetchAppState(name, resync, false)
if err != nil {
log.Errorf("Failed to sync app state: %v", err)
}
}
case "request-appstate-key":
if len(args) < 1 {
log.Errorf("Usage: request-appstate-key <ids...>")
return
}
var keyIDs = make([][]byte, len(args))
for i, id := range args {
decoded, err := hex.DecodeString(id)
if err != nil {
log.Errorf("Failed to decode %s as hex: %v", id, err)
return
}
keyIDs[i] = decoded
}
cli.DangerousInternals().RequestAppStateKeys(context.Background(), keyIDs)
case "unavailable-request":
if len(args) < 3 {
log.Errorf("Usage: unavailable-request <chat JID> <sender JID> <message ID>")
return
}
chat, ok := parseJID(args[0])
if !ok {
return
}
sender, ok := parseJID(args[1])
if !ok {
return
}
resp, err := cli.SendMessage(
context.Background(),
cli.Store.ID.ToNonAD(),
cli.BuildUnavailableMessageRequest(chat, sender, args[2]),
whatsmeow.SendRequestExtra{Peer: true},
)
fmt.Println(resp)
fmt.Println(err)
case "checkuser":
if len(args) < 1 {
log.Errorf("Usage: checkuser <phone numbers...>")
return
}
resp, err := cli.IsOnWhatsApp(args)
if err != nil {
log.Errorf("Failed to check if users are on WhatsApp:", err)
} else {
for _, item := range resp {
if item.VerifiedName != nil {
log.Infof("%s: on whatsapp: %t, JID: %s, business name: %s", item.Query, item.IsIn, item.JID, item.VerifiedName.Details.GetVerifiedName())
} else {
log.Infof("%s: on whatsapp: %t, JID: %s", item.Query, item.IsIn, item.JID)
}
}
}
case "checkupdate":
resp, err := cli.CheckUpdate()
if err != nil {
log.Errorf("Failed to check for updates: %v", err)
} else {
log.Debugf("Version data: %#v", resp)
if resp.ParsedVersion == store.GetWAVersion() {
log.Infof("Client is up to date")
} else if store.GetWAVersion().LessThan(resp.ParsedVersion) {
log.Warnf("Client is outdated")
} else {
log.Infof("Client is newer than latest")
}
}
case "subscribepresence":
if len(args) < 1 {
log.Errorf("Usage: subscribepresence <jid>")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
err := cli.SubscribePresence(jid)
if err != nil {
fmt.Println(err)
}
case "presence":
if len(args) == 0 {
log.Errorf("Usage: presence <available/unavailable>")
return
}
fmt.Println(cli.SendPresence(types.Presence(args[0])))
case "chatpresence":
if len(args) == 2 {
args = append(args, "")
} else if len(args) < 2 {
log.Errorf("Usage: chatpresence <jid> <composing/paused> [audio]")
return
}
jid, _ := types.ParseJID(args[0])
fmt.Println(cli.SendChatPresence(jid, types.ChatPresence(args[1]), types.ChatPresenceMedia(args[2])))
case "privacysettings":
resp, err := cli.TryFetchPrivacySettings(false)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%+v\n", resp)
}
case "setprivacysetting":
if len(args) < 2 {
log.Errorf("Usage: setprivacysetting <setting> <value>")
return
}
setting := types.PrivacySettingType(args[0])
value := types.PrivacySetting(args[1])
resp, err := cli.SetPrivacySetting(setting, value)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%+v\n", resp)
}
case "getuser":
if len(args) < 1 {
log.Errorf("Usage: getuser <jids...>")
return
}
var jids []types.JID
for _, arg := range args {
jid, ok := parseJID(arg)
if !ok {
return
}
jids = append(jids, jid)
}
resp, err := cli.GetUserInfo(jids)
if err != nil {
log.Errorf("Failed to get user info: %v", err)
} else {
for jid, info := range resp {
log.Infof("%s: %+v", jid, info)
}
}
case "mediaconn":
conn, err := cli.DangerousInternals().RefreshMediaConn(false)
if err != nil {
log.Errorf("Failed to get media connection: %v", err)
} else {
log.Infof("Media connection: %+v", conn)
}
case "raw":
var node waBinary.Node
if err := json.Unmarshal([]byte(strings.Join(args, " ")), &node); err != nil {
log.Errorf("Failed to parse args as JSON into XML node: %v", err)
} else if err = cli.DangerousInternals().SendNode(node); err != nil {
log.Errorf("Error sending node: %v", err)
} else {
log.Infof("Node sent")
}
case "listnewsletters":
newsletters, err := cli.GetSubscribedNewsletters()
if err != nil {
log.Errorf("Failed to get subscribed newsletters: %v", err)
return
}
for _, newsletter := range newsletters {
log.Infof("* %s: %s", newsletter.ID, newsletter.ThreadMeta.Name.Text)
}
case "getnewsletter":
jid, ok := parseJID(args[0])
if !ok {
return
}
meta, err := cli.GetNewsletterInfo(jid)
if err != nil {
log.Errorf("Failed to get info: %v", err)
} else {
log.Infof("Got info: %+v", meta)
}
case "getnewsletterinvite":
meta, err := cli.GetNewsletterInfoWithInvite(args[0])
if err != nil {
log.Errorf("Failed to get info: %v", err)
} else {
log.Infof("Got info: %+v", meta)
}
case "livesubscribenewsletter":
if len(args) < 1 {
log.Errorf("Usage: livesubscribenewsletter <jid>")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
dur, err := cli.NewsletterSubscribeLiveUpdates(context.TODO(), jid)
if err != nil {
log.Errorf("Failed to subscribe to live updates: %v", err)
} else {
log.Infof("Subscribed to live updates for %s for %s", jid, dur)
}
case "getnewslettermessages":
if len(args) < 1 {
log.Errorf("Usage: getnewslettermessages <jid> [count] [before id]")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
count := 100
var err error
if len(args) > 1 {
count, err = strconv.Atoi(args[1])
if err != nil {
log.Errorf("Invalid count: %v", err)
return
}
}
var before types.MessageServerID
if len(args) > 2 {
before, err = strconv.Atoi(args[2])
if err != nil {
log.Errorf("Invalid message ID: %v", err)
return
}
}
messages, err := cli.GetNewsletterMessages(jid, &whatsmeow.GetNewsletterMessagesParams{Count: count, Before: before})
if err != nil {
log.Errorf("Failed to get messages: %v", err)
} else {
for _, msg := range messages {
log.Infof("%d: %+v (viewed %d times)", msg.MessageServerID, msg.Message, msg.ViewsCount)
}
}
case "createnewsletter":
if len(args) < 1 {
log.Errorf("Usage: createnewsletter <name>")
return
}
resp, err := cli.CreateNewsletter(whatsmeow.CreateNewsletterParams{
Name: strings.Join(args, " "),
})
if err != nil {
log.Errorf("Failed to create newsletter: %v", err)
} else {
log.Infof("Created newsletter %+v", resp)
}
case "getavatar":
if len(args) < 1 {
log.Errorf("Usage: getavatar <jid> [existing ID] [--preview] [--community]")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
existingID := ""
if len(args) > 2 {
existingID = args[2]
}
var preview, isCommunity bool
for _, arg := range args {
if arg == "--preview" {
preview = true
} else if arg == "--community" {
isCommunity = true
}
}
pic, err := cli.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{
Preview: preview,
IsCommunity: isCommunity,
ExistingID: existingID,
})
if err != nil {
log.Errorf("Failed to get avatar: %v", err)
} else if pic != nil {
log.Infof("Got avatar ID %s: %s", pic.ID, pic.URL)
} else {
log.Infof("No avatar found")
}
case "getgroup":
if len(args) < 1 {
log.Errorf("Usage: getgroup <jid>")
return
}
group, ok := parseJID(args[0])
if !ok {
return
} else if group.Server != types.GroupServer {
log.Errorf("Input must be a group JID (@%s)", types.GroupServer)
return
}
resp, err := cli.GetGroupInfo(group)
if err != nil {
log.Errorf("Failed to get group info: %v", err)
} else {
log.Infof("Group info: %+v", resp)
}
case "subgroups":
if len(args) < 1 {
log.Errorf("Usage: subgroups <jid>")
return
}
group, ok := parseJID(args[0])
if !ok {
return
} else if group.Server != types.GroupServer {
log.Errorf("Input must be a group JID (@%s)", types.GroupServer)
return
}
resp, err := cli.GetSubGroups(group)
if err != nil {
log.Errorf("Failed to get subgroups: %v", err)
} else {
for _, sub := range resp {
log.Infof("Subgroup: %+v", sub)
}
}
case "communityparticipants":
if len(args) < 1 {
log.Errorf("Usage: communityparticipants <jid>")
return
}
group, ok := parseJID(args[0])
if !ok {
return
} else if group.Server != types.GroupServer {
log.Errorf("Input must be a group JID (@%s)", types.GroupServer)
return
}
resp, err := cli.GetLinkedGroupsParticipants(group)
if err != nil {
log.Errorf("Failed to get community participants: %v", err)
} else {
log.Infof("Community participants: %+v", resp)
}
case "listgroups":
groups, err := cli.GetJoinedGroups()
if err != nil {
log.Errorf("Failed to get group list: %v", err)
} else {
for _, group := range groups {
log.Infof("%+v", group)
}
}
case "getinvitelink":
if len(args) < 1 {
log.Errorf("Usage: getinvitelink <jid> [--reset]")
return
}
group, ok := parseJID(args[0])
if !ok {
return
} else if group.Server != types.GroupServer {
log.Errorf("Input must be a group JID (@%s)", types.GroupServer)
return
}
resp, err := cli.GetGroupInviteLink(group, len(args) > 1 && args[1] == "--reset")
if err != nil {
log.Errorf("Failed to get group invite link: %v", err)
} else {
log.Infof("Group invite link: %s", resp)
}
case "queryinvitelink":
if len(args) < 1 {
log.Errorf("Usage: queryinvitelink <link>")
return
}
resp, err := cli.GetGroupInfoFromLink(args[0])
if err != nil {
log.Errorf("Failed to resolve group invite link: %v", err)
} else {
log.Infof("Group info: %+v", resp)
}
case "querybusinesslink":
if len(args) < 1 {
log.Errorf("Usage: querybusinesslink <link>")
return
}
resp, err := cli.ResolveBusinessMessageLink(args[0])
if err != nil {
log.Errorf("Failed to resolve business message link: %v", err)
} else {
log.Infof("Business info: %+v", resp)
}
case "joininvitelink":
if len(args) < 1 {
log.Errorf("Usage: acceptinvitelink <link>")
return
}
groupID, err := cli.JoinGroupWithLink(args[0])
if err != nil {
log.Errorf("Failed to join group via invite link: %v", err)
} else {
log.Infof("Joined %s", groupID)
}
case "updateparticipant":
if len(args) < 3 {
log.Errorf("Usage: updateparticipant <jid> <action> <numbers...>")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
action := whatsmeow.ParticipantChange(args[1])
switch action {
case whatsmeow.ParticipantChangeAdd, whatsmeow.ParticipantChangeRemove, whatsmeow.ParticipantChangePromote, whatsmeow.ParticipantChangeDemote:
default:
log.Errorf("Valid actions: add, remove, promote, demote")
return
}
users := make([]types.JID, len(args)-2)
for i, arg := range args[2:] {
users[i], ok = parseJID(arg)
if !ok {
return
}
}
resp, err := cli.UpdateGroupParticipants(jid, users, action)
if err != nil {
log.Errorf("Failed to add participant: %v", err)
return
}
for _, item := range resp {
if action == whatsmeow.ParticipantChangeAdd && item.Error == 403 && item.AddRequest != nil {
log.Infof("Participant is private: %d %s %s %v", item.Error, item.JID, item.AddRequest.Code, item.AddRequest.Expiration)
cli.SendMessage(context.TODO(), item.JID, &waProto.Message{
GroupInviteMessage: &waProto.GroupInviteMessage{
InviteCode: proto.String(item.AddRequest.Code),
InviteExpiration: proto.Int64(item.AddRequest.Expiration.Unix()),
GroupJid: proto.String(jid.String()),
GroupName: proto.String("Test group"),
Caption: proto.String("This is a test group"),
},
})
} else if item.Error == 409 {
log.Infof("Participant already in group: %d %s %+v", item.Error, item.JID)
} else if item.Error == 0 {
log.Infof("Added participant: %d %s %+v", item.Error, item.JID)
} else {
log.Infof("Unknown status: %d %s %+v", item.Error, item.JID)
}
}
case "getrequestparticipant":
if len(args) < 1 {
log.Errorf("Usage: getrequestparticipant <jid>")
return
}
group, ok := parseJID(args[0])
if !ok {
log.Errorf("Invalid JID")
return
}
resp, err := cli.GetGroupRequestParticipants(group)
if err != nil {
log.Errorf("Failed to get request participants: %v", err)
} else {
log.Infof("Request participants: %+v", resp)
}
case "getstatusprivacy":
resp, err := cli.GetStatusPrivacy()
fmt.Println(err)
fmt.Println(resp)
case "setdisappeartimer":
if len(args) < 2 {
log.Errorf("Usage: setdisappeartimer <jid> <days>")
return
}
days, err := strconv.Atoi(args[1])
if err != nil {
log.Errorf("Invalid duration: %v", err)
return
}
recipient, ok := parseJID(args[0])
if !ok {
return
}
err = cli.SetDisappearingTimer(recipient, time.Duration(days)*24*time.Hour)
if err != nil {
log.Errorf("Failed to set disappearing timer: %v", err)
}
case "setdefaultdisappeartimer":
if len(args) < 1 {
log.Errorf("Usage: setdefaultdisappeartimer <days>")
return
}
days, err := strconv.Atoi(args[0])
if err != nil {
log.Errorf("Invalid duration: %v", err)
return
}
err = cli.SetDefaultDisappearingTimer(time.Duration(days) * 24 * time.Hour)
if err != nil {
log.Errorf("Failed to set default disappearing timer: %v", err)
}
case "send":
if len(args) < 2 {
log.Errorf("Usage: send <jid> <text>")
return
}
recipient, ok := parseJID(args[0])
if !ok {
return
}
msg := &waProto.Message{Conversation: proto.String(strings.Join(args[1:], " "))}
resp, err := cli.SendMessage(context.Background(), recipient, msg)
if err != nil {
log.Errorf("Error sending message: %v", err)
} else {
log.Infof("Message sent (server timestamp: %s)", resp.Timestamp)
}
case "sendpoll":
if len(args) < 7 {
log.Errorf("Usage: sendpoll <jid> <max answers> <question> -- <option 1> / <option 2> / ...")
return
}
recipient, ok := parseJID(args[0])
if !ok {
return
}
maxAnswers, err := strconv.Atoi(args[1])
if err != nil {
log.Errorf("Number of max answers must be an integer")
return
}
remainingArgs := strings.Join(args[2:], " ")
question, optionsStr, _ := strings.Cut(remainingArgs, "--")
question = strings.TrimSpace(question)
options := strings.Split(optionsStr, "/")
for i, opt := range options {
options[i] = strings.TrimSpace(opt)
}
resp, err := cli.SendMessage(context.Background(), recipient, cli.BuildPollCreation(question, options, maxAnswers))
if err != nil {
log.Errorf("Error sending message: %v", err)
} else {
log.Infof("Message sent (server timestamp: %s)", resp.Timestamp)
}
case "react":
if len(args) < 3 {
log.Errorf("Usage: react <jid> <message ID> <reaction>")
return
}
recipient, ok := parseJID(args[0])
if !ok {
return
}
messageID := args[1]
fromMe := false
if strings.HasPrefix(messageID, "me:") {
fromMe = true
messageID = messageID[len("me:"):]
}
reaction := args[2]
if reaction == "remove" {
reaction = ""
}
msg := &waProto.Message{
ReactionMessage: &waProto.ReactionMessage{
Key: &waProto.MessageKey{
RemoteJid: proto.String(recipient.String()),
FromMe: proto.Bool(fromMe),
Id: proto.String(messageID),
},
Text: proto.String(reaction),
SenderTimestampMs: proto.Int64(time.Now().UnixMilli()),
},
}
resp, err := cli.SendMessage(context.Background(), recipient, msg)
if err != nil {
log.Errorf("Error sending reaction: %v", err)
} else {
log.Infof("Reaction sent (server timestamp: %s)", resp.Timestamp)
}
case "revoke":
if len(args) < 2 {
log.Errorf("Usage: revoke <jid> <message ID>")
return
}
recipient, ok := parseJID(args[0])
if !ok {
return
}
messageID := args[1]
resp, err := cli.SendMessage(context.Background(), recipient, cli.BuildRevoke(recipient, types.EmptyJID, messageID))
if err != nil {
log.Errorf("Error sending revocation: %v", err)
} else {
log.Infof("Revocation sent (server timestamp: %s)", resp.Timestamp)
}
case "sendimg":
if len(args) < 2 {
log.Errorf("Usage: sendimg <jid> <image path> [caption]")
return
}
recipient, ok := parseJID(args[0])
if !ok {
return
}
data, err := os.ReadFile(args[1])
if err != nil {
log.Errorf("Failed to read %s: %v", args[0], err)
return
}
var uploaded whatsmeow.UploadResponse
if recipient.Server == types.NewsletterServer {
uploaded, err = cli.UploadNewsletter(context.Background(), data, whatsmeow.MediaImage)
} else {
uploaded, err = cli.Upload(context.Background(), data, whatsmeow.MediaImage)
}
if err != nil {
log.Errorf("Failed to upload file: %v", err)
return
}
msg := &waProto.Message{ImageMessage: &waProto.ImageMessage{
Caption: proto.String(strings.Join(args[2:], " ")),
Url: proto.String(uploaded.URL),
DirectPath: proto.String(uploaded.DirectPath),
MediaKey: uploaded.MediaKey,
Mimetype: proto.String(http.DetectContentType(data)),
FileEncSha256: uploaded.FileEncSHA256,
FileSha256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(data))),
}}
resp, err := cli.SendMessage(context.Background(), recipient, msg, whatsmeow.SendRequestExtra{
MediaHandle: uploaded.Handle,
})
if err != nil {
log.Errorf("Error sending image message: %v", err)
} else {
log.Infof("Image message sent (server timestamp: %s)", resp.Timestamp)
}
case "setpushname":
if len(args) == 0 {
log.Errorf("Usage: setpushname <name>")
return
}
err := cli.SendAppState(appstate.BuildSettingPushName(strings.Join(args, " ")))
if err != nil {
log.Errorf("Error setting push name: %v", err)
} else {
log.Infof("Push name updated")
}
case "setstatus":
if len(args) == 0 {
log.Errorf("Usage: setstatus <message>")
return
}
err := cli.SetStatusMessage(strings.Join(args, " "))
if err != nil {
log.Errorf("Error setting status message: %v", err)
} else {
log.Infof("Status updated")
}
case "archive":
if len(args) < 2 {
log.Errorf("Usage: archive <jid> <action>")
return
}
target, ok := parseJID(args[0])
if !ok {
return
}
action, err := strconv.ParseBool(args[1])
if err != nil {
log.Errorf("invalid second argument: %v", err)
return
}
err = cli.SendAppState(appstate.BuildArchive(target, action, time.Time{}, nil))
if err != nil {
log.Errorf("Error changing chat's archive state: %v", err)
}
case "mute":
if len(args) < 2 {
log.Errorf("Usage: mute <jid> <action>")
return
}
target, ok := parseJID(args[0])
if !ok {
return
}
action, err := strconv.ParseBool(args[1])
if err != nil {
log.Errorf("invalid second argument: %v", err)
return
}
err = cli.SendAppState(appstate.BuildMute(target, action, 1*time.Hour))
if err != nil {
log.Errorf("Error changing chat's mute state: %v", err)
}
case "pin":
if len(args) < 2 {
log.Errorf("Usage: pin <jid> <action>")
return
}
target, ok := parseJID(args[0])
if !ok {
return
}
action, err := strconv.ParseBool(args[1])
if err != nil {
log.Errorf("invalid second argument: %v", err)
return
}
err = cli.SendAppState(appstate.BuildPin(target, action))
if err != nil {
log.Errorf("Error changing chat's pin state: %v", err)
}
case "getblocklist":
blocklist, err := cli.GetBlocklist()
if err != nil {
log.Errorf("Failed to get blocked contacts list: %v", err)
} else {
log.Infof("Blocklist: %+v", blocklist)
}
case "block":
if len(args) < 1 {
log.Errorf("Usage: block <jid>")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
resp, err := cli.UpdateBlocklist(jid, events.BlocklistChangeActionBlock)
if err != nil {
log.Errorf("Error updating blocklist: %v", err)
} else {
log.Infof("Blocklist updated: %+v", resp)
}
case "unblock":
if len(args) < 1 {
log.Errorf("Usage: unblock <jid>")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
resp, err := cli.UpdateBlocklist(jid, events.BlocklistChangeActionUnblock)
if err != nil {
log.Errorf("Error updating blocklist: %v", err)
} else {
log.Infof("Blocklist updated: %+v", resp)
}
case "labelchat":
if len(args) < 3 {
log.Errorf("Usage: labelchat <jid> <labelID> <action>")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
labelID := args[1]
action, err := strconv.ParseBool(args[2])
if err != nil {
log.Errorf("invalid third argument: %v", err)
return
}
err = cli.SendAppState(appstate.BuildLabelChat(jid, labelID, action))
if err != nil {
log.Errorf("Error changing chat's label state: %v", err)
}
case "labelmessage":
if len(args) < 4 {
log.Errorf("Usage: labelmessage <jid> <labelID> <messageID> <action>")
return
}
jid, ok := parseJID(args[0])
if !ok {
return
}
labelID := args[1]
messageID := args[2]
action, err := strconv.ParseBool(args[3])
if err != nil {
log.Errorf("invalid fourth argument: %v", err)
return
}
err = cli.SendAppState(appstate.BuildLabelMessage(jid, labelID, messageID, action))
if err != nil {
log.Errorf("Error changing message's label state: %v", err)
}
case "editlabel":
if len(args) < 4 {
log.Errorf("Usage: editlabel <labelID> <name> <color> <action>")
return
}
labelID := args[0]
name := args[1]
color, err := strconv.Atoi(args[2])
if err != nil {
log.Errorf("invalid third argument: %v", err)
return
}
action, err := strconv.ParseBool(args[3])
if err != nil {
log.Errorf("invalid fourth argument: %v", err)
return
}
err = cli.SendAppState(appstate.BuildLabelEdit(labelID, name, int32(color), action))
if err != nil {
log.Errorf("Error editing label: %v", err)
}
}
}
var historySyncID int32
var startupTime = time.Now().Unix()
func handler(rawEvt interface{}) {
switch evt := rawEvt.(type) {
case *events.AppStateSyncComplete:
if len(cli.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
err := cli.SendPresence(types.PresenceAvailable)
if err != nil {
log.Warnf("Failed to send available presence: %v", err)
} else {
log.Infof("Marked self as available")
}
}
case *events.Connected, *events.PushNameSetting:
if len(cli.Store.PushName) == 0 {
return
}
// Send presence available when connecting and when the pushname is changed.
// This makes sure that outgoing messages always have the right pushname.
err := cli.SendPresence(types.PresenceAvailable)
if err != nil {
log.Warnf("Failed to send available presence: %v", err)
} else {
log.Infof("Marked self as available")
}
case *events.StreamReplaced:
os.Exit(0)
case *events.Message:
metaParts := []string{fmt.Sprintf("pushname: %s", evt.Info.PushName), fmt.Sprintf("timestamp: %s", evt.Info.Timestamp)}
if evt.Info.Type != "" {
metaParts = append(metaParts, fmt.Sprintf("type: %s", evt.Info.Type))
}
if evt.Info.Category != "" {
metaParts = append(metaParts, fmt.Sprintf("category: %s", evt.Info.Category))
}
if evt.IsViewOnce {
metaParts = append(metaParts, "view once")
}
if evt.IsViewOnce {
metaParts = append(metaParts, "ephemeral")
}
if evt.IsViewOnceV2 {
metaParts = append(metaParts, "ephemeral (v2)")
}
if evt.IsDocumentWithCaption {
metaParts = append(metaParts, "document with caption")
}
if evt.IsEdit {
metaParts = append(metaParts, "edit")
}
log.Infof("Received message %s from %s (%s): %+v", evt.Info.ID, evt.Info.SourceString(), strings.Join(metaParts, ", "), evt.Message)
if evt.Message.GetPollUpdateMessage() != nil {
decrypted, err := cli.DecryptPollVote(evt)
if err != nil {
log.Errorf("Failed to decrypt vote: %v", err)
} else {
log.Infof("Selected options in decrypted vote:")
for _, option := range decrypted.SelectedOptions {
log.Infof("- %X", option)
}
}
} else if evt.Message.GetEncReactionMessage() != nil {
decrypted, err := cli.DecryptReaction(evt)
if err != nil {
log.Errorf("Failed to decrypt encrypted reaction: %v", err)
} else {
log.Infof("Decrypted reaction: %+v", decrypted)
}
}
img := evt.Message.GetImageMessage()
if img != nil {
data, err := cli.Download(img)
if err != nil {
log.Errorf("Failed to download image: %v", err)
return
}
exts, _ := mime.ExtensionsByType(img.GetMimetype())
path := fmt.Sprintf("%s%s", evt.Info.ID, exts[0])
err = os.WriteFile(path, data, 0600)
if err != nil {
log.Errorf("Failed to save image: %v", err)
return
}
log.Infof("Saved image in message to %s", path)
}
case *events.Receipt:
if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf {
log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp)
} else if evt.Type == types.ReceiptTypeDelivered {
log.Infof("%s was delivered to %s at %s", evt.MessageIDs[0], evt.SourceString(), evt.Timestamp)
}
case *events.Presence:
if evt.Unavailable {
if evt.LastSeen.IsZero() {
log.Infof("%s is now offline", evt.From)
} else {
log.Infof("%s is now offline (last seen: %s)", evt.From, evt.LastSeen)
}
} else {
log.Infof("%s is now online", evt.From)
}
case *events.HistorySync:
id := atomic.AddInt32(&historySyncID, 1)
fileName := fmt.Sprintf("history-%d-%d.json", startupTime, id)
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
log.Errorf("Failed to open file to write history sync: %v", err)
return
}
enc := json.NewEncoder(file)
enc.SetIndent("", " ")
err = enc.Encode(evt.Data)
if err != nil {
log.Errorf("Failed to write history sync: %v", err)
return
}
log.Infof("Wrote history sync to %s", fileName)
_ = file.Close()
case *events.AppState:
log.Debugf("App state event: %+v / %+v", evt.Index, evt.SyncActionValue)
case *events.KeepAliveTimeout:
log.Debugf("Keepalive timeout event: %+v", evt)
case *events.KeepAliveRestored:
log.Debugf("Keepalive restored")
case *events.Blocklist:
log.Infof("Blocklist event: %+v", evt)
}
}