Skip to content

Instantly share code, notes, and snippets.

@vitouXY
Created April 7, 2023 22:56
Show Gist options
  • Save vitouXY/0e5909b0801f4ca5bae3dc508138def2 to your computer and use it in GitHub Desktop.
Save vitouXY/0e5909b0801f4ca5bae3dc508138def2 to your computer and use it in GitHub Desktop.
tulir-whatsmeow/mdtest/main.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/.
/* 1ee2ff1 07042023
mdtest/main.go
[!] https://github.com/tulir/whatsmeow/blob/main/mdtest/main.go
[!] https://github.com/tulir/whatsmeow
[!] https://github.com/danielgross/whatsapp-gpt
$ echo '{ "BlackList": ["56900000001", "56900000004"] }' > wspReq.json
*/
package main
import (
"bufio"
"context"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"mime"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"sync/atomic"
"syscall"
"time"
"bytes"
"net/url"
_ "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)
// { "phoneNumbers": [], "HotWord": [], "author": "" }
type Config struct {
BlackList []string
// PhoneNumbers []string
// HotWords []string
// Author string
// Author string
}
/*
// contains checks if a string is in an array of strings
func contains(arr []string, str string) bool {
// if the array is empty, return true
if len(arr) == 0 {
return true
}
for _, a := range arr {
if a == str {
return true
}
}
return false
}
*/
func readConfig() Config {
// Read the config file
file, err := os.Open("wspReq.json")
if err != nil {
fmt.Println("BlackList - Error opening JSON file:", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
config := Config{}
err = decoder.Decode(&config)
if err != nil {
fmt.Println("BlackList - Error decoding JSON file:", err)
}
return config
}
func stringInArray(str string, arr []string) bool {
for _, element := range arr {
if element == str {
return true
}
}
return false
}
func main() {
waBinary.IndentXML = true
flag.Parse()
if *debugLogs {
logLevel = "DEBUG"
}
if *requestFullSync {
store.DeviceProps.RequireFullSync = proto.Bool(true)
}
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)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
cli.Disconnect()
*/
c := make(chan os.Signal)
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 "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 "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 "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 "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 "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 "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 "multisend":
if len(args) < 3 {
log.Errorf("Usage: multisend <jids...> -- <text>")
return
}
var recipients []types.JID
for len(args) > 0 && args[0] != "--" {
recipient, ok := parseJID(args[0])
args = args[1:]
if !ok {
return
}
recipients = append(recipients, recipient)
}
if len(args) == 0 {
log.Errorf("Usage: multisend <jids...> -- <text> (the -- is required)")
return
}
msg := &waProto.Message{Conversation: proto.String(strings.Join(args[1:], " "))}
for _, recipient := range recipients {
go func(recipient types.JID) {
resp, err := cli.SendMessage(context.Background(), recipient, msg)
if err != nil {
log.Errorf("Error sending message to %s: %v", recipient, err)
} else {
log.Infof("Message sent to %s (server timestamp: %s)", recipient, resp.Timestamp)
}
}(recipient)
}
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
}
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)
if err != nil {
log.Errorf("Error sending image message: %v", err)
} else {
log.Infof("Image message sent (server timestamp: %s)", resp.Timestamp)
}
case "sendaud":
if len(args) < 2 {
log.Errorf("Usage: sendaud <jid> <audio path>")
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
}
uploaded, err := cli.Upload(context.Background(), data, whatsmeow.MediaAudio)
if err != nil {
log.Errorf("Failed to upload file: %v", err)
return
}
/*
// if minetype is audio/ogg
Ptt := false
if caption == "ppt" {
Ptt = true
}
*/
msg := &waProto.Message{AudioMessage: &waProto.AudioMessage{
Url: proto.String(uploaded.URL),
// Ptt: proto.Bool(Ptt),
Ptt: proto.Bool(true),
DirectPath: proto.String(uploaded.DirectPath),
MediaKey: uploaded.MediaKey,
// Mimetype: uploaded.MediaKey,
Mimetype: proto.String("audio/ogg; codecs=opus"),
FileSha256: uploaded.FileSHA256,
FileEncSha256: uploaded.FileEncSHA256,
FileLength: proto.Uint64(uploaded.FileLength),
},}
/*
msg := &waProto.Message{AudioMessage: &waProto.AudioMessage{
// Seconds: proto.Uint32(uint32(len(data))),
// StreamingSidecar: []byte{},
// Waveform: []byte{},
Url: proto.String(uploaded.URL),
Ptt: proto.Bool(true),
DirectPath: proto.String(uploaded.DirectPath),
MediaKey: uploaded.MediaKey,
Mimetype: proto.String("audio/ogg; codecs=opus"),
FileEncSha256: uploaded.FileEncSHA256,
FileSha256: uploaded.FileSHA256,
FileLength: proto.Uint64(uint64(len(data))),
}}
*/
//resp, err := cli.SendMessage(context.Background(), recipient, "", msg)
resp, err := cli.SendMessage(context.Background(), recipient, msg)
if err != nil {
log.Errorf("Error sending audio message: %v", err)
} else {
log.Infof("Image message sent (server timestamp: %s)", resp.Timestamp)
}
case "sendreply": // reply or quote
if len(args) < 3 {
log.Errorf("Usage: sendreply <jid> <message id> <reply>")
return
}
recipient, ok := parseJID(args[0])
if !ok {
return
}
messageID := args[1]
reply := args[2]
msg := &waProto.Message{
ExtendedTextMessage: &waProto.ExtendedTextMessage{
Text: proto.String(reply),
ContextInfo: &waProto.ContextInfo{
StanzaId: proto.String(messageID),
Participant: proto.String(recipient.String()),
//QuotedMessage: evt.Message,
QuotedMessage: &waProto.Message{Conversation: proto.String("☝️")},
},
},
}
//resp, err := cli.SendMessage(context.Background(), evt.Info.Chat, "", msg)
resp, err := cli.SendMessage(context.Background(), recipient, msg)
if err != nil {
log.Errorf("Error sending reply: %v", err)
} else {
log.Infof("Reply sent (server timestamp: %s)", resp.Timestamp)
}
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")
}
}
}
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.Info.IsGroup {
return
}
cfgData := readConfig()
denyPhoneNumbers := cfgData.BlackList
if stringInArray(evt.Info.Sender.User, denyPhoneNumbers) {
//fmt.Println("TEST - found - return")
return
}
/*
phoneNumbers := cfgData.PhoneNumbers
if !contains(phoneNumbers, evt.Info.Sender.User) {
//fmt.Println("TEST - contains PhoneNumbers - return")
return
}
if !contains(cfgData.HotWords, msg) {
//fmt.Println("TEST - contains HotWords - return")
return
}
// remove the hotwords from the message
for _, hotword := range cfgData.HotWords {
msg = msg[len(hotword):]
}
*/
var request_content string
if evt.Message.ExtendedTextMessage.GetText() != "" {
request_content = evt.Message.ExtendedTextMessage.GetText()
} else if evt.Message.GetConversation() != "" {
request_content = evt.Message.GetConversation()
}
/*
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)
}
}
*/
//case evt.Message.ImageMessage != nil:
img := evt.Message.GetImageMessage()
if img != nil {
request_content = img.GetCaption()
/*
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])
//path := fmt.Sprintf("%s-%s%s", sender, 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 evt.Message.AudioMessage != nil:
audio := evt.Message.GetAudioMessage()
if audio != nil {
data, err := cli.Download(audio)
if err != nil {
log.Errorf("Failed to download audio: %v", err)
return
}
exts, _ := mime.ExtensionsByType(audio.GetMimetype())
path := fmt.Sprintf("%s%s", evt.Info.ID, exts[0])
//path := fmt.Sprintf("%s-%s%s", sender, evt.Info.ID, exts[0])
err = os.WriteFile(path, data, 0600)
if err != nil {
log.Errorf("Failed to save audio: %v", err)
return
}
log.Infof("Saved audio in message to %s", path)
}
//case evt.Message.VideoMessage != nil:
video := evt.Message.GetVideoMessage()
if video != nil {
request_content = video.GetCaption()
/*
data, err := cli.Download(video)
if err != nil {
log.Errorf("Failed to download video: %v", err)
return
}
exts, _ := mime.ExtensionsByType(video.GetMimetype())
path := fmt.Sprintf("%s%s", evt.Info.ID, exts[0])
//path := fmt.Sprintf("%s-%s%s", sender, evt.Info.ID, exts[0])
err = os.WriteFile(path, data, 0600)
if err != nil {
log.Errorf("Failed to save video: %v", err)
return
}
log.Infof("Saved video in message to %s", path)
*/
}
//case evt.Message.DocumentMessage != nil:
document := evt.Message.GetDocumentMessage()
if document != nil {
request_content = document.GetCaption()
/*
data, err := cli.Download(document)
if err != nil {
log.Errorf("Failed to download audio: %v", err)
return
}
exts, _ := mime.ExtensionsByType(document.GetMimetype())
path := fmt.Sprintf("%s%s", evt.Info.ID, exts[0])
//path := fmt.Sprintf("%s-%s%s", sender, evt.Info.ID, exts[0])
err = os.WriteFile(path, data, 0600)
if err != nil {
log.Errorf("Failed to save document: %v", err)
return
}
log.Infof("Saved document in message to %s", path)
*/
}
if request_content == "" {
return
}
log.Infof("[*] Received message %s from %s: %s", evt.Info.ID, evt.Info.Sender.User, request_content)
urlEncoded := url.QueryEscape(request_content)
urlEncodedUser := url.QueryEscape(evt.Info.Sender.User)
urlEncodedID := url.QueryEscape(evt.Info.ID)
url := "http://localhost:5001/chat?f=" + urlEncodedUser + "&i=" + urlEncodedID + "&q=" + urlEncoded
respo, err := http.Get(url)
if err != nil {
log.Errorf("Error making request: %v", err)
return
}
buf := new(bytes.Buffer)
buf.ReadFrom(respo.Body)
response_content := buf.String()
if response_content == "" {
return
}
/*
response := &waProto.Message{Conversation: proto.String(string(response_content))}
userJid := types.NewJID(evt.Info.Sender.User, types.DefaultUserServer)
resp, err := cli.SendMessage(context.Background(), userJid, "", response)
if err != nil {
log.Errorf("Error sending message: %v", err)
} else {
log.Errorf("Message sent (server timestamp: %s)", resp.Timestamp)
}
*/
recipient, ok := parseJID(evt.Info.Sender.String())
if !ok {
return
}
//msg := &waProto.Message{Conversation: proto.String(string(response_content))}
messageID := evt.Info.ID
reply := string(response_content)
msg := &waProto.Message{
ExtendedTextMessage: &waProto.ExtendedTextMessage{
Text: proto.String(reply),
ContextInfo: &waProto.ContextInfo{
StanzaId: proto.String(messageID),
Participant: proto.String(recipient.String()),
QuotedMessage: evt.Message,
//QuotedMessage: &waProto.Message{Conversation: proto.String("🤖💬")},
},
},
}
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 *events.Receipt:
if evt.Type == events.ReceiptTypeRead || evt.Type == events.ReceiptTypeReadSelf {
log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp)
} else if evt.Type == events.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)
if evt.ErrorCount > 3 {
log.Debugf("Got >3 keepalive timeouts, forcing reconnect")
go func() {
cli.Disconnect()
err := cli.Connect()
if err != nil {
log.Errorf("Error force-reconnecting after keepalive timeouts: %v", err)
}
}()
}
case *events.KeepAliveRestored:
log.Debugf("Keepalive restored")
}
}
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
[!] https://github.com/tulir/whatsmeow
[!] https://github.com/danielgross/whatsapp-gpt
$ mkdir wspHistory
$ echo '{ "BlackList": ["56900000001", "56900000004"] }' > wspReq.json
$ uvicorn oaiRes:APP --host 127.0.0.1 --port 5001 --reload
"""
## ffmpeg
import fastapi ## uvicorn
import asyncio
import aiohttp
import json
import datetime
import typing
import pathlib
#import re
import os
import subprocess
## OpenAI API Completions - Chat
## https://beta.openai.com/examples/default-chat
## https://beta.openai.com/docs/api-reference/completions/create
## https://platform.openai.com/docs/guides/speech-to-text
OPENAI_URL = "https://api.openai.com/v1/"
## https://beta.openai.com/account/api-keys
OPENAI_API_KEY_LIST = []
#OPENAI_API_KEY_LIST = ["sk-aBcD...xYZ"]
#OPENAI_API_KEY_LIST = [os.environ["OPENAI_API_KEY"]]
OPENAI_API_KEY_LIST = [
"sk-...",
"sk-...",
"sk-..."
]
## https://platform.openai.com/docs/models/models
OPENAI_MODEL = "text-davinci-003"
#OPENAI_MODEL = "text-babbage-001"
## https://beta.openai.com/tokenizer
#OPENAI_MAX_TOKENS = 4000
OPENAI_MAX_TOKENS = 400
async def get_response(prompt: str, apikey: str) -> str:
try:
data = json.dumps({
"stop": ["Humano:", "Asistente:"],
"frequency_penalty": 0.5,
"presence_penalty": 0.0,
"top_p": 1.0,
"temperature": 0.5,
"model": OPENAI_MODEL,
"max_tokens": OPENAI_MAX_TOKENS,
"echo": False,
"n": 1,
"best_of": 1,
"logprobs": None,
"stream": False,
"prompt": prompt,
})
headers = {
"Authorization": "Bearer " + apikey,
"Content-Type": "application/json",
}
async with aiohttp.ClientSession() as session:
async with session.post(
OPENAI_URL + "completions",
headers=headers,
data=data
) as response:
result = await response.text()
response = json.loads(result)
except:
raise fastapi.HTTPException(status_code=408, detail="🤖🗯️")
return response
async def get_transcript(fileid: str, apikey: str) -> str:
try:
data = aiohttp.FormData()
data = aiohttp.FormData()
data.add_field('file',
open(fileid, 'rb'),
content_type='audio/wav')
data.add_field('model', 'whisper-1')
#data.add_field('response_format', 'text')
headers = {
"Authorization": "Bearer " + apikey,
#"Content-Type": "multipart/form-data",
}
async with aiohttp.ClientSession() as session:
async with session.post(
OPENAI_URL + "audio/transcriptions",
headers=headers,
data=data
) as response:
result = await response.text()
response = json.loads(result)
except:
raise fastapi.HTTPException(status_code=408, detail="🤖🗯️")
return response
APP = fastapi.FastAPI(title=__name__)
@APP.on_event("startup")
async def startup_event():
print("INFO: ", OPENAI_MODEL, "& Whisper")
"""
@APP.on_event("shutdown")
def shutdown_event():
print("Bye!")
"""
@APP.get("/chat") ## /chat?f=56900000000&i=A1BCFFF&q=Hola+Vito
async def chat(i: str, q: typing.Optional[str] = "", f: typing.Optional[str] = ""):
api_request_id = i.strip()
api_request_from = f.strip()
api_request = q.strip()
if api_request_id and api_request_id is not None and not api_request:
gfile_path = pathlib.Path(api_request_id + ".oga")
afile_path = pathlib.Path(api_request_id + ".wav")
if gfile_path.exists():
try:
command = ['ffmpeg', '-y', '-acodec', 'libopus', '-i', f'{gfile_path}', '-acodec', 'pcm_s16le',f'{afile_path}']
subprocess.run(command, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
except:pass
os.remove(gfile_path)
if afile_path.exists():
print(" MSG_ID:", api_request_id)
for keys in OPENAI_API_KEY_LIST:
#api_response_id = asyncio.run(get_transcript(afile_path, keys))
api_response_id = await asyncio.create_task(get_transcript(afile_path, keys))
if "text" in api_response_id:
break
else:
continue
os.remove(afile_path)
if "text" in api_response_id:
api_request = api_response_id["text"].strip()
elif "error" in api_response_id:
#raise fastapi.HTTPException(status_code=400, detail="🤖🗯️")
return ''
else:
#raise fastapi.HTTPException(status_code=405, detail="🤖🗯️")
return ''
if api_request_from and api_request_from is not None:
bfile_path = pathlib.Path("wspReq.json")
blackList = []
if bfile_path.exists():
with open(bfile_path, "r") as bfile:
blackList = json.load(bfile)["BlackList"]
if api_request_from in str(blackList):
#raise fastapi.HTTPException(status_code=401, detail="📵")
return ''
os.makedirs("./wspHistory", exist_ok=True)
file_path = pathlib.Path("./wspHistory/" + api_request_from + ".wspoai")
#file_path = pathlib.Path(api_request_from + ".wspoai")
print(" FROM_ID:", api_request_from)
if not api_request:
#raise fastapi.HTTPException(status_code=400, detail="🤖💭")
return ''
print("FROM_MSG:", api_request)
#return fastapi.Response(content="🤖: testing!", media_type="plain/text")
#now = datetime.datetime.now()
now = datetime.datetime.now(datetime.timezone.utc).astimezone()
days_months = {
"Monday": "Lunes",
"Tuesday": "Martes",
"Wednesday": "Miércoles",
"Thursday": "Jueves",
"Friday": "Viernes",
"Saturday": "Sábado",
"Sunday": "Domingo",
"January": "Enero",
"February": "Febrero",
"March": "Marzo",
"April": "Abril",
"May": "Mayo",
"June": "Junio",
"July": "Julio",
"August": "Agosto",
"September": "Septiembre",
"October": "Octubre",
"November": "Noviembre",
"December": "Diciembre",
}
get_day = days_months[now.strftime("%A")]
get_month = days_months[now.strftime("%B")]
get_date = now.strftime("%d de " + get_month + " del %Y")
get_hours = now.strftime("%H:%M")
chat_format = []
chat_format = json.loads(
'[{"role": "system", "content": "' + (
"La siguiente es una conversación en WhatsApp"
" con un asistente virtual,"
" respondiendo en lugar de Vito,"
" quien no puede participar en este momento."
" El asistente es ingenioso y astuto,"
" pero puede ser un poco brusco con"
" sus respuestas sarcásticas a las preguntas planteadas."
" Hoy es {}, {}, y actualmente son las {} horas de Chile."
" En la conversacion se pueden utilizar emojis y texto con formato."
).format(get_day, get_date, get_hours) + '"}]'
)
## History
if api_request_from and api_request_from is not None:
if file_path.exists():
with open(file_path, "r") as rfile:
chat_format_msg = json.load(rfile)
chat_format = chat_format + chat_format_msg
else:
chat_format.append({"role": "user", "content": "Hola!"})
chat_format.append({"role": "assistant", "content": "Hola, ¿en qué puedo ayudarte?"})
else:
##
chat_format.append({"role": "user", "content": "Hola!"})
chat_format.append({"role": "assistant", "content": "Hola, ¿en qué puedo ayudarte?"})
chat_format.append({"role": "user", "content": f"{api_request}"})
chat_plain_format = ""
for item in range(len(chat_format)):
item_role = chat_format[item]["role"].upper()
item_content = chat_format[item]["content"] + "\n"
if item_role == "SYSTEM":
item_role = ""
elif item_role == "USER":
item_role = "Humano: "
elif item_role == "ASSISTANT":
item_role = "Asistente: "
chat_plain_format = chat_plain_format + item_role + item_content
chat_plain_format = chat_plain_format + "Asistente:"
for keys in OPENAI_API_KEY_LIST:
api_response = await asyncio.create_task(get_response(chat_plain_format.strip(), keys))
if "choices" in api_response:
break
else:
continue
if "choices" in api_response:
api_response = api_response["choices"][0]["text"]
elif "error" in api_response:
#raise fastapi.HTTPException(status_code=400, detail="🤖🗯️")
return ''
else:
#raise fastapi.HTTPException(status_code=405, detail="🤖🗯️")
return ''
api_response = api_response.lstrip("Asistente: ").strip().lstrip("Asistente: ").strip()
#api_response = re.sub(r"^\n+IA: ", "", api_response)
#api_response = re.sub(r"^\n+", "", api_response)
#api_response = re.sub(r"^ ", "", api_response)
if api_response:
chat_format.append({"role": "assistant", "content": f"{api_response}"})
## History
if api_request_from and api_request_from is not None:
del chat_format[0]
with open(file_path, "w") as wfile:
json.dump(chat_format, wfile, indent=1, separators=(',', ': '))
##
#return fastapi.Response(content="🤖💬 " + api_response, media_type="plain/text")
return fastapi.Response(content=api_response, media_type="plain/text")
"""
${CURL:-curl} --request GET --url 'https://api.openai.com/v1/models' --header "Authorization: Bearer ${OPENAI_API_KEY:?}" |${JQ-jq} '.data[].id' - 2>/dev/null |${SED:-sed} -e 's#^"##g' -e 's#"$##g'
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment