Created
April 7, 2023 22:56
-
-
Save vitouXY/0e5909b0801f4ca5bae3dc508138def2 to your computer and use it in GitHub Desktop.
tulir-whatsmeow/mdtest/main.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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