From 608fbbfe5b57c137ecc0a31590f3190a5391e1e8 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 10:16:32 -0300 Subject: [PATCH 01/24] Moved webhook helpers to a separate class --- helpers.go | 18 ------------------ webhook/webhook.go | 28 ++++++++++++++++++++++++++++ wmiau.go | 6 ++++-- 3 files changed, 32 insertions(+), 20 deletions(-) create mode 100644 webhook/webhook.go diff --git a/helpers.go b/helpers.go index d21a5012..2e2a5f78 100644 --- a/helpers.go +++ b/helpers.go @@ -15,21 +15,3 @@ func updateUserInfo(values interface{}, field string, value string) interface{} values.(Values).m[field] = value return values } - -// webhook for regular messages -func callHook(myurl string, payload map[string]string, id int) { - log.Info().Str("url", myurl).Msg("Sending POST") - _, err := clientHttp[id].R().SetFormData(payload).Post(myurl) - - if err != nil { - log.Debug().Str("error", err.Error()) - } -} - -// webhook for messages with file attachments -func callHookFile(myurl string, payload map[string]string, id int, file string) { - log.Info().Str("file", file).Str("url", myurl).Msg("Sending POST") - clientHttp[id].R().SetFiles(map[string]string{ - "file": file, - }).SetFormData(payload).Post(myurl) -} diff --git a/webhook/webhook.go b/webhook/webhook.go new file mode 100644 index 00000000..6cb0a593 --- /dev/null +++ b/webhook/webhook.go @@ -0,0 +1,28 @@ +package webhook + +import ( + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" +) + +type Webhook struct { + ClientHttp map[int]*resty.Client +} + +// webhook for regular messages +func (w *Webhook) CallHook(myurl string, payload map[string]string, id int) { + log.Info().Str("url", myurl).Msg("Sending POST") + _, err := w.ClientHttp[id].R().SetFormData(payload).Post(myurl) + + if err != nil { + log.Debug().Str("error", err.Error()) + } +} + +// webhook for messages with file attachments +func (w *Webhook) CallHookFile(myurl string, payload map[string]string, id int, file string) { + log.Info().Str("file", file).Str("url", myurl).Msg("Sending POST") + w.ClientHttp[id].R().SetFiles(map[string]string{ + "file": file, + }).SetFormData(payload).Post(myurl) +} diff --git a/wmiau.go b/wmiau.go index d7436c05..dfbd18a9 100644 --- a/wmiau.go +++ b/wmiau.go @@ -15,6 +15,7 @@ import ( "strings" "sync/atomic" "time" + "wuzapi/webhook" "github.com/go-resty/resty/v2" "github.com/mdp/qrterminal/v3" @@ -598,16 +599,17 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { if webhookurl != "" { log.Info().Str("url", webhookurl).Msg("Calling webhook") values, _ := json.Marshal(postmap) + webhook := webhook.Webhook{ClientHttp: clientHttp} if path == "" { data := make(map[string]string) data["jsonData"] = string(values) data["token"] = mycli.token - go callHook(webhookurl, data, mycli.userID) + go webhook.CallHook(webhookurl, data, mycli.userID) } else { data := make(map[string]string) data["jsonData"] = string(values) data["token"] = mycli.token - go callHookFile(webhookurl, data, mycli.userID, path) + go webhook.CallHookFile(webhookurl, data, mycli.userID, path) } } else { log.Warn().Str("userid", strconv.Itoa(mycli.userID)).Msg("No webhook set for user") From 3f421e215b117c0b3caeedeff9050b5d4936c6c7 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 10:23:52 -0300 Subject: [PATCH 02/24] Created Message class to validate message fields --- handlers.go | 63 +++++++++++---------------------------- internal/helpers/wmiau.go | 48 +++++++++++++++++++++++++++++ message/message.go | 36 ++++++++++++++++++++++ 3 files changed, 101 insertions(+), 46 deletions(-) create mode 100644 internal/helpers/wmiau.go create mode 100644 message/message.go diff --git a/handlers.go b/handlers.go index 988206c1..215d72ce 100644 --- a/handlers.go +++ b/handlers.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "time" + "wuzapi/message" "github.com/patrickmn/go-cache" "github.com/vincent-petithory/dataurl" @@ -487,11 +488,10 @@ func (s *server) GetStatus() http.HandlerFunc { func (s *server) SendDocument() http.HandlerFunc { type documentStruct struct { - Phone string + message.Message Document string FileName string Id string - ContextInfo waProto.ContextInfo } return func(w http.ResponseWriter, r *http.Request) { @@ -529,7 +529,7 @@ func (s *server) SendDocument() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) + recipient, err := t.ValidateMessageFields() if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -604,11 +604,10 @@ func (s *server) SendDocument() http.HandlerFunc { func (s *server) SendAudio() http.HandlerFunc { type audioStruct struct { - Phone string + message.Message Audio string Caption string Id string - ContextInfo waProto.ContextInfo } return func(w http.ResponseWriter, r *http.Request) { @@ -641,7 +640,7 @@ func (s *server) SendAudio() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) + recipient, err := t.ValidateMessageFields() if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -720,11 +719,10 @@ func (s *server) SendAudio() http.HandlerFunc { func (s *server) SendImage() http.HandlerFunc { type imageStruct struct { - Phone string + message.Message Image string Caption string Id string - ContextInfo waProto.ContextInfo } return func(w http.ResponseWriter, r *http.Request) { @@ -757,7 +755,7 @@ func (s *server) SendImage() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) + recipient, err := t.ValidateMessageFields() if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -832,11 +830,10 @@ func (s *server) SendImage() http.HandlerFunc { func (s *server) SendSticker() http.HandlerFunc { type stickerStruct struct { - Phone string + message.Message Sticker string Id string PngThumbnail []byte - ContextInfo waProto.ContextInfo } return func(w http.ResponseWriter, r *http.Request) { @@ -869,7 +866,7 @@ func (s *server) SendSticker() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) + recipient, err := t.ValidateMessageFields() if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -944,12 +941,11 @@ func (s *server) SendSticker() http.HandlerFunc { func (s *server) SendVideo() http.HandlerFunc { type imageStruct struct { - Phone string + message.Message Video string Caption string Id string JpegThumbnail []byte - ContextInfo waProto.ContextInfo } return func(w http.ResponseWriter, r *http.Request) { @@ -982,7 +978,7 @@ func (s *server) SendVideo() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) + recipient, err := t.ValidateMessageFields() if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -1058,11 +1054,10 @@ func (s *server) SendVideo() http.HandlerFunc { func (s *server) SendContact() http.HandlerFunc { type contactStruct struct { - Phone string + message.Message Id string Name string Vcard string - ContextInfo waProto.ContextInfo } return func(w http.ResponseWriter, r *http.Request) { @@ -1098,7 +1093,7 @@ func (s *server) SendContact() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) + recipient, err := t.ValidateMessageFields() if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -1146,12 +1141,11 @@ func (s *server) SendContact() http.HandlerFunc { func (s *server) SendLocation() http.HandlerFunc { type locationStruct struct { - Phone string + message.Message Id string Name string Latitude float64 Longitude float64 - ContextInfo waProto.ContextInfo } return func(w http.ResponseWriter, r *http.Request) { @@ -1187,7 +1181,7 @@ func (s *server) SendLocation() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) + recipient, err := t.ValidateMessageFields() if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -1483,10 +1477,9 @@ func (s *server) SendList() http.HandlerFunc { func (s *server) SendMessage() http.HandlerFunc { type textStruct struct { - Phone string + message.Message Body string Id string - ContextInfo waProto.ContextInfo } return func(w http.ResponseWriter, r *http.Request) { @@ -1520,7 +1513,7 @@ func (s *server) SendMessage() http.HandlerFunc { return } - recipient, err := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) + recipient, err := t.ValidateMessageFields() if err != nil { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, err) @@ -2986,25 +2979,3 @@ func (s *server) Respond(w http.ResponseWriter, r *http.Request, status int, dat panic("respond: " + err.Error()) } } - -func validateMessageFields(phone string, stanzaid *string, participant *string) (types.JID, error) { - - recipient, ok := parseJID(phone) - if !ok { - return types.NewJID("", types.DefaultUserServer), errors.New("Could not parse Phone") - } - - if stanzaid != nil { - if participant == nil { - return types.NewJID("", types.DefaultUserServer), errors.New("Missing Participant in ContextInfo") - } - } - - if participant != nil { - if stanzaid == nil { - return types.NewJID("", types.DefaultUserServer), errors.New("Missing StanzaId in ContextInfo") - } - } - - return recipient, nil -} diff --git a/internal/helpers/wmiau.go b/internal/helpers/wmiau.go new file mode 100644 index 00000000..216d54e8 --- /dev/null +++ b/internal/helpers/wmiau.go @@ -0,0 +1,48 @@ +package helpers + +import ( + "strings" + + "github.com/rs/zerolog/log" + "go.mau.fi/whatsmeow/types" +) + +func ParseJID(arg string) (types.JID, bool) { + if arg == "" { + return types.NewJID("", types.DefaultUserServer), false + } + if arg[0] == '+' { + arg = arg[1:] + } + + // Basic only digit check for recipient phone number, we want to remove @server and .session + phonenumber := "" + phonenumber = strings.Split(arg, "@")[0] + phonenumber = strings.Split(phonenumber, ".")[0] + b := true + for _, c := range phonenumber { + if c < '0' || c > '9' { + b = false + break + } + } + if b == false { + log.Warn().Msg("Bad jid format, return empty") + recipient, _ := types.ParseJID("") + return recipient, false + } + + if !strings.ContainsRune(arg, '@') { + return types.NewJID(arg, types.DefaultUserServer), true + } else { + recipient, err := types.ParseJID(arg) + if err != nil { + log.Error().Err(err).Str("jid", arg).Msg("Invalid jid") + return recipient, false + } else if recipient.User == "" { + log.Error().Err(err).Str("jid", arg).Msg("Invalid jid. No server specified") + return recipient, false + } + return recipient, true + } +} diff --git a/message/message.go b/message/message.go new file mode 100644 index 00000000..0e90d824 --- /dev/null +++ b/message/message.go @@ -0,0 +1,36 @@ +package message + +import ( + "errors" + "wuzapi/internal/helpers" + + waProto "go.mau.fi/whatsmeow/binary/proto" + "go.mau.fi/whatsmeow/types" +) + +type Message struct { + Phone string + ContextInfo waProto.ContextInfo +} + +func (m *Message) ValidateMessageFields() (types.JID, error) { + + recipient, ok := helpers.ParseJID(m.Phone) + if !ok { + return types.NewJID("", types.DefaultUserServer), errors.New("Could not parse Phone") + } + + if m.ContextInfo.StanzaId != nil { + if m.ContextInfo.Participant == nil { + return types.NewJID("", types.DefaultUserServer), errors.New("Missing Participant in ContextInfo") + } + } + + if m.ContextInfo.Participant != nil { + if m.ContextInfo.StanzaId == nil { + return types.NewJID("", types.DefaultUserServer), errors.New("Missing StanzaId in ContextInfo") + } + } + + return recipient, nil +} From fd22edd331aec71ac1ca20000b10a34857ab314d Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 10:26:26 -0300 Subject: [PATCH 03/24] Removed duplicated function parseJID from wmiau.go --- handlers.go | 23 ++++++++++++----------- wmiau.go | 43 ++----------------------------------------- 2 files changed, 14 insertions(+), 52 deletions(-) diff --git a/handlers.go b/handlers.go index 215d72ce..5f3a9090 100644 --- a/handlers.go +++ b/handlers.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "time" + "wuzapi/internal/helpers" "wuzapi/message" "github.com/patrickmn/go-cache" @@ -1281,7 +1282,7 @@ func (s *server) SendButtons() http.HandlerFunc { return } - recipient, ok := parseJID(t.Phone) + recipient, ok := helpers.ParseJID(t.Phone) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) return @@ -1405,7 +1406,7 @@ func (s *server) SendList() http.HandlerFunc { s.Respond(w, r, http.StatusBadRequest, errors.New("missing Sections in Payload")) return } - recipient, ok := parseJID(t.Phone) + recipient, ok := helpers.ParseJID(t.Phone) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Phone")) return @@ -1623,7 +1624,7 @@ func (s *server) SendTemplate() http.HandlerFunc { return } - recipient, ok := parseJID(t.Phone) + recipient, ok := helpers.ParseJID(t.Phone) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) return @@ -2013,7 +2014,7 @@ func (s *server) GetUser() http.HandlerFunc { var jids []types.JID for _, arg := range t.Phone { - jid, ok := parseJID(arg) + jid, ok := helpers.ParseJID(arg) if !ok { return } @@ -2076,7 +2077,7 @@ func (s *server) GetAvatar() http.HandlerFunc { return } - jid, ok := parseJID(t.Phone) + jid, ok := helpers.ParseJID(t.Phone) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) return @@ -2181,7 +2182,7 @@ func (s *server) ChatPresence() http.HandlerFunc { return } - jid, ok := parseJID(t.Phone) + jid, ok := helpers.ParseJID(t.Phone) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) return @@ -2564,7 +2565,7 @@ func (s *server) React() http.HandlerFunc { return } - recipient, ok := parseJID(t.Phone) + recipient, ok := helpers.ParseJID(t.Phone) if !ok { log.Error().Msg(fmt.Sprintf("%s", err)) s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) @@ -2741,7 +2742,7 @@ func (s *server) GetGroupInfo() http.HandlerFunc { return } - group, ok := parseJID(t.GroupJID) + group, ok := helpers.ParseJID(t.GroupJID) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) return @@ -2794,7 +2795,7 @@ func (s *server) GetGroupInviteLink() http.HandlerFunc { return } - group, ok := parseJID(t.GroupJID) + group, ok := helpers.ParseJID(t.GroupJID) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) return @@ -2848,7 +2849,7 @@ func (s *server) SetGroupPhoto() http.HandlerFunc { return } - group, ok := parseJID(t.GroupJID) + group, ok := helpers.ParseJID(t.GroupJID) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) return @@ -2922,7 +2923,7 @@ func (s *server) SetGroupName() http.HandlerFunc { return } - group, ok := parseJID(t.GroupJID) + group, ok := helpers.ParseJID(t.GroupJID) if !ok { s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) return diff --git a/wmiau.go b/wmiau.go index dfbd18a9..2d892f06 100644 --- a/wmiau.go +++ b/wmiau.go @@ -15,6 +15,7 @@ import ( "strings" "sync/atomic" "time" + "wuzapi/internal/helpers" "wuzapi/webhook" "github.com/go-resty/resty/v2" @@ -108,46 +109,6 @@ func (s *server) connectOnStartup() { } } -func parseJID(arg string) (types.JID, bool) { - if arg == "" { - return types.NewJID("", types.DefaultUserServer), false - } - if arg[0] == '+' { - arg = arg[1:] - } - - // Basic only digit check for recipient phone number, we want to remove @server and .session - phonenumber := "" - phonenumber = strings.Split(arg, "@")[0] - phonenumber = strings.Split(phonenumber, ".")[0] - b := true - for _, c := range phonenumber { - if c < '0' || c > '9' { - b = false - break - } - } - if b == false { - log.Warn().Msg("Bad jid format, return empty") - recipient, _ := types.ParseJID("") - return recipient, false - } - - if !strings.ContainsRune(arg, '@') { - return types.NewJID(arg, types.DefaultUserServer), true - } else { - recipient, err := types.ParseJID(arg) - if err != nil { - log.Error().Err(err).Str("jid", arg).Msg("Invalid jid") - return recipient, false - } else if recipient.User == "" { - log.Error().Err(err).Str("jid", arg).Msg("Invalid jid. No server specified") - return recipient, false - } - return recipient, true - } -} - func (s *server) startClient(userID int, textjid string, token string, subscriptions []string) { log.Info().Str("userid", strconv.Itoa(userID)).Str("jid", textjid).Msg("Starting websocket connection to Whatsapp") @@ -187,7 +148,7 @@ func (s *server) startClient(userID int, textjid string, token string, subscript */ if textjid != "" { - jid, _ := parseJID(textjid) + jid, _ := helpers.ParseJID(textjid) // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead. //deviceStore, err := container.GetFirstDevice() deviceStore, err = container.GetDevice(jid) From 15714610445fee5de06a4690f19c665a4a517a2f Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 10:30:55 -0300 Subject: [PATCH 04/24] Moved Values class to internal types --- handlers.go | 135 +++++++++++++++----------------- helpers.go | 4 +- internal/types/message_types.go | 3 + internal/types/values.go | 9 +++ routes.go | 3 +- wmiau.go | 9 ++- 6 files changed, 86 insertions(+), 77 deletions(-) create mode 100644 internal/types/message_types.go create mode 100644 internal/types/values.go diff --git a/handlers.go b/handlers.go index 5f3a9090..54f16848 100644 --- a/handlers.go +++ b/handlers.go @@ -12,6 +12,7 @@ import ( "strings" "time" "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" "wuzapi/message" "github.com/patrickmn/go-cache" @@ -22,14 +23,6 @@ import ( "google.golang.org/protobuf/proto" ) -type Values struct { - m map[string]string -} - -func (v Values) Get(key string) string { - return v.m[key] -} - var messageTypes = []string{"Message", "ReadReceipt", "Presence", "HistorySync", "ChatPresence", "All"} func (s *server) authalice(next http.Handler) http.Handler { @@ -65,7 +58,7 @@ func (s *server) authalice(next http.Handler) http.Handler { return } userid, _ = strconv.Atoi(txtid) - v := Values{map[string]string{ + v := internalTypes.Values{map[string]string{ "Id": txtid, "Jid": jid, "Webhook": webhook, @@ -78,7 +71,7 @@ func (s *server) authalice(next http.Handler) http.Handler { } } else { ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) - userid, _ = strconv.Atoi(myuserinfo.(Values).Get("Id")) + userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) } if userid == 0 { @@ -123,7 +116,7 @@ func (s *server) auth(handler http.HandlerFunc) http.HandlerFunc { return } userid, _ = strconv.Atoi(txtid) - v := Values{map[string]string{ + v := internalTypes.Values{map[string]string{ "Id": txtid, "Jid": jid, "Webhook": webhook, @@ -136,7 +129,7 @@ func (s *server) auth(handler http.HandlerFunc) http.HandlerFunc { } } else { ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) - userid, _ = strconv.Atoi(myuserinfo.(Values).Get("Id")) + userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) } if userid == 0 { @@ -157,10 +150,10 @@ func (s *server) Connect() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - webhook := r.Context().Value("userinfo").(Values).Get("Webhook") - jid := r.Context().Value("userinfo").(Values).Get("Jid") - txtid := r.Context().Value("userinfo").(Values).Get("Id") - token := r.Context().Value("userinfo").(Values).Get("Token") + webhook := r.Context().Value("userinfo").(internalTypes.Values).Get("Webhook") + jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") userid, _ := strconv.Atoi(txtid) eventstring := "" @@ -239,9 +232,9 @@ func (s *server) Connect() http.HandlerFunc { func (s *server) Disconnect() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") - jid := r.Context().Value("userinfo").(Values).Get("Jid") - token := r.Context().Value("userinfo").(Values).Get("Token") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") + token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -286,7 +279,7 @@ func (s *server) GetWebhook() http.HandlerFunc { webhook := "" events := "" - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") rows, err := s.db.Query("SELECT webhook,events FROM users WHERE id=? LIMIT 1", txtid) if err != nil { @@ -327,8 +320,8 @@ func (s *server) SetWebhook() http.HandlerFunc { } return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") - token := r.Context().Value("userinfo").(Values).Get("Token") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") userid, _ := strconv.Atoi(txtid) decoder := json.NewDecoder(r.Body) @@ -364,7 +357,7 @@ func (s *server) SetWebhook() http.HandlerFunc { func (s *server) GetQR() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) code := "" @@ -416,8 +409,8 @@ func (s *server) GetQR() http.HandlerFunc { func (s *server) Logout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") - jid := r.Context().Value("userinfo").(Values).Get("Jid") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -463,7 +456,7 @@ func (s *server) GetStatus() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -490,14 +483,14 @@ func (s *server) SendDocument() http.HandlerFunc { type documentStruct struct { message.Message - Document string - FileName string - Id string + Document string + FileName string + Id string } return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) msgid := "" var resp whatsmeow.SendResponse @@ -606,14 +599,14 @@ func (s *server) SendAudio() http.HandlerFunc { type audioStruct struct { message.Message - Audio string - Caption string - Id string + Audio string + Caption string + Id string } return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) msgid := "" var resp whatsmeow.SendResponse @@ -721,14 +714,14 @@ func (s *server) SendImage() http.HandlerFunc { type imageStruct struct { message.Message - Image string - Caption string - Id string + Image string + Caption string + Id string } return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) msgid := "" var resp whatsmeow.SendResponse @@ -839,7 +832,7 @@ func (s *server) SendSticker() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) msgid := "" var resp whatsmeow.SendResponse @@ -951,7 +944,7 @@ func (s *server) SendVideo() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) msgid := "" var resp whatsmeow.SendResponse @@ -1056,14 +1049,14 @@ func (s *server) SendContact() http.HandlerFunc { type contactStruct struct { message.Message - Id string - Name string - Vcard string + Id string + Name string + Vcard string } return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -1143,15 +1136,15 @@ func (s *server) SendLocation() http.HandlerFunc { type locationStruct struct { message.Message - Id string - Name string - Latitude float64 - Longitude float64 + Id string + Name string + Latitude float64 + Longitude float64 } return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -1244,7 +1237,7 @@ func (s *server) SendButtons() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -1360,7 +1353,7 @@ func (s *server) SendList() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -1479,13 +1472,13 @@ func (s *server) SendMessage() http.HandlerFunc { type textStruct struct { message.Message - Body string - Id string + Body string + Id string } return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -1584,7 +1577,7 @@ func (s *server) SendTemplate() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -1931,7 +1924,7 @@ func (s *server) CheckUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -1991,7 +1984,7 @@ func (s *server) GetUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2056,7 +2049,7 @@ func (s *server) GetAvatar() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2119,7 +2112,7 @@ func (s *server) GetContacts() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2156,7 +2149,7 @@ func (s *server) ChatPresence() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2220,7 +2213,7 @@ func (s *server) DownloadImage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) mimetype := "" @@ -2300,7 +2293,7 @@ func (s *server) DownloadDocument() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) mimetype := "" @@ -2380,7 +2373,7 @@ func (s *server) DownloadVideo() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) mimetype := "" @@ -2460,7 +2453,7 @@ func (s *server) DownloadAudio() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) mimetype := "" @@ -2536,7 +2529,7 @@ func (s *server) React() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2632,7 +2625,7 @@ func (s *server) MarkRead() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2684,7 +2677,7 @@ func (s *server) ListGroups() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2726,7 +2719,7 @@ func (s *server) GetGroupInfo() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2779,7 +2772,7 @@ func (s *server) GetGroupInviteLink() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2833,7 +2826,7 @@ func (s *server) SetGroupPhoto() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { @@ -2907,7 +2900,7 @@ func (s *server) SetGroupName() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - txtid := r.Context().Value("userinfo").(Values).Get("Id") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") userid, _ := strconv.Atoi(txtid) if clientPointer[userid] == nil { diff --git a/helpers.go b/helpers.go index 2e2a5f78..7d7c194c 100644 --- a/helpers.go +++ b/helpers.go @@ -1,5 +1,7 @@ package main +import internalTypes "wuzapi/internal/types" + func Find(slice []string, val string) bool { for _, item := range slice { if item == val { @@ -12,6 +14,6 @@ func Find(slice []string, val string) bool { // Update entry in User map func updateUserInfo(values interface{}, field string, value string) interface{} { log.Debug().Str("field", field).Str("value", value).Msg("User info updated") - values.(Values).m[field] = value + values.(internalTypes.Values).M[field] = value return values } diff --git a/internal/types/message_types.go b/internal/types/message_types.go new file mode 100644 index 00000000..27264f5a --- /dev/null +++ b/internal/types/message_types.go @@ -0,0 +1,3 @@ +package internalTypes + +var MessageTypes = []string{"Message", "ReadReceipt", "Presence", "HistorySync", "ChatPresence", "All"} diff --git a/internal/types/values.go b/internal/types/values.go new file mode 100644 index 00000000..1ad49bb4 --- /dev/null +++ b/internal/types/values.go @@ -0,0 +1,9 @@ +package internalTypes + +type Values struct { + M map[string]string +} + +func (v Values) Get(key string) string { + return v.M[key] +} \ No newline at end of file diff --git a/routes.go b/routes.go index 6da34e48..c7e06fb2 100644 --- a/routes.go +++ b/routes.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "time" + internalTypes "wuzapi/internal/types" "github.com/justinas/alice" "github.com/rs/zerolog" @@ -39,7 +40,7 @@ func (s *server) routes() { Int("status", status). Int("size", size). Dur("duration", duration). - Str("userid", r.Context().Value("userinfo").(Values).Get("Id")). + Str("userid", r.Context().Value("userinfo").(internalTypes.Values).Get("Id")). Msg("Got API Request") })) c = c.Append(hlog.RemoteAddrHandler("ip")) diff --git a/wmiau.go b/wmiau.go index 2d892f06..a935c80f 100644 --- a/wmiau.go +++ b/wmiau.go @@ -16,6 +16,7 @@ import ( "sync/atomic" "time" "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" "wuzapi/webhook" "github.com/go-resty/resty/v2" @@ -69,7 +70,7 @@ func (s *server) connectOnStartup() { return } else { log.Info().Str("token", token).Msg("Connect to Whatsapp on startup") - v := Values{map[string]string{ + v := internalTypes.Values{map[string]string{ "Id": txtid, "Jid": jid, "Webhook": webhook, @@ -327,8 +328,8 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { if !found { log.Warn().Msg("No user info cached on pairing?") } else { - txtid := myuserinfo.(Values).Get("Id") - token := myuserinfo.(Values).Get("Token") + txtid := myuserinfo.(internalTypes.Values).Get("Id") + token := myuserinfo.(internalTypes.Values).Get("Token") v := updateUserInfo(myuserinfo, "Jid", fmt.Sprintf("%s", jid)) userinfocache.Set(token, v, cache.NoExpiration) log.Info().Str("jid", jid.String()).Str("userid", txtid).Str("token", token).Msg("User information set") @@ -549,7 +550,7 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { if !found { log.Warn().Str("token", mycli.token).Msg("Could not call webhook as there is no user for this token") } else { - webhookurl = myuserinfo.(Values).Get("Webhook") + webhookurl = myuserinfo.(internalTypes.Values).Get("Webhook") } if !Find(mycli.subscriptions, postmap["type"].(string)) && !Find(mycli.subscriptions, "All") { From 433a09c2e6bfab47998fd4ed4805244139e0c40c Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 10:34:28 -0300 Subject: [PATCH 05/24] Moved helpers into internal/helpers --- handlers.go | 16 ++++++++-------- helpers.go => internal/helpers/helpers.go | 10 +++++++--- wmiau.go | 12 ++++++------ 3 files changed, 21 insertions(+), 17 deletions(-) rename helpers.go => internal/helpers/helpers.go (66%) diff --git a/handlers.go b/handlers.go index 54f16848..3a701a14 100644 --- a/handlers.go +++ b/handlers.go @@ -58,7 +58,7 @@ func (s *server) authalice(next http.Handler) http.Handler { return } userid, _ = strconv.Atoi(txtid) - v := internalTypes.Values{map[string]string{ + v := internalTypes.Values{M: map[string]string{ "Id": txtid, "Jid": jid, "Webhook": webhook, @@ -116,7 +116,7 @@ func (s *server) auth(handler http.HandlerFunc) http.HandlerFunc { return } userid, _ = strconv.Atoi(txtid) - v := internalTypes.Values{map[string]string{ + v := internalTypes.Values{M: map[string]string{ "Id": txtid, "Jid": jid, "Webhook": webhook, @@ -173,16 +173,16 @@ func (s *server) Connect() http.HandlerFunc { var subscribedEvents []string if len(t.Subscribe) < 1 { - if !Find(subscribedEvents, "All") { + if !helpers.Find(subscribedEvents, "All") { subscribedEvents = append(subscribedEvents, "All") } } else { for _, arg := range t.Subscribe { - if !Find(messageTypes, arg) { + if !helpers.Find(messageTypes, arg) { log.Warn().Str("Type", arg).Msg("Message type discarded") continue } - if !Find(subscribedEvents, arg) { + if !helpers.Find(subscribedEvents, arg) { subscribedEvents = append(subscribedEvents, arg) } } @@ -193,7 +193,7 @@ func (s *server) Connect() http.HandlerFunc { log.Warn().Msg("Could not set events in users table") } log.Info().Str("events", eventstring).Msg("Setting subscribed events") - v := updateUserInfo(r.Context().Value("userinfo"), "Events", eventstring) + v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Events", eventstring) userinfocache.Set(token, v, cache.NoExpiration) log.Info().Str("jid", jid).Msg("Attempt to connect") @@ -249,7 +249,7 @@ func (s *server) Disconnect() http.HandlerFunc { if err != nil { log.Warn().Str("userid", txtid).Msg("Could not set events in users table") } - v := updateUserInfo(r.Context().Value("userinfo"), "Events", "") + v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Events", "") userinfocache.Set(token, v, cache.NoExpiration) response := map[string]interface{}{"Details": "Disconnected"} @@ -339,7 +339,7 @@ func (s *server) SetWebhook() http.HandlerFunc { return } - v := updateUserInfo(r.Context().Value("userinfo"), "Webhook", webhook) + v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Webhook", webhook) userinfocache.Set(token, v, cache.NoExpiration) response := map[string]interface{}{"webhook": webhook} diff --git a/helpers.go b/internal/helpers/helpers.go similarity index 66% rename from helpers.go rename to internal/helpers/helpers.go index 7d7c194c..ae92f32d 100644 --- a/helpers.go +++ b/internal/helpers/helpers.go @@ -1,6 +1,10 @@ -package main +package helpers -import internalTypes "wuzapi/internal/types" +import ( + internalTypes "wuzapi/internal/types" + + "github.com/rs/zerolog/log" +) func Find(slice []string, val string) bool { for _, item := range slice { @@ -12,7 +16,7 @@ func Find(slice []string, val string) bool { } // Update entry in User map -func updateUserInfo(values interface{}, field string, value string) interface{} { +func UpdateUserInfo(values interface{}, field string, value string) interface{} { log.Debug().Str("field", field).Str("value", value).Msg("User info updated") values.(internalTypes.Values).M[field] = value return values diff --git a/wmiau.go b/wmiau.go index a935c80f..4780735d 100644 --- a/wmiau.go +++ b/wmiau.go @@ -70,7 +70,7 @@ func (s *server) connectOnStartup() { return } else { log.Info().Str("token", token).Msg("Connect to Whatsapp on startup") - v := internalTypes.Values{map[string]string{ + v := internalTypes.Values{M: map[string]string{ "Id": txtid, "Jid": jid, "Webhook": webhook, @@ -84,16 +84,16 @@ func (s *server) connectOnStartup() { var subscribedEvents []string if len(eventarray) < 1 { - if !Find(subscribedEvents, "All") { + if !helpers.Find(subscribedEvents, "All") { subscribedEvents = append(subscribedEvents, "All") } } else { for _, arg := range eventarray { - if !Find(messageTypes, arg) { + if !helpers.Find(messageTypes, arg) { log.Warn().Str("Type", arg).Msg("Message type discarded") continue } - if !Find(subscribedEvents, arg) { + if !helpers.Find(subscribedEvents, arg) { subscribedEvents = append(subscribedEvents, arg) } } @@ -330,7 +330,7 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { } else { txtid := myuserinfo.(internalTypes.Values).Get("Id") token := myuserinfo.(internalTypes.Values).Get("Token") - v := updateUserInfo(myuserinfo, "Jid", fmt.Sprintf("%s", jid)) + v := helpers.UpdateUserInfo(myuserinfo, "Jid", fmt.Sprintf("%s", jid)) userinfocache.Set(token, v, cache.NoExpiration) log.Info().Str("jid", jid.String()).Str("userid", txtid).Str("token", token).Msg("User information set") } @@ -553,7 +553,7 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { webhookurl = myuserinfo.(internalTypes.Values).Get("Webhook") } - if !Find(mycli.subscriptions, postmap["type"].(string)) && !Find(mycli.subscriptions, "All") { + if !helpers.Find(mycli.subscriptions, postmap["type"].(string)) && !helpers.Find(mycli.subscriptions, "All") { log.Warn().Str("type", postmap["type"].(string)).Msg("Skipping webhook. Not subscribed for this type") return } From 36e3d46544a8691ec5d097e49917e3dff552c988 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 10:45:40 -0300 Subject: [PATCH 06/24] Move Myclient into internal helpers --- internal/helpers/wmiau.go | 335 +++++++++++++++++++++++++++++++++++++ wmiau.go | 341 ++------------------------------------ 2 files changed, 350 insertions(+), 326 deletions(-) diff --git a/internal/helpers/wmiau.go b/internal/helpers/wmiau.go index 216d54e8..cee1a456 100644 --- a/internal/helpers/wmiau.go +++ b/internal/helpers/wmiau.go @@ -1,10 +1,25 @@ package helpers import ( + "database/sql" + "encoding/json" + "fmt" + "mime" + "os" + "path/filepath" + "strconv" "strings" + "sync/atomic" + internalTypes "wuzapi/internal/types" + "wuzapi/webhook" + "github.com/go-resty/resty/v2" + "github.com/patrickmn/go-cache" "github.com/rs/zerolog/log" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" ) func ParseJID(arg string) (types.JID, bool) { @@ -46,3 +61,323 @@ func ParseJID(arg string) (types.JID, bool) { return recipient, true } } + +var historySyncID int32 + +type MyClient struct { + WAClient *whatsmeow.Client + EventHandlerID uint32 + UserID int + Token string + Subscriptions []string + Db *sql.DB + UserInfoCache *cache.Cache + KillChannel map[int](chan bool) + ClientHttp map[int]*resty.Client +} + +func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { + txtid := strconv.Itoa(mycli.UserID) + postmap := make(map[string]interface{}) + postmap["event"] = rawEvt + dowebhook := 0 + path := "" + + ex, err := os.Executable() + if err != nil { + panic(err) + } + exPath := filepath.Dir(ex) + + switch evt := rawEvt.(type) { + case *events.AppStateSyncComplete: + if len(mycli.WAClient.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock { + err := mycli.WAClient.SendPresence(types.PresenceAvailable) + if err != nil { + log.Warn().Err(err).Msg("Failed to send available presence") + } else { + log.Info().Msg("Marked self as available") + } + } + case *events.Connected, *events.PushNameSetting: + if len(mycli.WAClient.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 := mycli.WAClient.SendPresence(types.PresenceAvailable) + if err != nil { + log.Warn().Err(err).Msg("Failed to send available presence") + } else { + log.Info().Msg("Marked self as available") + } + sqlStmt := `UPDATE users SET connected=1 WHERE id=?` + _, err = mycli.Db.Exec(sqlStmt, mycli.UserID) + if err != nil { + log.Error().Err(err).Msg(sqlStmt) + return + } + case *events.PairSuccess: + log.Info().Str("userid", strconv.Itoa(mycli.UserID)).Str("token", mycli.Token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success") + jid := evt.ID + sqlStmt := `UPDATE users SET jid=? WHERE id=?` + _, err := mycli.Db.Exec(sqlStmt, jid, mycli.UserID) + if err != nil { + log.Error().Err(err).Msg(sqlStmt) + return + } + + myuserinfo, found := mycli.UserInfoCache.Get(mycli.Token) + if !found { + log.Warn().Msg("No user info cached on pairing?") + } else { + txtid := myuserinfo.(internalTypes.Values).Get("Id") + token := myuserinfo.(internalTypes.Values).Get("Token") + v := UpdateUserInfo(myuserinfo, "Jid", fmt.Sprintf("%s", jid)) + mycli.UserInfoCache.Set(token, v, cache.NoExpiration) + log.Info().Str("jid", jid.String()).Str("userid", txtid).Str("token", token).Msg("User information set") + } + case *events.StreamReplaced: + log.Info().Msg("Received StreamReplaced event") + return + case *events.Message: + postmap["type"] = "Message" + dowebhook = 1 + 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") + } + + log.Info().Str("id", evt.Info.ID).Str("source", evt.Info.SourceString()).Str("parts", strings.Join(metaParts, ", ")).Msg("Message Received") + + // try to get Image if any + img := evt.Message.GetImageMessage() + if img != nil { + + // check/creates user directory for files + userDirectory := fmt.Sprintf("%s/files/user_%s", exPath, txtid) + _, err := os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + log.Error().Err(errDir).Msg("Could not create user directory") + return + } + } + + data, err := mycli.WAClient.Download(img) + if err != nil { + log.Error().Err(err).Msg("Failed to download image") + return + } + exts, _ := mime.ExtensionsByType(img.GetMimetype()) + path = fmt.Sprintf("%s/%s%s", userDirectory, evt.Info.ID, exts[0]) + err = os.WriteFile(path, data, 0600) + if err != nil { + log.Error().Err(err).Msg("Failed to save image") + return + } + log.Info().Str("path", path).Msg("Image saved") + } + + // try to get Audio if any + audio := evt.Message.GetAudioMessage() + if audio != nil { + + // check/creates user directory for files + userDirectory := fmt.Sprintf("%s/files/user_%s", exPath, txtid) + _, err := os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + log.Error().Err(errDir).Msg("Could not create user directory") + return + } + } + + data, err := mycli.WAClient.Download(audio) + if err != nil { + log.Error().Err(err).Msg("Failed to download audio") + return + } + exts, _ := mime.ExtensionsByType(audio.GetMimetype()) + path = fmt.Sprintf("%s/%s%s", userDirectory, evt.Info.ID, exts[0]) + err = os.WriteFile(path, data, 0600) + if err != nil { + log.Error().Err(err).Msg("Failed to save audio") + return + } + log.Info().Str("path", path).Msg("Audio saved") + } + + // try to get Document if any + document := evt.Message.GetDocumentMessage() + if document != nil { + + // check/creates user directory for files + userDirectory := fmt.Sprintf("%s/files/user_%s", exPath, txtid) + _, err := os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + log.Error().Err(errDir).Msg("Could not create user directory") + return + } + } + + data, err := mycli.WAClient.Download(document) + if err != nil { + log.Error().Err(err).Msg("Failed to download document") + return + } + extension := "" + exts, err := mime.ExtensionsByType(document.GetMimetype()) + if err != nil { + extension = exts[0] + } else { + filename := document.FileName + extension = filepath.Ext(*filename) + } + path = fmt.Sprintf("%s/%s%s", userDirectory, evt.Info.ID, extension) + err = os.WriteFile(path, data, 0600) + if err != nil { + log.Error().Err(err).Msg("Failed to save document") + return + } + log.Info().Str("path", path).Msg("Document saved") + } + case *events.Receipt: + postmap["type"] = "ReadReceipt" + dowebhook = 1 + if evt.Type == events.ReceiptTypeRead || evt.Type == events.ReceiptTypeReadSelf { + log.Info().Strs("id", evt.MessageIDs).Str("source", evt.SourceString()).Str("timestamp", fmt.Sprintf("%d", evt.Timestamp)).Msg("Message was read") + if evt.Type == events.ReceiptTypeRead { + postmap["state"] = "Read" + } else { + postmap["state"] = "ReadSelf" + } + } else if evt.Type == events.ReceiptTypeDelivered { + postmap["state"] = "Delivered" + log.Info().Str("id", evt.MessageIDs[0]).Str("source", evt.SourceString()).Str("timestamp", fmt.Sprintf("%d", evt.Timestamp)).Msg("Message delivered") + } else { + // Discard webhooks for inactive or other delivery types + return + } + case *events.Presence: + postmap["type"] = "Presence" + dowebhook = 1 + if evt.Unavailable { + postmap["state"] = "offline" + if evt.LastSeen.IsZero() { + log.Info().Str("from", evt.From.String()).Msg("User is now offline") + } else { + log.Info().Str("from", evt.From.String()).Str("lastSeen", fmt.Sprintf("%d", evt.LastSeen)).Msg("User is now offline") + } + } else { + postmap["state"] = "online" + log.Info().Str("from", evt.From.String()).Msg("User is now online") + } + case *events.HistorySync: + postmap["type"] = "HistorySync" + dowebhook = 1 + + // check/creates user directory for files + userDirectory := fmt.Sprintf("%s/files/user_%s", exPath, txtid) + _, err := os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + log.Error().Err(errDir).Msg("Could not create user directory") + return + } + } + + id := atomic.AddInt32(&historySyncID, 1) + fileName := fmt.Sprintf("%s/history-%d.json", userDirectory, id) + file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + log.Error().Err(err).Msg("Failed to open file to write history sync") + return + } + enc := json.NewEncoder(file) + enc.SetIndent("", " ") + err = enc.Encode(evt.Data) + if err != nil { + log.Error().Err(err).Msg("Failed to write history sync") + return + } + log.Info().Str("filename", fileName).Msg("Wrote history sync") + _ = file.Close() + case *events.AppState: + log.Info().Str("index", fmt.Sprintf("%+v", evt.Index)).Str("actionValue", fmt.Sprintf("%+v", evt.SyncActionValue)).Msg("App state event received") + case *events.LoggedOut: + log.Info().Str("reason", evt.Reason.String()).Msg("Logged out") + mycli.KillChannel[mycli.UserID] <- true + sqlStmt := `UPDATE users SET connected=0 WHERE id=?` + _, err := mycli.Db.Exec(sqlStmt, mycli.UserID) + if err != nil { + log.Error().Err(err).Msg(sqlStmt) + return + } + case *events.ChatPresence: + postmap["type"] = "ChatPresence" + dowebhook = 1 + log.Info().Str("state", fmt.Sprintf("%s", evt.State)).Str("media", fmt.Sprintf("%s", evt.Media)).Str("chat", evt.MessageSource.Chat.String()).Str("sender", evt.MessageSource.Sender.String()).Msg("Chat Presence received") + case *events.CallOffer: + log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call offer") + case *events.CallAccept: + log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call accept") + case *events.CallTerminate: + log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call terminate") + case *events.CallOfferNotice: + log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call offer notice") + case *events.CallRelayLatency: + log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call relay latency") + default: + log.Warn().Str("event", fmt.Sprintf("%+v", evt)).Msg("Unhandled event") + } + + if dowebhook == 1 { + // call webhook + webhookurl := "" + myuserinfo, found := mycli.UserInfoCache.Get(mycli.Token) + if !found { + log.Warn().Str("token", mycli.Token).Msg("Could not call webhook as there is no user for this token") + } else { + webhookurl = myuserinfo.(internalTypes.Values).Get("Webhook") + } + + if !Find(mycli.Subscriptions, postmap["type"].(string)) && !Find(mycli.Subscriptions, "All") { + log.Warn().Str("type", postmap["type"].(string)).Msg("Skipping webhook. Not subscribed for this type") + return + } + + if webhookurl != "" { + log.Info().Str("url", webhookurl).Msg("Calling webhook") + values, _ := json.Marshal(postmap) + webhook := webhook.Webhook{ClientHttp: mycli.ClientHttp} + if path == "" { + data := make(map[string]string) + data["jsonData"] = string(values) + data["token"] = mycli.Token + go webhook.CallHook(webhookurl, data, mycli.UserID) + } else { + data := make(map[string]string) + data["jsonData"] = string(values) + data["token"] = mycli.Token + go webhook.CallHookFile(webhookurl, data, mycli.UserID, path) + } + } else { + log.Warn().Str("userid", strconv.Itoa(mycli.UserID)).Msg("No webhook set for user") + } + } +} diff --git a/wmiau.go b/wmiau.go index 4780735d..4e837efe 100644 --- a/wmiau.go +++ b/wmiau.go @@ -3,35 +3,27 @@ package main import ( "context" "crypto/tls" - "database/sql" "encoding/base64" - "encoding/json" "errors" "fmt" - "mime" "os" - "path/filepath" "strconv" "strings" - "sync/atomic" "time" "wuzapi/internal/helpers" internalTypes "wuzapi/internal/types" - "wuzapi/webhook" "github.com/go-resty/resty/v2" "github.com/mdp/qrterminal/v3" "github.com/patrickmn/go-cache" "github.com/skip2/go-qrcode" "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/appstate" waProto "go.mau.fi/whatsmeow/binary/proto" "go.mau.fi/whatsmeow/store" _ "modernc.org/sqlite" // "go.mau.fi/whatsmeow/store/sqlstore" - "go.mau.fi/whatsmeow/types" - "go.mau.fi/whatsmeow/types/events" + waLog "go.mau.fi/whatsmeow/util/log" //"google.golang.org/protobuf/proto" ) @@ -41,15 +33,6 @@ var clientPointer = make(map[int]*whatsmeow.Client) var clientHttp = make(map[int]*resty.Client) var historySyncID int32 -type MyClient struct { - WAClient *whatsmeow.Client - eventHandlerID uint32 - userID int - token string - subscriptions []string - db *sql.DB -} - // Connects to Whatsapp Websocket on server startup if last state was connected func (s *server) connectOnStartup() { rows, err := s.db.Query("SELECT id,token,jid,webhook,events FROM users WHERE connected=1") @@ -181,8 +164,20 @@ func (s *server) startClient(userID int, textjid string, token string, subscript client = whatsmeow.NewClient(deviceStore, nil) } clientPointer[userID] = client - mycli := MyClient{client, 1, userID, token, subscriptions, s.db} - mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler) + + mycli := helpers.MyClient{ + WAClient: client, + EventHandlerID: 1, + UserID: userID, + Token: token, + Subscriptions: subscriptions, + Db: s.db, + UserInfoCache: userinfocache, + KillChannel: killchannel, + ClientHttp: clientHttp, + } + + mycli.EventHandlerID = mycli.WAClient.AddEventHandler(mycli.MyEventHandler) clientHttp[userID] = resty.New() clientHttp[userID].SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) if *waDebug == "DEBUG" { @@ -272,309 +267,3 @@ func (s *server) startClient(userID int, textjid string, token string, subscript } } } - -func (mycli *MyClient) myEventHandler(rawEvt interface{}) { - txtid := strconv.Itoa(mycli.userID) - postmap := make(map[string]interface{}) - postmap["event"] = rawEvt - dowebhook := 0 - path := "" - - ex, err := os.Executable() - if err != nil { - panic(err) - } - exPath := filepath.Dir(ex) - - switch evt := rawEvt.(type) { - case *events.AppStateSyncComplete: - if len(mycli.WAClient.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock { - err := mycli.WAClient.SendPresence(types.PresenceAvailable) - if err != nil { - log.Warn().Err(err).Msg("Failed to send available presence") - } else { - log.Info().Msg("Marked self as available") - } - } - case *events.Connected, *events.PushNameSetting: - if len(mycli.WAClient.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 := mycli.WAClient.SendPresence(types.PresenceAvailable) - if err != nil { - log.Warn().Err(err).Msg("Failed to send available presence") - } else { - log.Info().Msg("Marked self as available") - } - sqlStmt := `UPDATE users SET connected=1 WHERE id=?` - _, err = mycli.db.Exec(sqlStmt, mycli.userID) - if err != nil { - log.Error().Err(err).Msg(sqlStmt) - return - } - case *events.PairSuccess: - log.Info().Str("userid", strconv.Itoa(mycli.userID)).Str("token", mycli.token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success") - jid := evt.ID - sqlStmt := `UPDATE users SET jid=? WHERE id=?` - _, err := mycli.db.Exec(sqlStmt, jid, mycli.userID) - if err != nil { - log.Error().Err(err).Msg(sqlStmt) - return - } - - myuserinfo, found := userinfocache.Get(mycli.token) - if !found { - log.Warn().Msg("No user info cached on pairing?") - } else { - txtid := myuserinfo.(internalTypes.Values).Get("Id") - token := myuserinfo.(internalTypes.Values).Get("Token") - v := helpers.UpdateUserInfo(myuserinfo, "Jid", fmt.Sprintf("%s", jid)) - userinfocache.Set(token, v, cache.NoExpiration) - log.Info().Str("jid", jid.String()).Str("userid", txtid).Str("token", token).Msg("User information set") - } - case *events.StreamReplaced: - log.Info().Msg("Received StreamReplaced event") - return - case *events.Message: - postmap["type"] = "Message" - dowebhook = 1 - 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") - } - - log.Info().Str("id", evt.Info.ID).Str("source", evt.Info.SourceString()).Str("parts", strings.Join(metaParts, ", ")).Msg("Message Received") - - // try to get Image if any - img := evt.Message.GetImageMessage() - if img != nil { - - // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", exPath, txtid) - _, err := os.Stat(userDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(userDirectory, 0751) - if errDir != nil { - log.Error().Err(errDir).Msg("Could not create user directory") - return - } - } - - data, err := mycli.WAClient.Download(img) - if err != nil { - log.Error().Err(err).Msg("Failed to download image") - return - } - exts, _ := mime.ExtensionsByType(img.GetMimetype()) - path = fmt.Sprintf("%s/%s%s", userDirectory, evt.Info.ID, exts[0]) - err = os.WriteFile(path, data, 0600) - if err != nil { - log.Error().Err(err).Msg("Failed to save image") - return - } - log.Info().Str("path", path).Msg("Image saved") - } - - // try to get Audio if any - audio := evt.Message.GetAudioMessage() - if audio != nil { - - // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", exPath, txtid) - _, err := os.Stat(userDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(userDirectory, 0751) - if errDir != nil { - log.Error().Err(errDir).Msg("Could not create user directory") - return - } - } - - data, err := mycli.WAClient.Download(audio) - if err != nil { - log.Error().Err(err).Msg("Failed to download audio") - return - } - exts, _ := mime.ExtensionsByType(audio.GetMimetype()) - path = fmt.Sprintf("%s/%s%s", userDirectory, evt.Info.ID, exts[0]) - err = os.WriteFile(path, data, 0600) - if err != nil { - log.Error().Err(err).Msg("Failed to save audio") - return - } - log.Info().Str("path", path).Msg("Audio saved") - } - - // try to get Document if any - document := evt.Message.GetDocumentMessage() - if document != nil { - - // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", exPath, txtid) - _, err := os.Stat(userDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(userDirectory, 0751) - if errDir != nil { - log.Error().Err(errDir).Msg("Could not create user directory") - return - } - } - - data, err := mycli.WAClient.Download(document) - if err != nil { - log.Error().Err(err).Msg("Failed to download document") - return - } - extension := "" - exts, err := mime.ExtensionsByType(document.GetMimetype()) - if err != nil { - extension = exts[0] - } else { - filename := document.FileName - extension = filepath.Ext(*filename) - } - path = fmt.Sprintf("%s/%s%s", userDirectory, evt.Info.ID, extension) - err = os.WriteFile(path, data, 0600) - if err != nil { - log.Error().Err(err).Msg("Failed to save document") - return - } - log.Info().Str("path", path).Msg("Document saved") - } - case *events.Receipt: - postmap["type"] = "ReadReceipt" - dowebhook = 1 - if evt.Type == events.ReceiptTypeRead || evt.Type == events.ReceiptTypeReadSelf { - log.Info().Strs("id", evt.MessageIDs).Str("source", evt.SourceString()).Str("timestamp", fmt.Sprintf("%d", evt.Timestamp)).Msg("Message was read") - if evt.Type == events.ReceiptTypeRead { - postmap["state"] = "Read" - } else { - postmap["state"] = "ReadSelf" - } - } else if evt.Type == events.ReceiptTypeDelivered { - postmap["state"] = "Delivered" - log.Info().Str("id", evt.MessageIDs[0]).Str("source", evt.SourceString()).Str("timestamp", fmt.Sprintf("%d", evt.Timestamp)).Msg("Message delivered") - } else { - // Discard webhooks for inactive or other delivery types - return - } - case *events.Presence: - postmap["type"] = "Presence" - dowebhook = 1 - if evt.Unavailable { - postmap["state"] = "offline" - if evt.LastSeen.IsZero() { - log.Info().Str("from", evt.From.String()).Msg("User is now offline") - } else { - log.Info().Str("from", evt.From.String()).Str("lastSeen", fmt.Sprintf("%d", evt.LastSeen)).Msg("User is now offline") - } - } else { - postmap["state"] = "online" - log.Info().Str("from", evt.From.String()).Msg("User is now online") - } - case *events.HistorySync: - postmap["type"] = "HistorySync" - dowebhook = 1 - - // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", exPath, txtid) - _, err := os.Stat(userDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(userDirectory, 0751) - if errDir != nil { - log.Error().Err(errDir).Msg("Could not create user directory") - return - } - } - - id := atomic.AddInt32(&historySyncID, 1) - fileName := fmt.Sprintf("%s/history-%d.json", userDirectory, id) - file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - log.Error().Err(err).Msg("Failed to open file to write history sync") - return - } - enc := json.NewEncoder(file) - enc.SetIndent("", " ") - err = enc.Encode(evt.Data) - if err != nil { - log.Error().Err(err).Msg("Failed to write history sync") - return - } - log.Info().Str("filename", fileName).Msg("Wrote history sync") - _ = file.Close() - case *events.AppState: - log.Info().Str("index", fmt.Sprintf("%+v", evt.Index)).Str("actionValue", fmt.Sprintf("%+v", evt.SyncActionValue)).Msg("App state event received") - case *events.LoggedOut: - log.Info().Str("reason", evt.Reason.String()).Msg("Logged out") - killchannel[mycli.userID] <- true - sqlStmt := `UPDATE users SET connected=0 WHERE id=?` - _, err := mycli.db.Exec(sqlStmt, mycli.userID) - if err != nil { - log.Error().Err(err).Msg(sqlStmt) - return - } - case *events.ChatPresence: - postmap["type"] = "ChatPresence" - dowebhook = 1 - log.Info().Str("state", fmt.Sprintf("%s", evt.State)).Str("media", fmt.Sprintf("%s", evt.Media)).Str("chat", evt.MessageSource.Chat.String()).Str("sender", evt.MessageSource.Sender.String()).Msg("Chat Presence received") - case *events.CallOffer: - log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call offer") - case *events.CallAccept: - log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call accept") - case *events.CallTerminate: - log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call terminate") - case *events.CallOfferNotice: - log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call offer notice") - case *events.CallRelayLatency: - log.Info().Str("event", fmt.Sprintf("%+v", evt)).Msg("Got call relay latency") - default: - log.Warn().Str("event", fmt.Sprintf("%+v", evt)).Msg("Unhandled event") - } - - if dowebhook == 1 { - // call webhook - webhookurl := "" - myuserinfo, found := userinfocache.Get(mycli.token) - if !found { - log.Warn().Str("token", mycli.token).Msg("Could not call webhook as there is no user for this token") - } else { - webhookurl = myuserinfo.(internalTypes.Values).Get("Webhook") - } - - if !helpers.Find(mycli.subscriptions, postmap["type"].(string)) && !helpers.Find(mycli.subscriptions, "All") { - log.Warn().Str("type", postmap["type"].(string)).Msg("Skipping webhook. Not subscribed for this type") - return - } - - if webhookurl != "" { - log.Info().Str("url", webhookurl).Msg("Calling webhook") - values, _ := json.Marshal(postmap) - webhook := webhook.Webhook{ClientHttp: clientHttp} - if path == "" { - data := make(map[string]string) - data["jsonData"] = string(values) - data["token"] = mycli.token - go webhook.CallHook(webhookurl, data, mycli.userID) - } else { - data := make(map[string]string) - data["jsonData"] = string(values) - data["token"] = mycli.token - go webhook.CallHookFile(webhookurl, data, mycli.userID, path) - } - } else { - log.Warn().Str("userid", strconv.Itoa(mycli.userID)).Msg("No webhook set for user") - } - } -} From 7f8237bb9279acbdc72301510d9a529f16bc84b3 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 10:47:24 -0300 Subject: [PATCH 07/24] Implemented ControllerBase --- .../controller_base/controller_base.go | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 controllers/controller_base/controller_base.go diff --git a/controllers/controller_base/controller_base.go b/controllers/controller_base/controller_base.go new file mode 100644 index 00000000..2c491b36 --- /dev/null +++ b/controllers/controller_base/controller_base.go @@ -0,0 +1,419 @@ +package controllerBase + +import ( + "context" + "crypto/tls" + "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "strconv" + "strings" + "time" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + + "github.com/go-resty/resty/v2" + "github.com/gorilla/mux" + "github.com/mdp/qrterminal/v3" + "github.com/patrickmn/go-cache" + "github.com/rs/zerolog/log" + "github.com/skip2/go-qrcode" + "go.mau.fi/whatsmeow" + waProto "go.mau.fi/whatsmeow/binary/proto" + "go.mau.fi/whatsmeow/store" + "go.mau.fi/whatsmeow/store/sqlstore" + waLog "go.mau.fi/whatsmeow/util/log" +) + +type Controller struct { + Db *sql.DB + Router *mux.Router + ExPath string + ClientPointer map[int]*whatsmeow.Client + KillChannel map[int](chan bool) + UserInfoCache *cache.Cache + Container *sqlstore.Container + WaDebug *string + ClientHttp map[int]*resty.Client + LogType *string +} + +// Writes JSON response to API clients +func (s *Controller) Respond(w http.ResponseWriter, r *http.Request, status int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + dataenvelope := map[string]interface{}{"code": status} + if err, ok := data.(error); ok { + dataenvelope["error"] = err.Error() + dataenvelope["success"] = false + } else { + mydata := make(map[string]interface{}) + err = json.Unmarshal([]byte(data.(string)), &mydata) + if err != nil { + log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Error unmarshalling JSON") + } + dataenvelope["data"] = mydata + dataenvelope["success"] = true + } + data = dataenvelope + + if err := json.NewEncoder(w).Encode(data); err != nil { + panic("respond: " + err.Error()) + } +} + +// Connects to Whatsapp Websocket on server startup if last state was connected +func (s *Controller) ConnectOnStartup() { + rows, err := s.Db.Query("SELECT id,token,jid,webhook,events FROM users WHERE connected=1") + if err != nil { + log.Error().Err(err).Msg("DB Problem") + return + } + defer rows.Close() + for rows.Next() { + txtid := "" + token := "" + jid := "" + webhook := "" + events := "" + err = rows.Scan(&txtid, &token, &jid, &webhook, &events) + if err != nil { + log.Error().Err(err).Msg("DB Problem") + return + } else { + log.Info().Str("token", token).Msg("Connect to Whatsapp on startup") + v := internalTypes.Values{M: map[string]string{ + "Id": txtid, + "Jid": jid, + "Webhook": webhook, + "Token": token, + "Events": events, + }} + s.UserInfoCache.Set(token, v, cache.NoExpiration) + userid, _ := strconv.Atoi(txtid) + // Gets and set subscription to webhook events + eventarray := strings.Split(events, ",") + + var subscribedEvents []string + if len(eventarray) < 1 { + if !helpers.Find(subscribedEvents, "All") { + subscribedEvents = append(subscribedEvents, "All") + } + } else { + for _, arg := range eventarray { + if !helpers.Find(internalTypes.MessageTypes, arg) { + log.Warn().Str("Type", arg).Msg("Message type discarded") + continue + } + if !helpers.Find(subscribedEvents, arg) { + subscribedEvents = append(subscribedEvents, arg) + } + } + } + eventstring := strings.Join(subscribedEvents, ",") + log.Info().Str("events", eventstring).Str("jid", jid).Msg("Attempt to connect") + s.KillChannel[userid] = make(chan bool) + go s.StartClient(userid, jid, token, subscribedEvents) + } + } + err = rows.Err() + if err != nil { + log.Error().Err(err).Msg("DB Problem") + } +} + +func (s *Controller) StartClient(userID int, textjid string, token string, subscriptions []string) { + + log.Info().Str("userid", strconv.Itoa(userID)).Str("jid", textjid).Msg("Starting websocket connection to Whatsapp") + + var deviceStore *store.Device + var err error + + if s.ClientPointer[userID] != nil { + isConnected := s.ClientPointer[userID].IsConnected() + if isConnected == true { + return + } + } + + /* container is initialized on main to have just one connection and avoid sqlite locks + + dbDirectory := "dbdata" + _, err = os.Stat(dbDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(dbDirectory, 0751) + if errDir != nil { + panic("Could not create dbdata directory") + } + } + + var container *sqlstore.Container + + if(*waDebug!="") { + dbLog := waLog.Stdout("Database", *waDebug, true) + container, err = sqlstore.New("sqlite", "file:./dbdata/main.db?_foreign_keys=on", dbLog) + } else { + container, err = sqlstore.New("sqlite", "file:./dbdata/main.db?_foreign_keys=on", nil) + } + if err != nil { + panic(err) + } + */ + + if textjid != "" { + jid, _ := helpers.ParseJID(textjid) + // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead. + //deviceStore, err := container.GetFirstDevice() + deviceStore, err = s.Container.GetDevice(jid) + if err != nil { + panic(err) + } + } else { + log.Warn().Msg("No jid found. Creating new device") + deviceStore = s.Container.NewDevice() + } + + if deviceStore == nil { + log.Warn().Msg("No store found. Creating new one") + deviceStore = s.Container.NewDevice() + } + + //store.CompanionProps.PlatformType = waProto.CompanionProps_CHROME.Enum() + //store.CompanionProps.Os = proto.String("Mac OS") + + osName := "Mac OS 10" + store.DeviceProps.PlatformType = waProto.DeviceProps_UNKNOWN.Enum() + store.DeviceProps.Os = &osName + + var client *whatsmeow.Client + if *s.WaDebug == "DEBUG" { + client = whatsmeow.NewClient( + deviceStore, + waLog.Stdout("Client", "DEBUG", true), + ) + s.ClientHttp[userID].SetDebug(true) + } else { + s.ClientHttp[userID].SetDebug(false) + client = whatsmeow.NewClient(deviceStore, nil) + } + + s.ClientPointer[userID] = client + + mycli := helpers.MyClient{ + WAClient: client, + EventHandlerID: 1, + UserID: userID, + Token: token, + Subscriptions: subscriptions, + UserInfoCache: s.UserInfoCache, + KillChannel: s.KillChannel, + Db: s.Db, + } + mycli.EventHandlerID = mycli.WAClient.AddEventHandler(mycli.MyEventHandler) + s.ClientHttp[userID] = resty.New() + s.ClientHttp[userID].SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) + + s.ClientHttp[userID].SetTimeout(5 * time.Second) + s.ClientHttp[userID].SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + + if client.Store.ID == nil { + // No ID stored, new login + + qrChan, err := client.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.Error().Err(err).Msg("Failed to get QR channel") + } + } else { + err = client.Connect() // Si no conectamos no se puede generar QR + if err != nil { + panic(err) + } + for evt := range qrChan { + if evt.Event == "code" { + // Display QR code in terminal (useful for testing/developing) + if *s.LogType != "json" { + qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) + fmt.Println("QR code:\n", evt.Code) + } + // Store encoded/embeded base64 QR on database for retrieval with the /qr endpoint + image, _ := qrcode.Encode(evt.Code, qrcode.Medium, 256) + base64qrcode := "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) + sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` + _, err := s.Db.Exec(sqlStmt, base64qrcode, userID) + if err != nil { + log.Error().Err(err).Msg(sqlStmt) + } + } else if evt.Event == "timeout" { + // Clear QR code from DB on timeout + sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` + _, err := s.Db.Exec(sqlStmt, "", userID) + if err != nil { + log.Error().Err(err).Msg(sqlStmt) + } + log.Warn().Msg("QR timeout killing channel") + delete(s.ClientPointer, userID) + s.KillChannel[userID] <- true + } else if evt.Event == "success" { + log.Info().Msg("QR pairing ok!") + // Clear QR code after pairing + sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` + _, err := s.Db.Exec(sqlStmt, "", userID) + if err != nil { + log.Error().Err(err).Msg(sqlStmt) + } + } else { + log.Info().Str("event", evt.Event).Msg("Login event") + } + } + } + + } else { + // Already logged in, just connect + log.Info().Msg("Already logged in, just connect") + err = client.Connect() + if err != nil { + panic(err) + } + } + + // Keep connected client live until disconnected/killed + for { + select { + case <-s.KillChannel[userID]: + log.Info().Str("userid", strconv.Itoa(userID)).Msg("Received kill signal") + client.Disconnect() + delete(s.ClientPointer, userID) + sqlStmt := `UPDATE users SET connected=0 WHERE id=?` + _, err := s.Db.Exec(sqlStmt, userID) + if err != nil { + log.Error().Err(err).Msg(sqlStmt) + } + return + default: + time.Sleep(1000 * time.Millisecond) + //log.Info().Str("jid",textjid).Msg("Loop the loop") + } + } +} + +func (s *Controller) Authalice(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + var ctx context.Context + userid := 0 + txtid := "" + webhook := "" + jid := "" + events := "" + + // Get token from headers or uri parameters + token := r.Header.Get("token") + if token == "" { + token = strings.Join(r.URL.Query()["token"], "") + } + + myuserinfo, found := s.UserInfoCache.Get(token) + if !found { + log.Info().Msg("Looking for user information in DB") + // Checks DB from matching user and store user values in context + rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&txtid, &webhook, &jid, &events) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + userid, _ = strconv.Atoi(txtid) + v := internalTypes.Values{M: map[string]string{ + "Id": txtid, + "Jid": jid, + "Webhook": webhook, + "Token": token, + "Events": events, + }} + + s.UserInfoCache.Set(token, v, cache.NoExpiration) + ctx = context.WithValue(r.Context(), "userinfo", v) + } + } else { + ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) + userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) + } + + if userid == 0 { + s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) + return + } + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// Middleware: Authenticate connections based on Token header/uri parameter +func (s *Controller) Auth(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + var ctx context.Context + userid := 0 + txtid := "" + webhook := "" + jid := "" + events := "" + + // Get token from headers or uri parameters + token := r.Header.Get("token") + if token == "" { + token = strings.Join(r.URL.Query()["token"], "") + } + + myuserinfo, found := s.UserInfoCache.Get(token) + if !found { + log.Info().Msg("Looking for user information in DB") + // Checks DB from matching user and store user values in context + rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&txtid, &webhook, &jid, &events) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + userid, _ = strconv.Atoi(txtid) + v := internalTypes.Values{M: map[string]string{ + "Id": txtid, + "Jid": jid, + "Webhook": webhook, + "Token": token, + "Events": events, + }} + + s.UserInfoCache.Set(token, v, cache.NoExpiration) + ctx = context.WithValue(r.Context(), "userinfo", v) + } + } else { + ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) + userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) + } + + if userid == 0 { + s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) + return + } + handler(w, r.WithContext(ctx)) + } +} From 2eba533e1ea386141260932582003c559ab85bf0 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 10:59:21 -0300 Subject: [PATCH 08/24] Implemented chat controller --- controllers/chat/chat_controller.go | 568 ++++++++++ controllers/chat/chat_message_controller.go | 1131 +++++++++++++++++++ handlers.go | 32 +- main.go | 15 +- routes.go | 63 +- wmiau.go | 12 +- 6 files changed, 1754 insertions(+), 67 deletions(-) create mode 100644 controllers/chat/chat_controller.go create mode 100644 controllers/chat/chat_message_controller.go diff --git a/controllers/chat/chat_controller.go b/controllers/chat/chat_controller.go new file mode 100644 index 00000000..7366d9f3 --- /dev/null +++ b/controllers/chat/chat_controller.go @@ -0,0 +1,568 @@ +package chat + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "strconv" + "strings" + "time" + "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + + "github.com/justinas/alice" + "github.com/rs/zerolog/log" + "github.com/vincent-petithory/dataurl" + "go.mau.fi/whatsmeow" + waProto "go.mau.fi/whatsmeow/binary/proto" + "go.mau.fi/whatsmeow/types" + "google.golang.org/protobuf/proto" +) + +type ChatController struct { + *controllerBase.Controller +} + +func (s *ChatController) SignRoutes(c alice.Chain) { + s.Router.Handle("/chat/react", c.Then(s.React())).Methods("POST") + s.Router.Handle("/chat/presence", c.Then(s.ChatPresence())).Methods("POST") + s.Router.Handle("/chat/markread", c.Then(s.MarkRead())).Methods("POST") + s.Router.Handle("/chat/downloadimage", c.Then(s.DownloadImage())).Methods("POST") + s.Router.Handle("/chat/downloadvideo", c.Then(s.DownloadVideo())).Methods("POST") + s.Router.Handle("/chat/downloadaudio", c.Then(s.DownloadAudio())).Methods("POST") + s.Router.Handle("/chat/downloaddocument", c.Then(s.DownloadDocument())).Methods("POST") +} + +// Sets Chat Presence (typing/paused/recording audio) +func (s *ChatController) ChatPresence() http.HandlerFunc { + + type chatPresenceStruct struct { + Phone string + State string + Media types.ChatPresenceMedia + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t chatPresenceStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if len(t.Phone) < 1 { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if len(t.State) < 1 { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing State in Payload")) + return + } + + jid, ok := helpers.ParseJID(t.Phone) + if !ok { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) + return + } + + err = s.ClientPointer[userid].SendChatPresence(jid, types.ChatPresence(t.State), types.ChatPresenceMedia(t.Media)) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Failure sending chat presence to Whatsapp servers")) + return + } + + response := map[string]interface{}{"Details": "Chat presence set successfuly"} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Downloads Image and returns base64 representation +func (s *ChatController) DownloadImage() http.HandlerFunc { + + type downloadImageStruct struct { + Url string + DirectPath string + MediaKey []byte + Mimetype string + FileEncSHA256 []byte + FileSHA256 []byte + FileLength uint64 + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + mimetype := "" + var imgdata []byte + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + // check/creates user directory for files + userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) + _, err := os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create user directory (%s)", userDirectory))) + return + } + } + + decoder := json.NewDecoder(r.Body) + var t downloadImageStruct + err = decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + msg := &waProto.Message{ImageMessage: &waProto.ImageMessage{ + Url: proto.String(t.Url), + DirectPath: proto.String(t.DirectPath), + MediaKey: t.MediaKey, + Mimetype: proto.String(t.Mimetype), + FileEncSha256: t.FileEncSHA256, + FileSha256: t.FileSHA256, + FileLength: &t.FileLength, + }} + + img := msg.GetImageMessage() + + if img != nil { + imgdata, err = s.ClientPointer[userid].Download(img) + if err != nil { + log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to download image") + msg := fmt.Sprintf("Failed to download image %v", err) + s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) + return + } + mimetype = img.GetMimetype() + } + + dataURL := dataurl.New(imgdata, mimetype) + response := map[string]interface{}{"Mimetype": mimetype, "Data": dataURL.String()} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Downloads Document and returns base64 representation +func (s *ChatController) DownloadDocument() http.HandlerFunc { + + type downloadDocumentStruct struct { + Url string + DirectPath string + MediaKey []byte + Mimetype string + FileEncSHA256 []byte + FileSHA256 []byte + FileLength uint64 + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + mimetype := "" + var docdata []byte + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + // check/creates user directory for files + userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) + _, err := os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create user directory (%s)", userDirectory))) + return + } + } + + decoder := json.NewDecoder(r.Body) + var t downloadDocumentStruct + err = decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + msg := &waProto.Message{DocumentMessage: &waProto.DocumentMessage{ + Url: proto.String(t.Url), + DirectPath: proto.String(t.DirectPath), + MediaKey: t.MediaKey, + Mimetype: proto.String(t.Mimetype), + FileEncSha256: t.FileEncSHA256, + FileSha256: t.FileSHA256, + FileLength: &t.FileLength, + }} + + doc := msg.GetDocumentMessage() + + if doc != nil { + docdata, err = s.ClientPointer[userid].Download(doc) + if err != nil { + log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to download document") + msg := fmt.Sprintf("Failed to download document %v", err) + s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) + return + } + mimetype = doc.GetMimetype() + } + + dataURL := dataurl.New(docdata, mimetype) + response := map[string]interface{}{"Mimetype": mimetype, "Data": dataURL.String()} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Downloads Video and returns base64 representation +func (s *ChatController) DownloadVideo() http.HandlerFunc { + + type downloadVideoStruct struct { + Url string + DirectPath string + MediaKey []byte + Mimetype string + FileEncSHA256 []byte + FileSHA256 []byte + FileLength uint64 + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + mimetype := "" + var docdata []byte + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + // check/creates user directory for files + userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) + _, err := os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create user directory (%s)", userDirectory))) + return + } + } + + decoder := json.NewDecoder(r.Body) + var t downloadVideoStruct + err = decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + msg := &waProto.Message{VideoMessage: &waProto.VideoMessage{ + Url: proto.String(t.Url), + DirectPath: proto.String(t.DirectPath), + MediaKey: t.MediaKey, + Mimetype: proto.String(t.Mimetype), + FileEncSha256: t.FileEncSHA256, + FileSha256: t.FileSHA256, + FileLength: &t.FileLength, + }} + + doc := msg.GetVideoMessage() + + if doc != nil { + docdata, err = s.ClientPointer[userid].Download(doc) + if err != nil { + log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to download video") + msg := fmt.Sprintf("Failed to download video %v", err) + s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) + return + } + mimetype = doc.GetMimetype() + } + + dataURL := dataurl.New(docdata, mimetype) + response := map[string]interface{}{"Mimetype": mimetype, "Data": dataURL.String()} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Downloads Audio and returns base64 representation +func (s *ChatController) DownloadAudio() http.HandlerFunc { + + type downloadAudioStruct struct { + Url string + DirectPath string + MediaKey []byte + Mimetype string + FileEncSHA256 []byte + FileSHA256 []byte + FileLength uint64 + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + mimetype := "" + var docdata []byte + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + // check/creates user directory for files + userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) + _, err := os.Stat(userDirectory) + if os.IsNotExist(err) { + errDir := os.MkdirAll(userDirectory, 0751) + if errDir != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create user directory (%s)", userDirectory))) + return + } + } + + decoder := json.NewDecoder(r.Body) + var t downloadAudioStruct + err = decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + msg := &waProto.Message{AudioMessage: &waProto.AudioMessage{ + Url: proto.String(t.Url), + DirectPath: proto.String(t.DirectPath), + MediaKey: t.MediaKey, + Mimetype: proto.String(t.Mimetype), + FileEncSha256: t.FileEncSHA256, + FileSha256: t.FileSHA256, + FileLength: &t.FileLength, + }} + + doc := msg.GetAudioMessage() + + if doc != nil { + docdata, err = s.ClientPointer[userid].Download(doc) + if err != nil { + log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to download audio") + msg := fmt.Sprintf("Failed to download audio %v", err) + s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) + return + } + mimetype = doc.GetMimetype() + } + + dataURL := dataurl.New(docdata, mimetype) + response := map[string]interface{}{"Mimetype": mimetype, "Data": dataURL.String()} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// React +func (s *ChatController) React() http.HandlerFunc { + + type textStruct struct { + Phone string + Body string + Id string + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + msgid := "" + var resp whatsmeow.SendResponse + + decoder := json.NewDecoder(r.Body) + var t textStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if t.Body == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Body in Payload")) + return + } + + recipient, ok := helpers.ParseJID(t.Phone) + if !ok { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) + return + } + + if t.Id == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Id in Payload")) + return + } else { + msgid = t.Id + } + + fromMe := false + if strings.HasPrefix(msgid, "me:") { + fromMe = true + msgid = msgid[len("me:"):] + } + reaction := t.Body + 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(msgid), + }, + Text: proto.String(reaction), + GroupingKey: proto.String(reaction), + SenderTimestampMs: proto.Int64(time.Now().UnixMilli()), + }, + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + + return + } +} + +// Mark messages as read +func (s *ChatController) MarkRead() http.HandlerFunc { + + type markReadStruct struct { + Id []string + Chat types.JID + Sender types.JID + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t markReadStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Chat.String() == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Chat in Payload")) + return + } + + if len(t.Id) < 1 { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Id in Payload")) + return + } + + err = s.ClientPointer[userid].MarkRead(t.Id, time.Now(), t.Chat, t.Sender) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Failure marking messages as read")) + return + } + + response := map[string]interface{}{"Details": "Message(s) marked as read"} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} diff --git a/controllers/chat/chat_message_controller.go b/controllers/chat/chat_message_controller.go new file mode 100644 index 00000000..a4b428b9 --- /dev/null +++ b/controllers/chat/chat_message_controller.go @@ -0,0 +1,1131 @@ +package chat + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + controllerBase "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + "wuzapi/message" + + "github.com/justinas/alice" + "github.com/rs/zerolog/log" + "github.com/vincent-petithory/dataurl" + "go.mau.fi/whatsmeow" + waProto "go.mau.fi/whatsmeow/binary/proto" + "google.golang.org/protobuf/proto" +) + +type ChatMessageController struct { + *controllerBase.Controller +} + +func (s *ChatMessageController) SignRoutes(c alice.Chain) { + s.Router.Handle("/chat/send/text", c.Then(s.SendMessage())).Methods("POST") + s.Router.Handle("/chat/send/image", c.Then(s.SendImage())).Methods("POST") + s.Router.Handle("/chat/send/audio", c.Then(s.SendAudio())).Methods("POST") + s.Router.Handle("/chat/send/document", c.Then(s.SendDocument())).Methods("POST") + s.Router.Handle("/chat/send/video", c.Then(s.SendVideo())).Methods("POST") + s.Router.Handle("/chat/send/sticker", c.Then(s.SendSticker())).Methods("POST") + s.Router.Handle("/chat/send/location", c.Then(s.SendLocation())).Methods("POST") + s.Router.Handle("/chat/send/contact", c.Then(s.SendContact())).Methods("POST") + s.Router.Handle("/chat/send/buttons", c.Then(s.SendButtons())).Methods("POST") + s.Router.Handle("/chat/send/list", c.Then(s.SendList())).Methods("POST") +} + +// Sends a regular text message +func (s *ChatMessageController) SendMessage() http.HandlerFunc { + + type textStruct struct { + message.Message + Phone string + Body string + Id string + ContextInfo waProto.ContextInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + msgid := "" + var resp whatsmeow.SendResponse + + decoder := json.NewDecoder(r.Body) + var t textStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if t.Body == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Body in Payload")) + return + } + + recipient, err := t.ValidateMessageFields() + if err != nil { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, err) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + // msg := &waProto.Message{Conversation: &t.Body} + + msg := &waProto.Message{ + ExtendedTextMessage: &waProto.ExtendedTextMessage{ + Text: &t.Body, + }, + } + + if t.ContextInfo.StanzaId != nil { + msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ + StanzaId: proto.String(*t.ContextInfo.StanzaId), + Participant: proto.String(*t.ContextInfo.Participant), + QuotedMessage: &waProto.Message{Conversation: proto.String("")}, + } + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("Error sending message: %v", err)) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + + return + } +} + +// Sends an Image message +func (s *ChatMessageController) SendImage() http.HandlerFunc { + + type imageStruct struct { + message.Message + Phone string + Image string + Caption string + Id string + ContextInfo waProto.ContextInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + msgid := "" + var resp whatsmeow.SendResponse + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t imageStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if t.Image == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Image in Payload")) + return + } + + recipient, err := t.ValidateMessageFields() + if err != nil { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, err) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + var uploaded whatsmeow.UploadResponse + var filedata []byte + + if t.Image[0:10] == "data:image" { + dataURL, err := dataurl.DecodeString(t.Image) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) + return + } else { + filedata = dataURL.Data + uploaded, err = s.ClientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaImage) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) + return + } + } + } else { + s.Respond(w, r, http.StatusBadRequest, errors.New("Image data should start with \"data:image/png;base64,\"")) + return + } + + msg := &waProto.Message{ImageMessage: &waProto.ImageMessage{ + Caption: proto.String(t.Caption), + Url: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(http.DetectContentType(filedata)), + FileEncSha256: uploaded.FileEncSHA256, + FileSha256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(filedata))), + }} + + if t.ContextInfo.StanzaId != nil { + msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ + StanzaId: proto.String(*t.ContextInfo.StanzaId), + Participant: proto.String(*t.ContextInfo.Participant), + QuotedMessage: &waProto.Message{Conversation: proto.String("")}, + } + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Sends Sticker message +func (s *ChatMessageController) SendSticker() http.HandlerFunc { + + type stickerStruct struct { + message.Message + Phone string + Sticker string + Id string + PngThumbnail []byte + ContextInfo waProto.ContextInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + msgid := "" + var resp whatsmeow.SendResponse + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t stickerStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if t.Sticker == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Sticker in Payload")) + return + } + + recipient, err := t.ValidateMessageFields() + if err != nil { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, err) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + var uploaded whatsmeow.UploadResponse + var filedata []byte + + if t.Sticker[0:4] == "data" { + dataURL, err := dataurl.DecodeString(t.Sticker) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) + return + } else { + filedata = dataURL.Data + uploaded, err = s.ClientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaImage) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) + return + } + } + } else { + s.Respond(w, r, http.StatusBadRequest, errors.New("Data should start with \"data:mime/type;base64,\"")) + return + } + + msg := &waProto.Message{StickerMessage: &waProto.StickerMessage{ + Url: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(http.DetectContentType(filedata)), + FileEncSha256: uploaded.FileEncSHA256, + FileSha256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(filedata))), + PngThumbnail: t.PngThumbnail, + }} + + if t.ContextInfo.StanzaId != nil { + msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ + StanzaId: proto.String(*t.ContextInfo.StanzaId), + Participant: proto.String(*t.ContextInfo.Participant), + QuotedMessage: &waProto.Message{Conversation: proto.String("")}, + } + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Sends Video message +func (s *ChatMessageController) SendVideo() http.HandlerFunc { + + type imageStruct struct { + message.Message + Phone string + Video string + Caption string + Id string + JpegThumbnail []byte + ContextInfo waProto.ContextInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + msgid := "" + var resp whatsmeow.SendResponse + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t imageStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if t.Video == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Video in Payload")) + return + } + + recipient, err := t.ValidateMessageFields() + if err != nil { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, err) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + var uploaded whatsmeow.UploadResponse + var filedata []byte + + if t.Video[0:4] == "data" { + dataURL, err := dataurl.DecodeString(t.Video) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) + return + } else { + filedata = dataURL.Data + uploaded, err = s.ClientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaVideo) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) + return + } + } + } else { + s.Respond(w, r, http.StatusBadRequest, errors.New("Data should start with \"data:mime/type;base64,\"")) + return + } + + msg := &waProto.Message{VideoMessage: &waProto.VideoMessage{ + Caption: proto.String(t.Caption), + Url: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(http.DetectContentType(filedata)), + FileEncSha256: uploaded.FileEncSHA256, + FileSha256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(filedata))), + JpegThumbnail: t.JpegThumbnail, + }} + + if t.ContextInfo.StanzaId != nil { + msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ + StanzaId: proto.String(*t.ContextInfo.StanzaId), + Participant: proto.String(*t.ContextInfo.Participant), + QuotedMessage: &waProto.Message{Conversation: proto.String("")}, + } + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Sends Contact +func (s *ChatMessageController) SendContact() http.HandlerFunc { + + type contactStruct struct { + message.Message + Phone string + Id string + Name string + Vcard string + ContextInfo waProto.ContextInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + msgid := "" + var resp whatsmeow.SendResponse + + decoder := json.NewDecoder(r.Body) + var t contactStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + if t.Name == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Name in Payload")) + return + } + if t.Vcard == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Vcard in Payload")) + return + } + + recipient, err := t.ValidateMessageFields() + if err != nil { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, err) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + msg := &waProto.Message{ContactMessage: &waProto.ContactMessage{ + DisplayName: &t.Name, + Vcard: &t.Vcard, + }} + + if t.ContextInfo.StanzaId != nil { + msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ + StanzaId: proto.String(*t.ContextInfo.StanzaId), + Participant: proto.String(*t.ContextInfo.Participant), + QuotedMessage: &waProto.Message{Conversation: proto.String("")}, + } + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Sends location +func (s *ChatMessageController) SendLocation() http.HandlerFunc { + + type locationStruct struct { + message.Message + Phone string + Id string + Name string + Latitude float64 + Longitude float64 + ContextInfo waProto.ContextInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + msgid := "" + var resp whatsmeow.SendResponse + + decoder := json.NewDecoder(r.Body) + var t locationStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + if t.Latitude == 0 { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Latitude in Payload")) + return + } + if t.Longitude == 0 { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Longitude in Payload")) + return + } + + recipient, err := t.ValidateMessageFields() + if err != nil { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, err) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + msg := &waProto.Message{LocationMessage: &waProto.LocationMessage{ + DegreesLatitude: &t.Latitude, + DegreesLongitude: &t.Longitude, + Name: &t.Name, + }} + + if t.ContextInfo.StanzaId != nil { + msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ + StanzaId: proto.String(*t.ContextInfo.StanzaId), + Participant: proto.String(*t.ContextInfo.Participant), + QuotedMessage: &waProto.Message{Conversation: proto.String("")}, + } + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Sends Buttons (not implemented, does not work) + +func (s *ChatMessageController) SendButtons() http.HandlerFunc { + + type buttonStruct struct { + ButtonId string + ButtonText string + } + type textStruct struct { + Phone string + Title string + Buttons []buttonStruct + Id string + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + msgid := "" + var resp whatsmeow.SendResponse + + decoder := json.NewDecoder(r.Body) + var t textStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if t.Title == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Title in Payload")) + return + } + + if len(t.Buttons) < 1 { + s.Respond(w, r, http.StatusBadRequest, errors.New("missing Buttons in Payload")) + return + } + if len(t.Buttons) > 3 { + s.Respond(w, r, http.StatusBadRequest, errors.New("buttons cant more than 3")) + return + } + + recipient, ok := helpers.ParseJID(t.Phone) + if !ok { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + var buttons []*waProto.ButtonsMessage_Button + + for _, item := range t.Buttons { + buttons = append(buttons, &waProto.ButtonsMessage_Button{ + ButtonId: proto.String(item.ButtonId), + ButtonText: &waProto.ButtonsMessage_Button_ButtonText{DisplayText: proto.String(item.ButtonText)}, + Type: waProto.ButtonsMessage_Button_RESPONSE.Enum(), + NativeFlowInfo: &waProto.ButtonsMessage_Button_NativeFlowInfo{}, + }) + } + + msg2 := &waProto.ButtonsMessage{ + ContentText: proto.String(t.Title), + HeaderType: waProto.ButtonsMessage_EMPTY.Enum(), + Buttons: buttons, + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, &waProto.Message{ViewOnceMessage: &waProto.FutureProofMessage{ + Message: &waProto.Message{ + ButtonsMessage: msg2, + }, + }}) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// SendList +// https://github.com/tulir/whatsmeow/issues/305 +func (s *ChatMessageController) SendList() http.HandlerFunc { + + type rowsStruct struct { + RowId string + Title string + Description string + } + + type sectionsStruct struct { + Title string + Rows []rowsStruct + } + + type listStruct struct { + Phone string + Title string + Description string + ButtonText string + FooterText string + Sections []sectionsStruct + Id string + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("no session")) + return + } + + msgid := "" + var resp whatsmeow.SendResponse + + decoder := json.NewDecoder(r.Body) + var t listStruct + err := decoder.Decode(&t) + marshal, _ := json.Marshal(t) + fmt.Println(string(marshal)) + if err != nil { + fmt.Println(err) + s.Respond(w, r, http.StatusBadRequest, errors.New("could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("missing Phone in Payload")) + return + } + + if t.Title == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("missing Title in Payload")) + return + } + + if t.Description == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("missing Description in Payload")) + return + } + + if t.ButtonText == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("missing ButtonText in Payload")) + return + } + + if len(t.Sections) < 1 { + s.Respond(w, r, http.StatusBadRequest, errors.New("missing Sections in Payload")) + return + } + recipient, ok := helpers.ParseJID(t.Phone) + if !ok { + s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Phone")) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + var sections []*waProto.ListMessage_Section + + for _, item := range t.Sections { + var rows []*waProto.ListMessage_Row + id := 1 + for _, row := range item.Rows { + var idtext string + if row.RowId == "" { + idtext = strconv.Itoa(id) + } else { + idtext = row.RowId + } + rows = append(rows, &waProto.ListMessage_Row{ + RowId: proto.String(idtext), + Title: proto.String(row.Title), + Description: proto.String(row.Description), + }) + } + + sections = append(sections, &waProto.ListMessage_Section{ + Title: proto.String(item.Title), + Rows: rows, + }) + } + msg1 := &waProto.ListMessage{ + Title: proto.String(t.Title), + Description: proto.String(t.Description), + ButtonText: proto.String(t.ButtonText), + ListType: waProto.ListMessage_SINGLE_SELECT.Enum(), + Sections: sections, + FooterText: proto.String(t.FooterText), + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, &waProto.Message{ + ViewOnceMessage: &waProto.FutureProofMessage{ + Message: &waProto.Message{ + ListMessage: msg1, + }, + }}) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Sends a document/attachment message +func (s *ChatMessageController) SendDocument() http.HandlerFunc { + + type documentStruct struct { + message.Message + Phone string + Document string + FileName string + Id string + ContextInfo waProto.ContextInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + msgid := "" + var resp whatsmeow.SendResponse + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t documentStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if t.Document == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Document in Payload")) + return + } + + if t.FileName == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing FileName in Payload")) + return + } + + recipient, err := t.ValidateMessageFields() + if err != nil { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, err) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + var uploaded whatsmeow.UploadResponse + var filedata []byte + + if t.Document[0:29] == "data:application/octet-stream" { + dataURL, err := dataurl.DecodeString(t.Document) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) + return + } else { + filedata = dataURL.Data + uploaded, err = s.ClientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaDocument) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) + return + } + } + } else { + s.Respond(w, r, http.StatusBadRequest, errors.New("Document data should start with \"data:application/octet-stream;base64,\"")) + return + } + + msg := &waProto.Message{DocumentMessage: &waProto.DocumentMessage{ + Url: proto.String(uploaded.URL), + FileName: &t.FileName, + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + Mimetype: proto.String(http.DetectContentType(filedata)), + FileEncSha256: uploaded.FileEncSHA256, + FileSha256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(filedata))), + }} + + if t.ContextInfo.StanzaId != nil { + msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ + StanzaId: proto.String(*t.ContextInfo.StanzaId), + Participant: proto.String(*t.ContextInfo.Participant), + QuotedMessage: &waProto.Message{Conversation: proto.String("")}, + } + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Sends an audio message +func (s *ChatMessageController) SendAudio() http.HandlerFunc { + + type audioStruct struct { + message.Message + Phone string + Audio string + Caption string + Id string + ContextInfo waProto.ContextInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + msgid := "" + var resp whatsmeow.SendResponse + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t audioStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if t.Phone == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + if t.Audio == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Audio in Payload")) + return + } + + recipient, err := t.ValidateMessageFields() + if err != nil { + log.Error().Msg(fmt.Sprintf("%s", err)) + s.Respond(w, r, http.StatusBadRequest, err) + return + } + + if t.Id == "" { + msgid = whatsmeow.GenerateMessageID() + } else { + msgid = t.Id + } + + var uploaded whatsmeow.UploadResponse + var filedata []byte + + if t.Audio[0:14] == "data:audio/ogg" { + dataURL, err := dataurl.DecodeString(t.Audio) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) + return + } else { + filedata = dataURL.Data + uploaded, err = s.ClientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaAudio) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) + return + } + } + } else { + s.Respond(w, r, http.StatusBadRequest, errors.New("Audio data should start with \"data:audio/ogg;base64,\"")) + return + } + + ptt := true + mime := "audio/ogg; codecs=opus" + + msg := &waProto.Message{AudioMessage: &waProto.AudioMessage{ + Url: proto.String(uploaded.URL), + DirectPath: proto.String(uploaded.DirectPath), + MediaKey: uploaded.MediaKey, + //Mimetype: proto.String(http.DetectContentType(filedata)), + Mimetype: &mime, + FileEncSha256: uploaded.FileEncSHA256, + FileSha256: uploaded.FileSHA256, + FileLength: proto.Uint64(uint64(len(filedata))), + Ptt: &ptt, + }} + + if t.ContextInfo.StanzaId != nil { + msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ + StanzaId: proto.String(*t.ContextInfo.StanzaId), + Participant: proto.String(*t.ContextInfo.Participant), + QuotedMessage: &waProto.Message{Conversation: proto.String("")}, + } + } + + resp, err = s.ClientPointer[userid].SendMessage(context.Background(), recipient, msg) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) + return + } + + log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") + response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} diff --git a/handlers.go b/handlers.go index 3a701a14..32b117d8 100644 --- a/handlers.go +++ b/handlers.go @@ -45,7 +45,7 @@ func (s *server) authalice(next http.Handler) http.Handler { if !found { log.Info().Msg("Looking for user information in DB") // Checks DB from matching user and store user values in context - rows, err := s.db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return @@ -103,7 +103,7 @@ func (s *server) auth(handler http.HandlerFunc) http.HandlerFunc { if !found { log.Info().Msg("Looking for user information in DB") // Checks DB from matching user and store user values in context - rows, err := s.db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return @@ -188,7 +188,7 @@ func (s *server) Connect() http.HandlerFunc { } } eventstring = strings.Join(subscribedEvents, ",") - _, err = s.db.Exec("UPDATE users SET events=? WHERE id=?", eventstring, userid) + _, err = s.Db.Exec("UPDATE users SET events=? WHERE id=?", eventstring, userid) if err != nil { log.Warn().Msg("Could not set events in users table") } @@ -245,7 +245,7 @@ func (s *server) Disconnect() http.HandlerFunc { if clientPointer[userid].IsLoggedIn() == true { log.Info().Str("jid", jid).Msg("Disconnection successfull") killchannel[userid] <- true - _, err := s.db.Exec("UPDATE users SET events=? WHERE id=?", "", userid) + _, err := s.Db.Exec("UPDATE users SET events=? WHERE id=?", "", userid) if err != nil { log.Warn().Str("userid", txtid).Msg("Could not set events in users table") } @@ -281,7 +281,7 @@ func (s *server) GetWebhook() http.HandlerFunc { events := "" txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - rows, err := s.db.Query("SELECT webhook,events FROM users WHERE id=? LIMIT 1", txtid) + rows, err := s.Db.Query("SELECT webhook,events FROM users WHERE id=? LIMIT 1", txtid) if err != nil { s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not get webhook: %v", err))) return @@ -333,7 +333,7 @@ func (s *server) SetWebhook() http.HandlerFunc { } var webhook = t.WebhookURL - _, err = s.db.Exec("UPDATE users SET webhook=? WHERE id=?", webhook, userid) + _, err = s.Db.Exec("UPDATE users SET webhook=? WHERE id=?", webhook, userid) if err != nil { s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("%s", err))) return @@ -369,7 +369,7 @@ func (s *server) GetQR() http.HandlerFunc { s.Respond(w, r, http.StatusInternalServerError, errors.New("Not connected")) return } - rows, err := s.db.Query("SELECT qrcode AS code FROM users WHERE id=? LIMIT 1", userid) + rows, err := s.Db.Query("SELECT qrcode AS code FROM users WHERE id=? LIMIT 1", userid) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return @@ -1768,7 +1768,7 @@ func (s *server) CreateUser() http.HandlerFunc { } var userID int - err = s.db.QueryRow("SELECT id FROM users WHERE token=? LIMIT 1", t.Token).Scan(&userID) + err = s.Db.QueryRow("SELECT id FROM users WHERE token=? LIMIT 1", t.Token).Scan(&userID) if err == nil { s.Respond(w, r, http.StatusBadRequest, errors.New("User already exists")) return @@ -1777,7 +1777,7 @@ func (s *server) CreateUser() http.HandlerFunc { return } - result, err := s.db.Exec("INSERT INTO users (name, token) VALUES (?, ?)", t.Name, t.Token) + result, err := s.Db.Exec("INSERT INTO users (name, token) VALUES (?, ?)", t.Name, t.Token) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return @@ -1827,7 +1827,7 @@ func (s *server) DeleteUser() http.HandlerFunc { return } - rows, err := s.db.Query("SELECT name FROM users WHERE token=? LIMIT 1", t.Token) + rows, err := s.Db.Query("SELECT name FROM users WHERE token=? LIMIT 1", t.Token) if err != nil { rows.Close() s.Respond(w, r, http.StatusInternalServerError, err) @@ -1850,7 +1850,7 @@ func (s *server) DeleteUser() http.HandlerFunc { } } - _, err = s.db.Query(fmt.Sprintf("delete from users where token='%s'", t.Token)) + _, err = s.Db.Query(fmt.Sprintf("delete from users where token='%s'", t.Token)) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return @@ -1882,7 +1882,7 @@ func (s *server) GetUserByToken() http.HandlerFunc { } var user userResponse - err := s.db.QueryRow("SELECT id, name, token, webhook, jid FROM users WHERE token=? LIMIT 1", t.Token).Scan( + err := s.Db.QueryRow("SELECT id, name, token, webhook, jid FROM users WHERE token=? LIMIT 1", t.Token).Scan( &user.ID, &user.Name, &user.Token, &user.Webhook, &user.Jid, ) if err != nil { @@ -2225,7 +2225,7 @@ func (s *server) DownloadImage() http.HandlerFunc { } // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", s.exPath, txtid) + userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) _, err := os.Stat(userDirectory) if os.IsNotExist(err) { errDir := os.MkdirAll(userDirectory, 0751) @@ -2305,7 +2305,7 @@ func (s *server) DownloadDocument() http.HandlerFunc { } // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", s.exPath, txtid) + userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) _, err := os.Stat(userDirectory) if os.IsNotExist(err) { errDir := os.MkdirAll(userDirectory, 0751) @@ -2385,7 +2385,7 @@ func (s *server) DownloadVideo() http.HandlerFunc { } // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", s.exPath, txtid) + userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) _, err := os.Stat(userDirectory) if os.IsNotExist(err) { errDir := os.MkdirAll(userDirectory, 0751) @@ -2465,7 +2465,7 @@ func (s *server) DownloadAudio() http.HandlerFunc { } // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", s.exPath, txtid) + userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) _, err := os.Stat(userDirectory) if os.IsNotExist(err) { errDir := os.MkdirAll(userDirectory, 0751) diff --git a/main.go b/main.go index 1d27b387..811c5889 100755 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "path/filepath" "syscall" "time" + controllerBase "wuzapi/controllers/controller_base" "go.mau.fi/whatsmeow/store/sqlstore" waLog "go.mau.fi/whatsmeow/util/log" @@ -22,9 +23,7 @@ import ( ) type server struct { - db *sql.DB - router *mux.Router - exPath string + controllerBase.Controller } var ( @@ -101,9 +100,11 @@ func main() { } s := &server{ - router: mux.NewRouter(), - db: db, - exPath: exPath, + controllerBase.Controller{ + Router: mux.NewRouter(), + Db: db, + ExPath: exPath, + }, } s.routes() @@ -111,7 +112,7 @@ func main() { srv := &http.Server{ Addr: *address + ":" + *port, - Handler: s.router, + Handler: s.Router, } done := make(chan os.Signal, 1) diff --git a/routes.go b/routes.go index c7e06fb2..f1c28fdb 100644 --- a/routes.go +++ b/routes.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "time" + "wuzapi/controllers/chat" internalTypes "wuzapi/internal/types" "github.com/justinas/alice" @@ -48,48 +49,34 @@ func (s *server) routes() { c = c.Append(hlog.RefererHandler("referer")) c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id")) - s.router.Handle("/session/connect", c.Then(s.Connect())).Methods("POST") - s.router.Handle("/session/disconnect", c.Then(s.Disconnect())).Methods("POST") - s.router.Handle("/session/logout", c.Then(s.Logout())).Methods("POST") - s.router.Handle("/session/status", c.Then(s.GetStatus())).Methods("GET") - s.router.Handle("/session/qr", c.Then(s.GetQR())).Methods("GET") + s.Router.Handle("/session/connect", c.Then(s.Connect())).Methods("POST") + s.Router.Handle("/session/disconnect", c.Then(s.Disconnect())).Methods("POST") + s.Router.Handle("/session/logout", c.Then(s.Logout())).Methods("POST") + s.Router.Handle("/session/status", c.Then(s.GetStatus())).Methods("GET") + s.Router.Handle("/session/qr", c.Then(s.GetQR())).Methods("GET") - s.router.Handle("/webhook", c.Then(s.SetWebhook())).Methods("POST") - s.router.Handle("/webhook", c.Then(s.GetWebhook())).Methods("GET") + s.Router.Handle("/webhook", c.Then(s.SetWebhook())).Methods("POST") + s.Router.Handle("/webhook", c.Then(s.GetWebhook())).Methods("GET") - s.router.Handle("/chat/send/text", c.Then(s.SendMessage())).Methods("POST") - s.router.Handle("/chat/send/image", c.Then(s.SendImage())).Methods("POST") - s.router.Handle("/chat/send/audio", c.Then(s.SendAudio())).Methods("POST") - s.router.Handle("/chat/send/document", c.Then(s.SendDocument())).Methods("POST") - // s.router.Handle("/chat/send/template", c.Then(s.SendTemplate())).Methods("POST") - s.router.Handle("/chat/send/video", c.Then(s.SendVideo())).Methods("POST") - s.router.Handle("/chat/send/sticker", c.Then(s.SendSticker())).Methods("POST") - s.router.Handle("/chat/send/location", c.Then(s.SendLocation())).Methods("POST") - s.router.Handle("/chat/send/contact", c.Then(s.SendContact())).Methods("POST") - s.router.Handle("/chat/react", c.Then(s.React())).Methods("POST") - s.router.Handle("/chat/send/buttons", c.Then(s.SendButtons())).Methods("POST") - s.router.Handle("/chat/send/list", c.Then(s.SendList())).Methods("POST") + chatController := &chat.ChatController{Controller: &s.Controller} + chatController.SignRoutes(c) - s.router.Handle("/user/create", c.Then(s.CreateUser())).Methods("POST") - s.router.Handle("/user/delete", c.Then(s.DeleteUser())).Methods("POST") - s.router.Handle("/user/fetch", c.Then(s.GetUserByToken())).Methods("POST") - s.router.Handle("/user/info", c.Then(s.GetUser())).Methods("GET") - s.router.Handle("/user/check", c.Then(s.CheckUser())).Methods("POST") - s.router.Handle("/user/avatar", c.Then(s.GetAvatar())).Methods("POST") - s.router.Handle("/user/contacts", c.Then(s.GetContacts())).Methods("GET") + chatMessageController := &chat.ChatMessageController{Controller: &s.Controller} + chatMessageController.SignRoutes(c) - s.router.Handle("/chat/presence", c.Then(s.ChatPresence())).Methods("POST") - s.router.Handle("/chat/markread", c.Then(s.MarkRead())).Methods("POST") - s.router.Handle("/chat/downloadimage", c.Then(s.DownloadImage())).Methods("POST") - s.router.Handle("/chat/downloadvideo", c.Then(s.DownloadVideo())).Methods("POST") - s.router.Handle("/chat/downloadaudio", c.Then(s.DownloadAudio())).Methods("POST") - s.router.Handle("/chat/downloaddocument", c.Then(s.DownloadDocument())).Methods("POST") + s.Router.Handle("/user/create", c.Then(s.CreateUser())).Methods("POST") + s.Router.Handle("/user/delete", c.Then(s.DeleteUser())).Methods("POST") + s.Router.Handle("/user/fetch", c.Then(s.GetUserByToken())).Methods("POST") + s.Router.Handle("/user/info", c.Then(s.GetUser())).Methods("GET") + s.Router.Handle("/user/check", c.Then(s.CheckUser())).Methods("POST") + s.Router.Handle("/user/avatar", c.Then(s.GetAvatar())).Methods("POST") + s.Router.Handle("/user/contacts", c.Then(s.GetContacts())).Methods("GET") - s.router.Handle("/group/list", c.Then(s.ListGroups())).Methods("GET") - s.router.Handle("/group/info", c.Then(s.GetGroupInfo())).Methods("GET") - s.router.Handle("/group/invitelink", c.Then(s.GetGroupInviteLink())).Methods("GET") - s.router.Handle("/group/photo", c.Then(s.SetGroupPhoto())).Methods("POST") - s.router.Handle("/group/name", c.Then(s.SetGroupName())).Methods("POST") + s.Router.Handle("/group/list", c.Then(s.ListGroups())).Methods("GET") + s.Router.Handle("/group/info", c.Then(s.GetGroupInfo())).Methods("GET") + s.Router.Handle("/group/invitelink", c.Then(s.GetGroupInviteLink())).Methods("GET") + s.Router.Handle("/group/photo", c.Then(s.SetGroupPhoto())).Methods("POST") + s.Router.Handle("/group/name", c.Then(s.SetGroupName())).Methods("POST") - s.router.PathPrefix("/").Handler(http.FileServer(http.Dir(exPath + "/static/"))) + s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir(exPath + "/static/"))) } diff --git a/wmiau.go b/wmiau.go index 4e837efe..3b59367f 100644 --- a/wmiau.go +++ b/wmiau.go @@ -35,7 +35,7 @@ var historySyncID int32 // Connects to Whatsapp Websocket on server startup if last state was connected func (s *server) connectOnStartup() { - rows, err := s.db.Query("SELECT id,token,jid,webhook,events FROM users WHERE connected=1") + rows, err := s.Db.Query("SELECT id,token,jid,webhook,events FROM users WHERE connected=1") if err != nil { log.Error().Err(err).Msg("DB Problem") return @@ -171,7 +171,7 @@ func (s *server) startClient(userID int, textjid string, token string, subscript UserID: userID, Token: token, Subscriptions: subscriptions, - Db: s.db, + Db: s.Db, UserInfoCache: userinfocache, KillChannel: killchannel, ClientHttp: clientHttp, @@ -211,14 +211,14 @@ func (s *server) startClient(userID int, textjid string, token string, subscript image, _ := qrcode.Encode(evt.Code, qrcode.Medium, 256) base64qrcode := "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.db.Exec(sqlStmt, base64qrcode, userID) + _, err := s.Db.Exec(sqlStmt, base64qrcode, userID) if err != nil { log.Error().Err(err).Msg(sqlStmt) } } else if evt.Event == "timeout" { // Clear QR code from DB on timeout sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.db.Exec(sqlStmt, "", userID) + _, err := s.Db.Exec(sqlStmt, "", userID) if err != nil { log.Error().Err(err).Msg(sqlStmt) } @@ -229,7 +229,7 @@ func (s *server) startClient(userID int, textjid string, token string, subscript log.Info().Msg("QR pairing ok!") // Clear QR code after pairing sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.db.Exec(sqlStmt, "", userID) + _, err := s.Db.Exec(sqlStmt, "", userID) if err != nil { log.Error().Err(err).Msg(sqlStmt) } @@ -256,7 +256,7 @@ func (s *server) startClient(userID int, textjid string, token string, subscript client.Disconnect() delete(clientPointer, userID) sqlStmt := `UPDATE users SET connected=0 WHERE id=?` - _, err := s.db.Exec(sqlStmt, userID) + _, err := s.Db.Exec(sqlStmt, userID) if err != nil { log.Error().Err(err).Msg(sqlStmt) } From b3938f6d273b3a6f12bcc6494c82c91265c374b3 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 11:02:03 -0300 Subject: [PATCH 09/24] Implemented group controller --- controllers/group/group_controller.go | 310 ++++++++++++++++++++++++++ main.go | 4 +- routes.go | 12 +- 3 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 controllers/group/group_controller.go diff --git a/controllers/group/group_controller.go b/controllers/group/group_controller.go new file mode 100644 index 00000000..8c35a91d --- /dev/null +++ b/controllers/group/group_controller.go @@ -0,0 +1,310 @@ +package group + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + + "github.com/justinas/alice" + "github.com/rs/zerolog/log" + "github.com/vincent-petithory/dataurl" + "go.mau.fi/whatsmeow/types" +) + +type GroupController struct { + *controllerBase.Controller +} + +func (s *GroupController) SignRoutes(c alice.Chain) { + s.Router.Handle("/group/list", c.Then(s.ListGroups())).Methods("GET") + s.Router.Handle("/group/info", c.Then(s.GetGroupInfo())).Methods("GET") + s.Router.Handle("/group/invitelink", c.Then(s.GetGroupInviteLink())).Methods("GET") + s.Router.Handle("/group/photo", c.Then(s.SetGroupPhoto())).Methods("POST") + s.Router.Handle("/group/name", c.Then(s.SetGroupName())).Methods("POST") +} + +// List groups +func (s *GroupController) ListGroups() http.HandlerFunc { + + type GroupCollection struct { + Groups []types.GroupInfo `json:"groups" binding:"required"` + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + resp, err := s.ClientPointer[userid].GetJoinedGroups() + + if err != nil { + msg := fmt.Sprintf("Failed to get group list: %v", err) + log.Error().Msg(msg) + s.Respond(w, r, http.StatusInternalServerError, msg) + return + } + + gc := new(GroupCollection) + for _, info := range resp { + gc.Groups = append(gc.Groups, *info) + } + + responseJson, err := json.Marshal(gc) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + + return + } +} + +// Get group info +func (s *GroupController) GetGroupInfo() http.HandlerFunc { + + type getGroupInfoStruct struct { + GroupJID string + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t getGroupInfoStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + group, ok := helpers.ParseJID(t.GroupJID) + if !ok { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) + return + } + + resp, err := s.ClientPointer[userid].GetGroupInfo(group) + + if err != nil { + msg := fmt.Sprintf("Failed to get group info: %v", err) + log.Error().Msg(msg) + s.Respond(w, r, http.StatusInternalServerError, msg) + return + } + + responseJson, err := json.Marshal(resp) + + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + + return + } +} + +// Get group invite link +func (s *GroupController) GetGroupInviteLink() http.HandlerFunc { + + type getGroupInfoStruct struct { + GroupJID string + Reset bool + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t getGroupInfoStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + group, ok := helpers.ParseJID(t.GroupJID) + if !ok { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) + return + } + + resp, err := s.ClientPointer[userid].GetGroupInviteLink(group, t.Reset) + + if err != nil { + log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to get group invite link") + msg := fmt.Sprintf("Failed to get group invite link: %v", err) + s.Respond(w, r, http.StatusInternalServerError, msg) + return + } + + response := map[string]interface{}{"InviteLink": resp} + responseJson, err := json.Marshal(response) + + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + + return + } +} + +// Set group photo +func (s *GroupController) SetGroupPhoto() http.HandlerFunc { + + type setGroupPhotoStruct struct { + GroupJID string + Image string + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t setGroupPhotoStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + group, ok := helpers.ParseJID(t.GroupJID) + if !ok { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) + return + } + + if t.Image == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Image in Payload")) + return + } + + var filedata []byte + + if t.Image[0:13] == "data:image/jp" { + dataURL, err := dataurl.DecodeString(t.Image) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) + return + } else { + filedata = dataURL.Data + } + } else { + s.Respond(w, r, http.StatusBadRequest, errors.New("Image data should start with \"data:image/jpeg;base64,\"")) + return + } + + picture_id, err := s.ClientPointer[userid].SetGroupPhoto(group, filedata) + + if err != nil { + log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to set group photo") + msg := fmt.Sprintf("Failed to set group photo: %v", err) + s.Respond(w, r, http.StatusInternalServerError, msg) + return + } + + response := map[string]interface{}{"Details": "Group Photo set successfully", "PictureID": picture_id} + responseJson, err := json.Marshal(response) + + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + + return + } +} + +// Set group name +func (s *GroupController) SetGroupName() http.HandlerFunc { + + type setGroupNameStruct struct { + GroupJID string + Name string + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t setGroupNameStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + group, ok := helpers.ParseJID(t.GroupJID) + if !ok { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) + return + } + + if t.Name == "" { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Name in Payload")) + return + } + + err = s.ClientPointer[userid].SetGroupName(group, t.Name) + + if err != nil { + log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to set group name") + msg := fmt.Sprintf("Failed to set group name: %v", err) + s.Respond(w, r, http.StatusInternalServerError, msg) + return + } + + response := map[string]interface{}{"Details": "Group Name set successfully"} + responseJson, err := json.Marshal(response) + + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + + return + } +} diff --git a/main.go b/main.go index 811c5889..b43236ce 100755 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ import ( ) type server struct { - controllerBase.Controller + *controllerBase.Controller } var ( @@ -100,7 +100,7 @@ func main() { } s := &server{ - controllerBase.Controller{ + &controllerBase.Controller{ Router: mux.NewRouter(), Db: db, ExPath: exPath, diff --git a/routes.go b/routes.go index f1c28fdb..cc08e3ec 100644 --- a/routes.go +++ b/routes.go @@ -6,6 +6,7 @@ import ( "path/filepath" "time" "wuzapi/controllers/chat" + "wuzapi/controllers/group" internalTypes "wuzapi/internal/types" "github.com/justinas/alice" @@ -58,10 +59,10 @@ func (s *server) routes() { s.Router.Handle("/webhook", c.Then(s.SetWebhook())).Methods("POST") s.Router.Handle("/webhook", c.Then(s.GetWebhook())).Methods("GET") - chatController := &chat.ChatController{Controller: &s.Controller} + chatController := &chat.ChatController{Controller: s.Controller} chatController.SignRoutes(c) - chatMessageController := &chat.ChatMessageController{Controller: &s.Controller} + chatMessageController := &chat.ChatMessageController{Controller: s.Controller} chatMessageController.SignRoutes(c) s.Router.Handle("/user/create", c.Then(s.CreateUser())).Methods("POST") @@ -72,11 +73,8 @@ func (s *server) routes() { s.Router.Handle("/user/avatar", c.Then(s.GetAvatar())).Methods("POST") s.Router.Handle("/user/contacts", c.Then(s.GetContacts())).Methods("GET") - s.Router.Handle("/group/list", c.Then(s.ListGroups())).Methods("GET") - s.Router.Handle("/group/info", c.Then(s.GetGroupInfo())).Methods("GET") - s.Router.Handle("/group/invitelink", c.Then(s.GetGroupInviteLink())).Methods("GET") - s.Router.Handle("/group/photo", c.Then(s.SetGroupPhoto())).Methods("POST") - s.Router.Handle("/group/name", c.Then(s.SetGroupName())).Methods("POST") + groupController := &group.GroupController{Controller: s.Controller} + groupController.SignRoutes(c) s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir(exPath + "/static/"))) } From 706cd303e7d4585eba7cdd62b5d837cb5de060d8 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 11:03:33 -0300 Subject: [PATCH 10/24] Implemented session controller --- controllers/session/session_controller.go | 288 ++++++++++++++++++++++ routes.go | 8 +- 2 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 controllers/session/session_controller.go diff --git a/controllers/session/session_controller.go b/controllers/session/session_controller.go new file mode 100644 index 00000000..3a56db1a --- /dev/null +++ b/controllers/session/session_controller.go @@ -0,0 +1,288 @@ +package session + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + + "github.com/justinas/alice" + "github.com/patrickmn/go-cache" + "github.com/rs/zerolog/log" +) + +type SessionController struct { + *controllerBase.Controller +} + +func (s *SessionController) SignRoutes(c alice.Chain) { + s.Router.Handle("/session/connect", c.Then(s.Connect())).Methods("POST") + s.Router.Handle("/session/disconnect", c.Then(s.Disconnect())).Methods("POST") + s.Router.Handle("/session/logout", c.Then(s.Logout())).Methods("POST") + s.Router.Handle("/session/status", c.Then(s.GetStatus())).Methods("GET") + s.Router.Handle("/session/qr", c.Then(s.GetQR())).Methods("GET") +} + +// Connects to Whatsapp Servers +func (s *SessionController) Connect() http.HandlerFunc { + + type connectStruct struct { + Subscribe []string + Immediate bool + } + + return func(w http.ResponseWriter, r *http.Request) { + + webhook := r.Context().Value("userinfo").(internalTypes.Values).Get("Webhook") + jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") + userid, _ := strconv.Atoi(txtid) + eventstring := "" + + // Decodes request BODY looking for events to subscribe + decoder := json.NewDecoder(r.Body) + var t connectStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if s.ClientPointer[userid] != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Already Connected")) + return + } else { + + var subscribedEvents []string + if len(t.Subscribe) < 1 { + if !helpers.Find(subscribedEvents, "All") { + subscribedEvents = append(subscribedEvents, "All") + } + } else { + for _, arg := range t.Subscribe { + if !helpers.Find(internalTypes.MessageTypes, arg) { + log.Warn().Str("Type", arg).Msg("Message type discarded") + continue + } + if !helpers.Find(subscribedEvents, arg) { + subscribedEvents = append(subscribedEvents, arg) + } + } + } + eventstring = strings.Join(subscribedEvents, ",") + _, err = s.Db.Exec("UPDATE users SET events=? WHERE id=?", eventstring, userid) + if err != nil { + log.Warn().Msg("Could not set events in users table") + } + log.Info().Str("events", eventstring).Msg("Setting subscribed events") + v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Events", eventstring) + s.UserInfoCache.Set(token, v, cache.NoExpiration) + + log.Info().Str("jid", jid).Msg("Attempt to connect") + s.KillChannel[userid] = make(chan bool) + go s.StartClient(userid, jid, token, subscribedEvents) + + if t.Immediate == false { + log.Warn().Msg("Waiting 10 seconds") + time.Sleep(10000 * time.Millisecond) + + if s.ClientPointer[userid] != nil { + if !s.ClientPointer[userid].IsConnected() { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to Connect")) + return + } + } else { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to Connect")) + return + } + } + } + + response := map[string]interface{}{"webhook": webhook, "jid": jid, "events": eventstring, "details": "Connected!"} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + return + } + } +} + +// Disconnects from Whatsapp websocket, does not log out device +func (s *SessionController) Disconnect() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") + token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + if s.ClientPointer[userid].IsConnected() == true { + if s.ClientPointer[userid].IsLoggedIn() == true { + log.Info().Str("jid", jid).Msg("Disconnection successfull") + s.KillChannel[userid] <- true + _, err := s.Db.Exec("UPDATE users SET events=? WHERE id=?", "", userid) + if err != nil { + log.Warn().Str("userid", txtid).Msg("Could not set events in users table") + } + v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Events", "") + s.UserInfoCache.Set(token, v, cache.NoExpiration) + + response := map[string]interface{}{"Details": "Disconnected"} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } else { + log.Warn().Str("jid", jid).Msg("Ignoring disconnect as it was not connected") + s.Respond(w, r, http.StatusInternalServerError, errors.New("Cannot disconnect because it is not logged in")) + return + } + } else { + log.Warn().Str("jid", jid).Msg("Ignoring disconnect as it was not connected") + s.Respond(w, r, http.StatusInternalServerError, errors.New("Cannot disconnect because it is not logged in")) + return + } + } +} + +// Logs out device from Whatsapp (requires to scan QR next time) +func (s *SessionController) Logout() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } else { + if s.ClientPointer[userid].IsLoggedIn() == true && s.ClientPointer[userid].IsConnected() == true { + err := s.ClientPointer[userid].Logout() + if err != nil { + log.Error().Str("jid", jid).Msg("Could not perform logout") + s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not perform logout")) + return + } else { + log.Info().Str("jid", jid).Msg("Logged out") + s.KillChannel[userid] <- true + } + } else { + if s.ClientPointer[userid].IsConnected() == true { + log.Warn().Str("jid", jid).Msg("Ignoring logout as it was not logged in") + s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not disconnect as it was not logged in")) + return + } else { + log.Warn().Str("jid", jid).Msg("Ignoring logout as it was not connected") + s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not disconnect as it was not connected")) + return + } + } + } + + response := map[string]interface{}{"Details": "Logged out"} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Gets Connected and LoggedIn Status +func (s *SessionController) GetStatus() http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + isConnected := s.ClientPointer[userid].IsConnected() + isLoggedIn := s.ClientPointer[userid].IsLoggedIn() + + response := map[string]interface{}{"Connected": isConnected, "LoggedIn": isLoggedIn} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Gets QR code encoded in Base64 +func (s *SessionController) GetQR() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + code := "" + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } else { + if s.ClientPointer[userid].IsConnected() == false { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Not connected")) + return + } + rows, err := s.Db.Query("SELECT qrcode AS code FROM users WHERE id=? LIMIT 1", userid) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&code) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + } + err = rows.Err() + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + if s.ClientPointer[userid].IsLoggedIn() == true { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Already Loggedin")) + return + } + } + + log.Info().Str("userid", txtid).Str("qrcode", code).Msg("Get QR successful") + response := map[string]interface{}{"QRCode": fmt.Sprintf("%s", code)} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} diff --git a/routes.go b/routes.go index cc08e3ec..6dc08813 100644 --- a/routes.go +++ b/routes.go @@ -7,6 +7,7 @@ import ( "time" "wuzapi/controllers/chat" "wuzapi/controllers/group" + "wuzapi/controllers/session" internalTypes "wuzapi/internal/types" "github.com/justinas/alice" @@ -50,11 +51,8 @@ func (s *server) routes() { c = c.Append(hlog.RefererHandler("referer")) c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id")) - s.Router.Handle("/session/connect", c.Then(s.Connect())).Methods("POST") - s.Router.Handle("/session/disconnect", c.Then(s.Disconnect())).Methods("POST") - s.Router.Handle("/session/logout", c.Then(s.Logout())).Methods("POST") - s.Router.Handle("/session/status", c.Then(s.GetStatus())).Methods("GET") - s.Router.Handle("/session/qr", c.Then(s.GetQR())).Methods("GET") + sessionController := &session.SessionController{Controller: s.Controller} + sessionController.SignRoutes(c) s.Router.Handle("/webhook", c.Then(s.SetWebhook())).Methods("POST") s.Router.Handle("/webhook", c.Then(s.GetWebhook())).Methods("GET") From f56fdcb3bf2e328976386987d1b583c4c680fd72 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 11:05:05 -0300 Subject: [PATCH 11/24] Implemented user controller --- controllers/user/user_controller.go | 262 ++++++++++++++++++++++++++++ routes.go | 10 +- 2 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 controllers/user/user_controller.go diff --git a/controllers/user/user_controller.go b/controllers/user/user_controller.go new file mode 100644 index 00000000..847ebe15 --- /dev/null +++ b/controllers/user/user_controller.go @@ -0,0 +1,262 @@ +package user + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + controllerBase "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + + "github.com/justinas/alice" + "github.com/rs/zerolog/log" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" +) + +type UserController struct { + *controllerBase.Controller +} + +func (s *UserController) SignRoutes(c alice.Chain) { + s.Router.Handle("/user/info", c.Then(s.GetUser())).Methods("GET") + s.Router.Handle("/user/check", c.Then(s.CheckUser())).Methods("POST") + s.Router.Handle("/user/avatar", c.Then(s.GetAvatar())).Methods("POST") + s.Router.Handle("/user/contacts", c.Then(s.GetContacts())).Methods("GET") +} + +// checks if users/phones are on Whatsapp +func (s *UserController) CheckUser() http.HandlerFunc { + + type checkUserStruct struct { + Phone []string + } + + type User struct { + Query string + IsInWhatsapp bool + JID string + VerifiedName string + } + + type UserCollection struct { + Users []User + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t checkUserStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if len(t.Phone) < 1 { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + resp, err := s.ClientPointer[userid].IsOnWhatsApp(t.Phone) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to check if users are on WhatsApp: %s", err))) + return + } + + uc := new(UserCollection) + for _, item := range resp { + if item.VerifiedName != nil { + var msg = User{Query: item.Query, IsInWhatsapp: item.IsIn, JID: fmt.Sprintf("%s", item.JID), VerifiedName: item.VerifiedName.Details.GetVerifiedName()} + uc.Users = append(uc.Users, msg) + } else { + var msg = User{Query: item.Query, IsInWhatsapp: item.IsIn, JID: fmt.Sprintf("%s", item.JID), VerifiedName: ""} + uc.Users = append(uc.Users, msg) + } + } + responseJson, err := json.Marshal(uc) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Gets user information +func (s *UserController) GetUser() http.HandlerFunc { + + type checkUserStruct struct { + Phone []string + } + + type UserCollection struct { + Users map[types.JID]types.UserInfo + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t checkUserStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if len(t.Phone) < 1 { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + var jids []types.JID + for _, arg := range t.Phone { + jid, ok := helpers.ParseJID(arg) + if !ok { + return + } + jids = append(jids, jid) + } + resp, err := s.ClientPointer[userid].GetUserInfo(jids) + + if err != nil { + msg := fmt.Sprintf("Failed to get user info: %v", err) + log.Error().Msg(msg) + s.Respond(w, r, http.StatusInternalServerError, msg) + return + } + + uc := new(UserCollection) + uc.Users = make(map[types.JID]types.UserInfo) + + for jid, info := range resp { + uc.Users[jid] = info + } + + responseJson, err := json.Marshal(uc) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Gets avatar info for user +func (s *UserController) GetAvatar() http.HandlerFunc { + + type getAvatarStruct struct { + Phone string + Preview bool + } + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + decoder := json.NewDecoder(r.Body) + var t getAvatarStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + if len(t.Phone) < 1 { + s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) + return + } + + jid, ok := helpers.ParseJID(t.Phone) + if !ok { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) + return + } + + var pic *types.ProfilePictureInfo + + existingID := "" + pic, err = s.ClientPointer[userid].GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{ + Preview: t.Preview, + ExistingID: existingID, + }) + if err != nil { + msg := fmt.Sprintf("Failed to get avatar: %v", err) + log.Error().Msg(msg) + s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) + return + } + + if pic == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No avatar found")) + return + } + + log.Info().Str("id", pic.ID).Str("url", pic.URL).Msg("Got avatar") + + responseJson, err := json.Marshal(pic) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Gets all contacts +func (s *UserController) GetContacts() http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + userid, _ := strconv.Atoi(txtid) + + if s.ClientPointer[userid] == nil { + s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + return + } + + result := map[types.JID]types.ContactInfo{} + result, err := s.ClientPointer[userid].Store.Contacts.GetAllContacts() + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + responseJson, err := json.Marshal(result) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + + return + } +} diff --git a/routes.go b/routes.go index 6dc08813..05a2e8ed 100644 --- a/routes.go +++ b/routes.go @@ -8,6 +8,7 @@ import ( "wuzapi/controllers/chat" "wuzapi/controllers/group" "wuzapi/controllers/session" + "wuzapi/controllers/user" internalTypes "wuzapi/internal/types" "github.com/justinas/alice" @@ -63,13 +64,8 @@ func (s *server) routes() { chatMessageController := &chat.ChatMessageController{Controller: s.Controller} chatMessageController.SignRoutes(c) - s.Router.Handle("/user/create", c.Then(s.CreateUser())).Methods("POST") - s.Router.Handle("/user/delete", c.Then(s.DeleteUser())).Methods("POST") - s.Router.Handle("/user/fetch", c.Then(s.GetUserByToken())).Methods("POST") - s.Router.Handle("/user/info", c.Then(s.GetUser())).Methods("GET") - s.Router.Handle("/user/check", c.Then(s.CheckUser())).Methods("POST") - s.Router.Handle("/user/avatar", c.Then(s.GetAvatar())).Methods("POST") - s.Router.Handle("/user/contacts", c.Then(s.GetContacts())).Methods("GET") + userController := &user.UserController{Controller: s.Controller} + userController.SignRoutes(c) groupController := &group.GroupController{Controller: s.Controller} groupController.SignRoutes(c) From 83ec087bf11008caf413cfed01068adf92bf9757 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 11:08:48 -0300 Subject: [PATCH 12/24] UserController fix - new methods implemented --- controllers/user/user_controller.go | 157 ++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/controllers/user/user_controller.go b/controllers/user/user_controller.go index 847ebe15..a92187da 100644 --- a/controllers/user/user_controller.go +++ b/controllers/user/user_controller.go @@ -1,6 +1,7 @@ package user import ( + "database/sql" "encoding/json" "errors" "fmt" @@ -21,12 +22,168 @@ type UserController struct { } func (s *UserController) SignRoutes(c alice.Chain) { + s.Router.Handle("/user/create", c.Then(s.CreateUser())).Methods("POST") + s.Router.Handle("/user/delete", c.Then(s.DeleteUser())).Methods("POST") + s.Router.Handle("/user/fetch", c.Then(s.GetUserByToken())).Methods("POST") s.Router.Handle("/user/info", c.Then(s.GetUser())).Methods("GET") s.Router.Handle("/user/check", c.Then(s.CheckUser())).Methods("POST") s.Router.Handle("/user/avatar", c.Then(s.GetAvatar())).Methods("POST") s.Router.Handle("/user/contacts", c.Then(s.GetContacts())).Methods("GET") } +func (s *UserController) CreateUser() http.HandlerFunc { + type userStruct struct { + Name string `json:"name" binding:"required"` + Token string `json:"token" binding:"required"` + } + + return func(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + + var t userStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + var userID int + err = s.Db.QueryRow("SELECT id FROM users WHERE token=? LIMIT 1", t.Token).Scan(&userID) + if err == nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("User already exists")) + return + } else if err != sql.ErrNoRows { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + result, err := s.Db.Exec("INSERT INTO users (name, token) VALUES (?, ?)", t.Name, t.Token) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + userIDInt64, err := result.LastInsertId() + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + userID = int(userIDInt64) + + user := struct { + ID int `json:"id"` + Name string `json:"name"` + Token string `json:"token"` + }{ + ID: userID, + Name: t.Name, + Token: t.Token, + } + + responseJSON, err := json.Marshal(user) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err.Error()) + return + } + + s.Respond(w, r, http.StatusCreated, string(responseJSON)) + return + } +} + +func (s *UserController) DeleteUser() http.HandlerFunc { + type userStruct struct { + Token string `json:"token" binding:"required"` + } + + return func(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + + var t userStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + return + } + + rows, err := s.Db.Query("SELECT name FROM users WHERE token=? LIMIT 1", t.Token) + if err != nil { + rows.Close() + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + for rows.Next() { + var name string + err = rows.Scan(&name) + if err != nil { + rows.Close() + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + if name == "admin" { + rows.Close() + s.Respond(w, r, http.StatusBadRequest, errors.New("Cannot delete admin user")) + return + } + } + + _, err = s.Db.Query(fmt.Sprintf("delete from users where token='%s'", t.Token)) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + s.Respond(w, r, http.StatusOK, "User deleted") + return + } +} + +func (s *UserController) GetUserByToken() http.HandlerFunc { + type userResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Token string `json:"token"` + Webhook string `json:"webhook"` + Jid string `json:"jid"` + } + + return func(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + + var t struct { + Token string `json:"token" binding:"required"` + } + if err := decoder.Decode(&t); err != nil { + s.Respond(w, r, http.StatusBadRequest, err.Error()) + return + } + + var user userResponse + err := s.Db.QueryRow("SELECT id, name, token, webhook, jid FROM users WHERE token=? LIMIT 1", t.Token).Scan( + &user.ID, &user.Name, &user.Token, &user.Webhook, &user.Jid, + ) + if err != nil { + if err == sql.ErrNoRows { + s.Respond(w, r, http.StatusNotFound, "User not found") + return + } + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + responseJSON, err := json.Marshal(user) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err.Error()) + return + } + + s.Respond(w, r, http.StatusOK, string(responseJSON)) + } +} + // checks if users/phones are on Whatsapp func (s *UserController) CheckUser() http.HandlerFunc { From b8b634770d07a5c32ce905a58a7ace879b3b9865 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 11:09:29 -0300 Subject: [PATCH 13/24] Implemented WebhookController --- controllers/webhook/webhook_controller.go | 104 +++++++++++++++ handlers.go | 153 ---------------------- routes.go | 5 +- 3 files changed, 107 insertions(+), 155 deletions(-) create mode 100644 controllers/webhook/webhook_controller.go diff --git a/controllers/webhook/webhook_controller.go b/controllers/webhook/webhook_controller.go new file mode 100644 index 00000000..48e1b5bf --- /dev/null +++ b/controllers/webhook/webhook_controller.go @@ -0,0 +1,104 @@ +package webhook + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + controllerBase "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + + "github.com/justinas/alice" + "github.com/patrickmn/go-cache" +) + +type WebhookController struct { + *controllerBase.Controller +} + +func (s *WebhookController) SignRoutes(c alice.Chain) { + s.Router.Handle("/webhook", c.Then(s.SetWebhook())).Methods("POST") + s.Router.Handle("/webhook", c.Then(s.GetWebhook())).Methods("GET") +} + +// Gets WebHook +func (s *WebhookController) GetWebhook() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + webhook := "" + events := "" + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + + rows, err := s.Db.Query("SELECT webhook,events FROM users WHERE id=? LIMIT 1", txtid) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("Could not get webhook: %v", err)) + return + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&webhook, &events) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("Could not get webhook: %s", fmt.Sprintf("%s", err))) + return + } + } + err = rows.Err() + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("Could not get webhook: %s", fmt.Sprintf("%s", err))) + return + } + + eventarray := strings.Split(events, ",") + + response := map[string]interface{}{"webhook": webhook, "subscribe": eventarray} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} + +// Sets WebHook +func (s *WebhookController) SetWebhook() http.HandlerFunc { + type webhookStruct struct { + WebhookURL string + } + return func(w http.ResponseWriter, r *http.Request) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") + userid, _ := strconv.Atoi(txtid) + + decoder := json.NewDecoder(r.Body) + var t webhookStruct + err := decoder.Decode(&t) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("Could not set webhook: %v", err)) + return + } + var webhook = t.WebhookURL + + _, err = s.Db.Exec("UPDATE users SET webhook=? WHERE id=?", webhook, userid) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("%s", err)) + return + } + + v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Webhook", webhook) + s.UserInfoCache.Set(token, v, cache.NoExpiration) + + response := map[string]interface{}{"webhook": webhook} + responseJson, err := json.Marshal(response) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} diff --git a/handlers.go b/handlers.go index 32b117d8..5a19057b 100644 --- a/handlers.go +++ b/handlers.go @@ -1751,159 +1751,6 @@ func CreateAdminUser(db *sql.DB, token *string) { } } -func (s *server) CreateUser() http.HandlerFunc { - type userStruct struct { - Name string `json:"name" binding:"required"` - Token string `json:"token" binding:"required"` - } - - return func(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) - - var t userStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - var userID int - err = s.Db.QueryRow("SELECT id FROM users WHERE token=? LIMIT 1", t.Token).Scan(&userID) - if err == nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("User already exists")) - return - } else if err != sql.ErrNoRows { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - result, err := s.Db.Exec("INSERT INTO users (name, token) VALUES (?, ?)", t.Name, t.Token) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - userIDInt64, err := result.LastInsertId() - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - userID = int(userIDInt64) - - user := struct { - ID int `json:"id"` - Name string `json:"name"` - Token string `json:"token"` - }{ - ID: userID, - Name: t.Name, - Token: t.Token, - } - - responseJSON, err := json.Marshal(user) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err.Error()) - return - } - - s.Respond(w, r, http.StatusCreated, string(responseJSON)) - return - } -} - -func (s *server) DeleteUser() http.HandlerFunc { - type userStruct struct { - Token string `json:"token" binding:"required"` - } - - return func(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) - - var t userStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - rows, err := s.Db.Query("SELECT name FROM users WHERE token=? LIMIT 1", t.Token) - if err != nil { - rows.Close() - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - for rows.Next() { - var name string - err = rows.Scan(&name) - if err != nil { - rows.Close() - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - if name == "admin" { - rows.Close() - s.Respond(w, r, http.StatusBadRequest, errors.New("Cannot delete admin user")) - return - } - } - - _, err = s.Db.Query(fmt.Sprintf("delete from users where token='%s'", t.Token)) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - s.Respond(w, r, http.StatusOK, "User deleted") - return - } -} - -func (s *server) GetUserByToken() http.HandlerFunc { - type userResponse struct { - ID int `json:"id"` - Name string `json:"name"` - Token string `json:"token"` - Webhook string `json:"webhook"` - Jid string `json:"jid"` - } - - return func(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) - - var t struct { - Token string `json:"token" binding:"required"` - } - if err := decoder.Decode(&t); err != nil { - s.Respond(w, r, http.StatusBadRequest, err.Error()) - return - } - - var user userResponse - err := s.Db.QueryRow("SELECT id, name, token, webhook, jid FROM users WHERE token=? LIMIT 1", t.Token).Scan( - &user.ID, &user.Name, &user.Token, &user.Webhook, &user.Jid, - ) - if err != nil { - if err == sql.ErrNoRows { - s.Respond(w, r, http.StatusNotFound, "User not found") - return - } - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - responseJSON, err := json.Marshal(user) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err.Error()) - return - } - - s.Respond(w, r, http.StatusOK, string(responseJSON)) - } -} - // checks if users/phones are on Whatsapp func (s *server) CheckUser() http.HandlerFunc { diff --git a/routes.go b/routes.go index 05a2e8ed..f71c255a 100644 --- a/routes.go +++ b/routes.go @@ -9,6 +9,7 @@ import ( "wuzapi/controllers/group" "wuzapi/controllers/session" "wuzapi/controllers/user" + "wuzapi/controllers/webhook" internalTypes "wuzapi/internal/types" "github.com/justinas/alice" @@ -55,8 +56,8 @@ func (s *server) routes() { sessionController := &session.SessionController{Controller: s.Controller} sessionController.SignRoutes(c) - s.Router.Handle("/webhook", c.Then(s.SetWebhook())).Methods("POST") - s.Router.Handle("/webhook", c.Then(s.GetWebhook())).Methods("GET") + webhookController := &webhook.WebhookController{Controller: s.Controller} + webhookController.SignRoutes(c) chatController := &chat.ChatController{Controller: s.Controller} chatController.SignRoutes(c) From ddc9dc426a520f64dc112728be0b08107f5d5a8b Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 11:18:47 -0300 Subject: [PATCH 14/24] Deleted handlers.go and wmiau.go --- .../controller_base/controller_base.go | 117 +- handlers.go | 2822 ----------------- main.go | 55 +- routes.go | 17 +- wmiau.go | 269 -- 5 files changed, 168 insertions(+), 3112 deletions(-) delete mode 100644 handlers.go delete mode 100644 wmiau.go diff --git a/controllers/controller_base/controller_base.go b/controllers/controller_base/controller_base.go index 2c491b36..13ae18f7 100644 --- a/controllers/controller_base/controller_base.go +++ b/controllers/controller_base/controller_base.go @@ -303,7 +303,7 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc } } -func (s *Controller) Authalice(next http.Handler) http.Handler { +func (s *Controller) AuthAlice(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var ctx context.Context @@ -417,3 +417,118 @@ func (s *Controller) Auth(handler http.HandlerFunc) http.HandlerFunc { handler(w, r.WithContext(ctx)) } } + +func (s *Controller) authalice(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + var ctx context.Context + userid := 0 + txtid := "" + webhook := "" + jid := "" + events := "" + + // Get token from headers or uri parameters + token := r.Header.Get("token") + if token == "" { + token = strings.Join(r.URL.Query()["token"], "") + } + + myuserinfo, found := s.UserInfoCache.Get(token) + if !found { + log.Info().Msg("Looking for user information in DB") + // Checks DB from matching user and store user values in context + rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&txtid, &webhook, &jid, &events) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + userid, _ = strconv.Atoi(txtid) + v := internalTypes.Values{M: map[string]string{ + "Id": txtid, + "Jid": jid, + "Webhook": webhook, + "Token": token, + "Events": events, + }} + + s.UserInfoCache.Set(token, v, cache.NoExpiration) + ctx = context.WithValue(r.Context(), "userinfo", v) + } + } else { + ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) + userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) + } + + if userid == 0 { + s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) + return + } + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// Middleware: Authenticate connections based on Token header/uri parameter +func (s *Controller) auth(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + var ctx context.Context + userid := 0 + txtid := "" + webhook := "" + jid := "" + events := "" + + // Get token from headers or uri parameters + token := r.Header.Get("token") + if token == "" { + token = strings.Join(r.URL.Query()["token"], "") + } + + myuserinfo, found := s.UserInfoCache.Get(token) + if !found { + log.Info().Msg("Looking for user information in DB") + // Checks DB from matching user and store user values in context + rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&txtid, &webhook, &jid, &events) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + userid, _ = strconv.Atoi(txtid) + v := internalTypes.Values{M: map[string]string{ + "Id": txtid, + "Jid": jid, + "Webhook": webhook, + "Token": token, + "Events": events, + }} + + s.UserInfoCache.Set(token, v, cache.NoExpiration) + ctx = context.WithValue(r.Context(), "userinfo", v) + } + } else { + ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) + userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) + } + + if userid == 0 { + s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) + return + } + handler(w, r.WithContext(ctx)) + } +} diff --git a/handlers.go b/handlers.go deleted file mode 100644 index 5a19057b..00000000 --- a/handlers.go +++ /dev/null @@ -1,2822 +0,0 @@ -package main - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "os" - "strconv" - "strings" - "time" - "wuzapi/internal/helpers" - internalTypes "wuzapi/internal/types" - "wuzapi/message" - - "github.com/patrickmn/go-cache" - "github.com/vincent-petithory/dataurl" - "go.mau.fi/whatsmeow" - waProto "go.mau.fi/whatsmeow/binary/proto" - "go.mau.fi/whatsmeow/types" - "google.golang.org/protobuf/proto" -) - -var messageTypes = []string{"Message", "ReadReceipt", "Presence", "HistorySync", "ChatPresence", "All"} - -func (s *server) authalice(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - var ctx context.Context - userid := 0 - txtid := "" - webhook := "" - jid := "" - events := "" - - // Get token from headers or uri parameters - token := r.Header.Get("token") - if token == "" { - token = strings.Join(r.URL.Query()["token"], "") - } - - myuserinfo, found := userinfocache.Get(token) - if !found { - log.Info().Msg("Looking for user information in DB") - // Checks DB from matching user and store user values in context - rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&txtid, &webhook, &jid, &events) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - userid, _ = strconv.Atoi(txtid) - v := internalTypes.Values{M: map[string]string{ - "Id": txtid, - "Jid": jid, - "Webhook": webhook, - "Token": token, - "Events": events, - }} - - userinfocache.Set(token, v, cache.NoExpiration) - ctx = context.WithValue(r.Context(), "userinfo", v) - } - } else { - ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) - userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) - } - - if userid == 0 { - s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) - return - } - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -// Middleware: Authenticate connections based on Token header/uri parameter -func (s *server) auth(handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - var ctx context.Context - userid := 0 - txtid := "" - webhook := "" - jid := "" - events := "" - - // Get token from headers or uri parameters - token := r.Header.Get("token") - if token == "" { - token = strings.Join(r.URL.Query()["token"], "") - } - - myuserinfo, found := userinfocache.Get(token) - if !found { - log.Info().Msg("Looking for user information in DB") - // Checks DB from matching user and store user values in context - rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&txtid, &webhook, &jid, &events) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - userid, _ = strconv.Atoi(txtid) - v := internalTypes.Values{M: map[string]string{ - "Id": txtid, - "Jid": jid, - "Webhook": webhook, - "Token": token, - "Events": events, - }} - - userinfocache.Set(token, v, cache.NoExpiration) - ctx = context.WithValue(r.Context(), "userinfo", v) - } - } else { - ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) - userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) - } - - if userid == 0 { - s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) - return - } - handler(w, r.WithContext(ctx)) - } -} - -// Connects to Whatsapp Servers -func (s *server) Connect() http.HandlerFunc { - - type connectStruct struct { - Subscribe []string - Immediate bool - } - - return func(w http.ResponseWriter, r *http.Request) { - - webhook := r.Context().Value("userinfo").(internalTypes.Values).Get("Webhook") - jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") - userid, _ := strconv.Atoi(txtid) - eventstring := "" - - // Decodes request BODY looking for events to subscribe - decoder := json.NewDecoder(r.Body) - var t connectStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if clientPointer[userid] != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Already Connected")) - return - } else { - - var subscribedEvents []string - if len(t.Subscribe) < 1 { - if !helpers.Find(subscribedEvents, "All") { - subscribedEvents = append(subscribedEvents, "All") - } - } else { - for _, arg := range t.Subscribe { - if !helpers.Find(messageTypes, arg) { - log.Warn().Str("Type", arg).Msg("Message type discarded") - continue - } - if !helpers.Find(subscribedEvents, arg) { - subscribedEvents = append(subscribedEvents, arg) - } - } - } - eventstring = strings.Join(subscribedEvents, ",") - _, err = s.Db.Exec("UPDATE users SET events=? WHERE id=?", eventstring, userid) - if err != nil { - log.Warn().Msg("Could not set events in users table") - } - log.Info().Str("events", eventstring).Msg("Setting subscribed events") - v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Events", eventstring) - userinfocache.Set(token, v, cache.NoExpiration) - - log.Info().Str("jid", jid).Msg("Attempt to connect") - killchannel[userid] = make(chan bool) - go s.startClient(userid, jid, token, subscribedEvents) - - if t.Immediate == false { - log.Warn().Msg("Waiting 10 seconds") - time.Sleep(10000 * time.Millisecond) - - if clientPointer[userid] != nil { - if !clientPointer[userid].IsConnected() { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to Connect")) - return - } - } else { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to Connect")) - return - } - } - } - - response := map[string]interface{}{"webhook": webhook, "jid": jid, "events": eventstring, "details": "Connected!"} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - return - } - } -} - -// Disconnects from Whatsapp websocket, does not log out device -func (s *server) Disconnect() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") - token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - if clientPointer[userid].IsConnected() == true { - if clientPointer[userid].IsLoggedIn() == true { - log.Info().Str("jid", jid).Msg("Disconnection successfull") - killchannel[userid] <- true - _, err := s.Db.Exec("UPDATE users SET events=? WHERE id=?", "", userid) - if err != nil { - log.Warn().Str("userid", txtid).Msg("Could not set events in users table") - } - v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Events", "") - userinfocache.Set(token, v, cache.NoExpiration) - - response := map[string]interface{}{"Details": "Disconnected"} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } else { - log.Warn().Str("jid", jid).Msg("Ignoring disconnect as it was not connected") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Cannot disconnect because it is not logged in")) - return - } - } else { - log.Warn().Str("jid", jid).Msg("Ignoring disconnect as it was not connected") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Cannot disconnect because it is not logged in")) - return - } - } -} - -// Gets WebHook -func (s *server) GetWebhook() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - webhook := "" - events := "" - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - - rows, err := s.Db.Query("SELECT webhook,events FROM users WHERE id=? LIMIT 1", txtid) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not get webhook: %v", err))) - return - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&webhook, &events) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not get webhook: %s", fmt.Sprintf("%s", err)))) - return - } - } - err = rows.Err() - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not get webhook: %s", fmt.Sprintf("%s", err)))) - return - } - - eventarray := strings.Split(events, ",") - - response := map[string]interface{}{"webhook": webhook, "subscribe": eventarray} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sets WebHook -func (s *server) SetWebhook() http.HandlerFunc { - type webhookStruct struct { - WebhookURL string - } - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - token := r.Context().Value("userinfo").(internalTypes.Values).Get("Token") - userid, _ := strconv.Atoi(txtid) - - decoder := json.NewDecoder(r.Body) - var t webhookStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not set webhook: %v", err))) - return - } - var webhook = t.WebhookURL - - _, err = s.Db.Exec("UPDATE users SET webhook=? WHERE id=?", webhook, userid) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("%s", err))) - return - } - - v := helpers.UpdateUserInfo(r.Context().Value("userinfo"), "Webhook", webhook) - userinfocache.Set(token, v, cache.NoExpiration) - - response := map[string]interface{}{"webhook": webhook} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Gets QR code encoded in Base64 -func (s *server) GetQR() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - code := "" - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } else { - if clientPointer[userid].IsConnected() == false { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Not connected")) - return - } - rows, err := s.Db.Query("SELECT qrcode AS code FROM users WHERE id=? LIMIT 1", userid) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&code) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - } - err = rows.Err() - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - if clientPointer[userid].IsLoggedIn() == true { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Already Loggedin")) - return - } - } - - log.Info().Str("userid", txtid).Str("qrcode", code).Msg("Get QR successful") - response := map[string]interface{}{"QRCode": fmt.Sprintf("%s", code)} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Logs out device from Whatsapp (requires to scan QR next time) -func (s *server) Logout() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - jid := r.Context().Value("userinfo").(internalTypes.Values).Get("Jid") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } else { - if clientPointer[userid].IsLoggedIn() == true && clientPointer[userid].IsConnected() == true { - err := clientPointer[userid].Logout() - if err != nil { - log.Error().Str("jid", jid).Msg("Could not perform logout") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not perform logout")) - return - } else { - log.Info().Str("jid", jid).Msg("Logged out") - killchannel[userid] <- true - } - } else { - if clientPointer[userid].IsConnected() == true { - log.Warn().Str("jid", jid).Msg("Ignoring logout as it was not logged in") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not disconnect as it was not logged in")) - return - } else { - log.Warn().Str("jid", jid).Msg("Ignoring logout as it was not connected") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not disconnect as it was not connected")) - return - } - } - } - - response := map[string]interface{}{"Details": "Logged out"} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Gets Connected and LoggedIn Status -func (s *server) GetStatus() http.HandlerFunc { - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - isConnected := clientPointer[userid].IsConnected() - isLoggedIn := clientPointer[userid].IsLoggedIn() - - response := map[string]interface{}{"Connected": isConnected, "LoggedIn": isLoggedIn} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends a document/attachment message -func (s *server) SendDocument() http.HandlerFunc { - - type documentStruct struct { - message.Message - Document string - FileName string - Id string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - msgid := "" - var resp whatsmeow.SendResponse - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t documentStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Document == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Document in Payload")) - return - } - - if t.FileName == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing FileName in Payload")) - return - } - - recipient, err := t.ValidateMessageFields() - if err != nil { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, err) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - var uploaded whatsmeow.UploadResponse - var filedata []byte - - if t.Document[0:29] == "data:application/octet-stream" { - dataURL, err := dataurl.DecodeString(t.Document) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) - return - } else { - filedata = dataURL.Data - uploaded, err = clientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaDocument) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) - return - } - } - } else { - s.Respond(w, r, http.StatusBadRequest, errors.New("Document data should start with \"data:application/octet-stream;base64,\"")) - return - } - - msg := &waProto.Message{DocumentMessage: &waProto.DocumentMessage{ - Url: proto.String(uploaded.URL), - FileName: &t.FileName, - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - Mimetype: proto.String(http.DetectContentType(filedata)), - FileEncSha256: uploaded.FileEncSHA256, - FileSha256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(filedata))), - }} - - if t.ContextInfo.StanzaId != nil { - msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ - StanzaId: proto.String(*t.ContextInfo.StanzaId), - Participant: proto.String(*t.ContextInfo.Participant), - QuotedMessage: &waProto.Message{Conversation: proto.String("")}, - } - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends an audio message -func (s *server) SendAudio() http.HandlerFunc { - - type audioStruct struct { - message.Message - Audio string - Caption string - Id string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - msgid := "" - var resp whatsmeow.SendResponse - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t audioStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Audio == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Audio in Payload")) - return - } - - recipient, err := t.ValidateMessageFields() - if err != nil { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, err) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - var uploaded whatsmeow.UploadResponse - var filedata []byte - - if t.Audio[0:14] == "data:audio/ogg" { - dataURL, err := dataurl.DecodeString(t.Audio) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) - return - } else { - filedata = dataURL.Data - uploaded, err = clientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaAudio) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) - return - } - } - } else { - s.Respond(w, r, http.StatusBadRequest, errors.New("Audio data should start with \"data:audio/ogg;base64,\"")) - return - } - - ptt := true - mime := "audio/ogg; codecs=opus" - - msg := &waProto.Message{AudioMessage: &waProto.AudioMessage{ - Url: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - //Mimetype: proto.String(http.DetectContentType(filedata)), - Mimetype: &mime, - FileEncSha256: uploaded.FileEncSHA256, - FileSha256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(filedata))), - Ptt: &ptt, - }} - - if t.ContextInfo.StanzaId != nil { - msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ - StanzaId: proto.String(*t.ContextInfo.StanzaId), - Participant: proto.String(*t.ContextInfo.Participant), - QuotedMessage: &waProto.Message{Conversation: proto.String("")}, - } - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends an Image message -func (s *server) SendImage() http.HandlerFunc { - - type imageStruct struct { - message.Message - Image string - Caption string - Id string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - msgid := "" - var resp whatsmeow.SendResponse - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t imageStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Image == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Image in Payload")) - return - } - - recipient, err := t.ValidateMessageFields() - if err != nil { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, err) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - var uploaded whatsmeow.UploadResponse - var filedata []byte - - if t.Image[0:10] == "data:image" { - dataURL, err := dataurl.DecodeString(t.Image) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) - return - } else { - filedata = dataURL.Data - uploaded, err = clientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaImage) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) - return - } - } - } else { - s.Respond(w, r, http.StatusBadRequest, errors.New("Image data should start with \"data:image/png;base64,\"")) - return - } - - msg := &waProto.Message{ImageMessage: &waProto.ImageMessage{ - Caption: proto.String(t.Caption), - Url: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - Mimetype: proto.String(http.DetectContentType(filedata)), - FileEncSha256: uploaded.FileEncSHA256, - FileSha256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(filedata))), - }} - - if t.ContextInfo.StanzaId != nil { - msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ - StanzaId: proto.String(*t.ContextInfo.StanzaId), - Participant: proto.String(*t.ContextInfo.Participant), - QuotedMessage: &waProto.Message{Conversation: proto.String("")}, - } - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends Sticker message -func (s *server) SendSticker() http.HandlerFunc { - - type stickerStruct struct { - message.Message - Sticker string - Id string - PngThumbnail []byte - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - msgid := "" - var resp whatsmeow.SendResponse - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t stickerStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Sticker == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Sticker in Payload")) - return - } - - recipient, err := t.ValidateMessageFields() - if err != nil { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, err) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - var uploaded whatsmeow.UploadResponse - var filedata []byte - - if t.Sticker[0:4] == "data" { - dataURL, err := dataurl.DecodeString(t.Sticker) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) - return - } else { - filedata = dataURL.Data - uploaded, err = clientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaImage) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) - return - } - } - } else { - s.Respond(w, r, http.StatusBadRequest, errors.New("Data should start with \"data:mime/type;base64,\"")) - return - } - - msg := &waProto.Message{StickerMessage: &waProto.StickerMessage{ - Url: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - Mimetype: proto.String(http.DetectContentType(filedata)), - FileEncSha256: uploaded.FileEncSHA256, - FileSha256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(filedata))), - PngThumbnail: t.PngThumbnail, - }} - - if t.ContextInfo.StanzaId != nil { - msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ - StanzaId: proto.String(*t.ContextInfo.StanzaId), - Participant: proto.String(*t.ContextInfo.Participant), - QuotedMessage: &waProto.Message{Conversation: proto.String("")}, - } - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends Video message -func (s *server) SendVideo() http.HandlerFunc { - - type imageStruct struct { - message.Message - Video string - Caption string - Id string - JpegThumbnail []byte - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - msgid := "" - var resp whatsmeow.SendResponse - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t imageStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Video == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Video in Payload")) - return - } - - recipient, err := t.ValidateMessageFields() - if err != nil { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, err) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - var uploaded whatsmeow.UploadResponse - var filedata []byte - - if t.Video[0:4] == "data" { - dataURL, err := dataurl.DecodeString(t.Video) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) - return - } else { - filedata = dataURL.Data - uploaded, err = clientPointer[userid].Upload(context.Background(), filedata, whatsmeow.MediaVideo) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to upload file: %v", err))) - return - } - } - } else { - s.Respond(w, r, http.StatusBadRequest, errors.New("Data should start with \"data:mime/type;base64,\"")) - return - } - - msg := &waProto.Message{VideoMessage: &waProto.VideoMessage{ - Caption: proto.String(t.Caption), - Url: proto.String(uploaded.URL), - DirectPath: proto.String(uploaded.DirectPath), - MediaKey: uploaded.MediaKey, - Mimetype: proto.String(http.DetectContentType(filedata)), - FileEncSha256: uploaded.FileEncSHA256, - FileSha256: uploaded.FileSHA256, - FileLength: proto.Uint64(uint64(len(filedata))), - JpegThumbnail: t.JpegThumbnail, - }} - - if t.ContextInfo.StanzaId != nil { - msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ - StanzaId: proto.String(*t.ContextInfo.StanzaId), - Participant: proto.String(*t.ContextInfo.Participant), - QuotedMessage: &waProto.Message{Conversation: proto.String("")}, - } - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends Contact -func (s *server) SendContact() http.HandlerFunc { - - type contactStruct struct { - message.Message - Id string - Name string - Vcard string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - msgid := "" - var resp whatsmeow.SendResponse - - decoder := json.NewDecoder(r.Body) - var t contactStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - if t.Name == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Name in Payload")) - return - } - if t.Vcard == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Vcard in Payload")) - return - } - - recipient, err := t.ValidateMessageFields() - if err != nil { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, err) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - msg := &waProto.Message{ContactMessage: &waProto.ContactMessage{ - DisplayName: &t.Name, - Vcard: &t.Vcard, - }} - - if t.ContextInfo.StanzaId != nil { - msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ - StanzaId: proto.String(*t.ContextInfo.StanzaId), - Participant: proto.String(*t.ContextInfo.Participant), - QuotedMessage: &waProto.Message{Conversation: proto.String("")}, - } - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends location -func (s *server) SendLocation() http.HandlerFunc { - - type locationStruct struct { - message.Message - Id string - Name string - Latitude float64 - Longitude float64 - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - msgid := "" - var resp whatsmeow.SendResponse - - decoder := json.NewDecoder(r.Body) - var t locationStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - if t.Latitude == 0 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Latitude in Payload")) - return - } - if t.Longitude == 0 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Longitude in Payload")) - return - } - - recipient, err := t.ValidateMessageFields() - if err != nil { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, err) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - msg := &waProto.Message{LocationMessage: &waProto.LocationMessage{ - DegreesLatitude: &t.Latitude, - DegreesLongitude: &t.Longitude, - Name: &t.Name, - }} - - if t.ContextInfo.StanzaId != nil { - msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ - StanzaId: proto.String(*t.ContextInfo.StanzaId), - Participant: proto.String(*t.ContextInfo.Participant), - QuotedMessage: &waProto.Message{Conversation: proto.String("")}, - } - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends Buttons (not implemented, does not work) - -func (s *server) SendButtons() http.HandlerFunc { - - type buttonStruct struct { - ButtonId string - ButtonText string - } - type textStruct struct { - Phone string - Title string - Buttons []buttonStruct - Id string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - msgid := "" - var resp whatsmeow.SendResponse - - decoder := json.NewDecoder(r.Body) - var t textStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Title == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Title in Payload")) - return - } - - if len(t.Buttons) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("missing Buttons in Payload")) - return - } - if len(t.Buttons) > 3 { - s.Respond(w, r, http.StatusBadRequest, errors.New("buttons cant more than 3")) - return - } - - recipient, ok := helpers.ParseJID(t.Phone) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - var buttons []*waProto.ButtonsMessage_Button - - for _, item := range t.Buttons { - buttons = append(buttons, &waProto.ButtonsMessage_Button{ - ButtonId: proto.String(item.ButtonId), - ButtonText: &waProto.ButtonsMessage_Button_ButtonText{DisplayText: proto.String(item.ButtonText)}, - Type: waProto.ButtonsMessage_Button_RESPONSE.Enum(), - NativeFlowInfo: &waProto.ButtonsMessage_Button_NativeFlowInfo{}, - }) - } - - msg2 := &waProto.ButtonsMessage{ - ContentText: proto.String(t.Title), - HeaderType: waProto.ButtonsMessage_EMPTY.Enum(), - Buttons: buttons, - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, &waProto.Message{ViewOnceMessage: &waProto.FutureProofMessage{ - Message: &waProto.Message{ - ButtonsMessage: msg2, - }, - }}) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// SendList -// https://github.com/tulir/whatsmeow/issues/305 -func (s *server) SendList() http.HandlerFunc { - - type rowsStruct struct { - RowId string - Title string - Description string - } - - type sectionsStruct struct { - Title string - Rows []rowsStruct - } - - type listStruct struct { - Phone string - Title string - Description string - ButtonText string - FooterText string - Sections []sectionsStruct - Id string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("no session")) - return - } - - msgid := "" - var resp whatsmeow.SendResponse - - decoder := json.NewDecoder(r.Body) - var t listStruct - err := decoder.Decode(&t) - marshal, _ := json.Marshal(t) - fmt.Println(string(marshal)) - if err != nil { - fmt.Println(err) - s.Respond(w, r, http.StatusBadRequest, errors.New("could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("missing Phone in Payload")) - return - } - - if t.Title == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("missing Title in Payload")) - return - } - - if t.Description == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("missing Description in Payload")) - return - } - - if t.ButtonText == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("missing ButtonText in Payload")) - return - } - - if len(t.Sections) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("missing Sections in Payload")) - return - } - recipient, ok := helpers.ParseJID(t.Phone) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("could not parse Phone")) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - var sections []*waProto.ListMessage_Section - - for _, item := range t.Sections { - var rows []*waProto.ListMessage_Row - id := 1 - for _, row := range item.Rows { - var idtext string - if row.RowId == "" { - idtext = strconv.Itoa(id) - } else { - idtext = row.RowId - } - rows = append(rows, &waProto.ListMessage_Row{ - RowId: proto.String(idtext), - Title: proto.String(row.Title), - Description: proto.String(row.Description), - }) - } - - sections = append(sections, &waProto.ListMessage_Section{ - Title: proto.String(item.Title), - Rows: rows, - }) - } - msg1 := &waProto.ListMessage{ - Title: proto.String(t.Title), - Description: proto.String(t.Description), - ButtonText: proto.String(t.ButtonText), - ListType: waProto.ListMessage_SINGLE_SELECT.Enum(), - Sections: sections, - FooterText: proto.String(t.FooterText), - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, &waProto.Message{ - ViewOnceMessage: &waProto.FutureProofMessage{ - Message: &waProto.Message{ - ListMessage: msg1, - }, - }}) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Sends a regular text message -func (s *server) SendMessage() http.HandlerFunc { - - type textStruct struct { - message.Message - Body string - Id string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - msgid := "" - var resp whatsmeow.SendResponse - - decoder := json.NewDecoder(r.Body) - var t textStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Body == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Body in Payload")) - return - } - - recipient, err := t.ValidateMessageFields() - if err != nil { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, err) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - // msg := &waProto.Message{Conversation: &t.Body} - - msg := &waProto.Message{ - ExtendedTextMessage: &waProto.ExtendedTextMessage{ - Text: &t.Body, - }, - } - - if t.ContextInfo.StanzaId != nil { - msg.ExtendedTextMessage.ContextInfo = &waProto.ContextInfo{ - StanzaId: proto.String(*t.ContextInfo.StanzaId), - Participant: proto.String(*t.ContextInfo.Participant), - QuotedMessage: &waProto.Message{Conversation: proto.String("")}, - } - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - - return - } -} - -/* -// Sends a Template message -func (s *server) SendTemplate() http.HandlerFunc { - - type buttonStruct struct { - DisplayText string - Id string - Url string - PhoneNumber string - Type string - } - - type templateStruct struct { - Phone string - Content string - Footer string - Id string - Buttons []buttonStruct - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - msgid := "" - var resp whatsmeow.SendResponse -//var ts time.Time - - decoder := json.NewDecoder(r.Body) - var t templateStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Content == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Content in Payload")) - return - } - - if t.Footer == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Footer in Payload")) - return - } - - if len(t.Buttons) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Buttons in Payload")) - return - } - - recipient, ok := helpers.ParseJID(t.Phone) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) - return - } - - if t.Id == "" { - msgid = whatsmeow.GenerateMessageID() - } else { - msgid = t.Id - } - - var buttons []*waProto.HydratedTemplateButton - - id := 1 - for _, item := range t.Buttons { - switch item.Type { - case "quickreply": - var idtext string - text := item.DisplayText - if item.Id == "" { - idtext = strconv.Itoa(id) - } else { - idtext = item.Id - } - buttons = append(buttons, &waProto.HydratedTemplateButton{ - HydratedButton: &waProto.HydratedTemplateButton_QuickReplyButton{ - QuickReplyButton: &waProto.HydratedQuickReplyButton{ - DisplayText: &text, - Id: proto.String(idtext), - }, - }, - }) - case "url": - text := item.DisplayText - url := item.Url - buttons = append(buttons, &waProto.HydratedTemplateButton{ - HydratedButton: &waProto.HydratedTemplateButton_UrlButton{ - UrlButton: &waProto.HydratedURLButton{ - DisplayText: &text, - Url: &url, - }, - }, - }) - case "call": - text := item.DisplayText - phonenumber := item.PhoneNumber - buttons = append(buttons, &waProto.HydratedTemplateButton{ - HydratedButton: &waProto.HydratedTemplateButton_CallButton{ - CallButton: &waProto.HydratedCallButton{ - DisplayText: &text, - PhoneNumber: &phonenumber, - }, - }, - }) - default: - text := item.DisplayText - buttons = append(buttons, &waProto.HydratedTemplateButton{ - HydratedButton: &waProto.HydratedTemplateButton_QuickReplyButton{ - QuickReplyButton: &waProto.HydratedQuickReplyButton{ - DisplayText: &text, - Id: proto.String(string(id)), - }, - }, - }) - } - id++ - } - - msg := &waProto.Message{TemplateMessage: &waProto.TemplateMessage{ - HydratedTemplate: &waProto.HydratedFourRowTemplate{ - HydratedContentText: proto.String(t.Content), - HydratedFooterText: proto.String(t.Footer), - HydratedButtons: buttons, - TemplateId: proto.String("1"), - }, - }, - } - - resp, err = clientPointer[userid].SendMessage(context.Background(),recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} -*/ - -func CreateAdminUser(db *sql.DB, token *string) { - tx, err := db.Begin() - if err != nil { - log.Fatal().Err(err).Msg("Could not begin transaction") - os.Exit(1) - } - defer tx.Rollback() - - rows, err := tx.Query("SELECT id FROM users WHERE name='admin' LIMIT 1") - if err != nil { - log.Fatal().Err(err).Msg("Could not query users table") - os.Exit(1) - } - - if !rows.Next() { - sqlStmtInsert := fmt.Sprintf("INSERT INTO users (name, token) values ('admin', '%s')", *token) - _, err = tx.Exec(sqlStmtInsert) - if err != nil { - log.Fatal().Err(err).Msg("Could not insert admin user") - os.Exit(1) - } - } else { - sqlStmtUpdate := fmt.Sprintf("UPDATE users SET token='%s' WHERE name='admin'", *token) - _, err = tx.Exec(sqlStmtUpdate) - if err != nil { - log.Fatal().Err(err).Msg("Could not update admin user") - os.Exit(1) - } - } - - if err := tx.Commit(); err != nil { - log.Fatal().Err(err).Msg("Could not commit transaction") - os.Exit(1) - } -} - -// checks if users/phones are on Whatsapp -func (s *server) CheckUser() http.HandlerFunc { - - type checkUserStruct struct { - Phone []string - } - - type User struct { - Query string - IsInWhatsapp bool - JID string - VerifiedName string - } - - type UserCollection struct { - Users []User - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t checkUserStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if len(t.Phone) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - resp, err := clientPointer[userid].IsOnWhatsApp(t.Phone) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Failed to check if users are on WhatsApp: %s", err))) - return - } - - uc := new(UserCollection) - for _, item := range resp { - if item.VerifiedName != nil { - var msg = User{Query: item.Query, IsInWhatsapp: item.IsIn, JID: fmt.Sprintf("%s", item.JID), VerifiedName: item.VerifiedName.Details.GetVerifiedName()} - uc.Users = append(uc.Users, msg) - } else { - var msg = User{Query: item.Query, IsInWhatsapp: item.IsIn, JID: fmt.Sprintf("%s", item.JID), VerifiedName: ""} - uc.Users = append(uc.Users, msg) - } - } - responseJson, err := json.Marshal(uc) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Gets user information -func (s *server) GetUser() http.HandlerFunc { - - type checkUserStruct struct { - Phone []string - } - - type UserCollection struct { - Users map[types.JID]types.UserInfo - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t checkUserStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if len(t.Phone) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - var jids []types.JID - for _, arg := range t.Phone { - jid, ok := helpers.ParseJID(arg) - if !ok { - return - } - jids = append(jids, jid) - } - resp, err := clientPointer[userid].GetUserInfo(jids) - - if err != nil { - msg := fmt.Sprintf("Failed to get user info: %v", err) - log.Error().Msg(msg) - s.Respond(w, r, http.StatusInternalServerError, msg) - return - } - - uc := new(UserCollection) - uc.Users = make(map[types.JID]types.UserInfo) - - for jid, info := range resp { - uc.Users[jid] = info - } - - responseJson, err := json.Marshal(uc) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Gets avatar info for user -func (s *server) GetAvatar() http.HandlerFunc { - - type getAvatarStruct struct { - Phone string - Preview bool - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t getAvatarStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if len(t.Phone) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - jid, ok := helpers.ParseJID(t.Phone) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) - return - } - - var pic *types.ProfilePictureInfo - - existingID := "" - pic, err = clientPointer[userid].GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{ - Preview: t.Preview, - ExistingID: existingID, - }) - if err != nil { - msg := fmt.Sprintf("Failed to get avatar: %v", err) - log.Error().Msg(msg) - s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) - return - } - - if pic == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No avatar found")) - return - } - - log.Info().Str("id", pic.ID).Str("url", pic.URL).Msg("Got avatar") - - responseJson, err := json.Marshal(pic) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Gets all contacts -func (s *server) GetContacts() http.HandlerFunc { - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - result := map[types.JID]types.ContactInfo{} - result, err := clientPointer[userid].Store.Contacts.GetAllContacts() - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - responseJson, err := json.Marshal(result) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - - return - } -} - -// Sets Chat Presence (typing/paused/recording audio) -func (s *server) ChatPresence() http.HandlerFunc { - - type chatPresenceStruct struct { - Phone string - State string - Media types.ChatPresenceMedia - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t chatPresenceStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if len(t.Phone) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if len(t.State) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing State in Payload")) - return - } - - jid, ok := helpers.ParseJID(t.Phone) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Phone")) - return - } - - err = clientPointer[userid].SendChatPresence(jid, types.ChatPresence(t.State), types.ChatPresenceMedia(t.Media)) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Failure sending chat presence to Whatsapp servers")) - return - } - - response := map[string]interface{}{"Details": "Chat presence set successfuly"} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Downloads Image and returns base64 representation -func (s *server) DownloadImage() http.HandlerFunc { - - type downloadImageStruct struct { - Url string - DirectPath string - MediaKey []byte - Mimetype string - FileEncSHA256 []byte - FileSHA256 []byte - FileLength uint64 - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - mimetype := "" - var imgdata []byte - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) - _, err := os.Stat(userDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(userDirectory, 0751) - if errDir != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create user directory (%s)", userDirectory))) - return - } - } - - decoder := json.NewDecoder(r.Body) - var t downloadImageStruct - err = decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - msg := &waProto.Message{ImageMessage: &waProto.ImageMessage{ - Url: proto.String(t.Url), - DirectPath: proto.String(t.DirectPath), - MediaKey: t.MediaKey, - Mimetype: proto.String(t.Mimetype), - FileEncSha256: t.FileEncSHA256, - FileSha256: t.FileSHA256, - FileLength: &t.FileLength, - }} - - img := msg.GetImageMessage() - - if img != nil { - imgdata, err = clientPointer[userid].Download(img) - if err != nil { - log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to download image") - msg := fmt.Sprintf("Failed to download image %v", err) - s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) - return - } - mimetype = img.GetMimetype() - } - - dataURL := dataurl.New(imgdata, mimetype) - response := map[string]interface{}{"Mimetype": mimetype, "Data": dataURL.String()} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Downloads Document and returns base64 representation -func (s *server) DownloadDocument() http.HandlerFunc { - - type downloadDocumentStruct struct { - Url string - DirectPath string - MediaKey []byte - Mimetype string - FileEncSHA256 []byte - FileSHA256 []byte - FileLength uint64 - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - mimetype := "" - var docdata []byte - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) - _, err := os.Stat(userDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(userDirectory, 0751) - if errDir != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create user directory (%s)", userDirectory))) - return - } - } - - decoder := json.NewDecoder(r.Body) - var t downloadDocumentStruct - err = decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - msg := &waProto.Message{DocumentMessage: &waProto.DocumentMessage{ - Url: proto.String(t.Url), - DirectPath: proto.String(t.DirectPath), - MediaKey: t.MediaKey, - Mimetype: proto.String(t.Mimetype), - FileEncSha256: t.FileEncSHA256, - FileSha256: t.FileSHA256, - FileLength: &t.FileLength, - }} - - doc := msg.GetDocumentMessage() - - if doc != nil { - docdata, err = clientPointer[userid].Download(doc) - if err != nil { - log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to download document") - msg := fmt.Sprintf("Failed to download document %v", err) - s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) - return - } - mimetype = doc.GetMimetype() - } - - dataURL := dataurl.New(docdata, mimetype) - response := map[string]interface{}{"Mimetype": mimetype, "Data": dataURL.String()} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Downloads Video and returns base64 representation -func (s *server) DownloadVideo() http.HandlerFunc { - - type downloadVideoStruct struct { - Url string - DirectPath string - MediaKey []byte - Mimetype string - FileEncSHA256 []byte - FileSHA256 []byte - FileLength uint64 - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - mimetype := "" - var docdata []byte - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) - _, err := os.Stat(userDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(userDirectory, 0751) - if errDir != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create user directory (%s)", userDirectory))) - return - } - } - - decoder := json.NewDecoder(r.Body) - var t downloadVideoStruct - err = decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - msg := &waProto.Message{VideoMessage: &waProto.VideoMessage{ - Url: proto.String(t.Url), - DirectPath: proto.String(t.DirectPath), - MediaKey: t.MediaKey, - Mimetype: proto.String(t.Mimetype), - FileEncSha256: t.FileEncSHA256, - FileSha256: t.FileSHA256, - FileLength: &t.FileLength, - }} - - doc := msg.GetVideoMessage() - - if doc != nil { - docdata, err = clientPointer[userid].Download(doc) - if err != nil { - log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to download video") - msg := fmt.Sprintf("Failed to download video %v", err) - s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) - return - } - mimetype = doc.GetMimetype() - } - - dataURL := dataurl.New(docdata, mimetype) - response := map[string]interface{}{"Mimetype": mimetype, "Data": dataURL.String()} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// Downloads Audio and returns base64 representation -func (s *server) DownloadAudio() http.HandlerFunc { - - type downloadAudioStruct struct { - Url string - DirectPath string - MediaKey []byte - Mimetype string - FileEncSHA256 []byte - FileSHA256 []byte - FileLength uint64 - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - mimetype := "" - var docdata []byte - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - // check/creates user directory for files - userDirectory := fmt.Sprintf("%s/files/user_%s", s.ExPath, txtid) - _, err := os.Stat(userDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(userDirectory, 0751) - if errDir != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Could not create user directory (%s)", userDirectory))) - return - } - } - - decoder := json.NewDecoder(r.Body) - var t downloadAudioStruct - err = decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - msg := &waProto.Message{AudioMessage: &waProto.AudioMessage{ - Url: proto.String(t.Url), - DirectPath: proto.String(t.DirectPath), - MediaKey: t.MediaKey, - Mimetype: proto.String(t.Mimetype), - FileEncSha256: t.FileEncSHA256, - FileSha256: t.FileSHA256, - FileLength: &t.FileLength, - }} - - doc := msg.GetAudioMessage() - - if doc != nil { - docdata, err = clientPointer[userid].Download(doc) - if err != nil { - log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to download audio") - msg := fmt.Sprintf("Failed to download audio %v", err) - s.Respond(w, r, http.StatusInternalServerError, errors.New(msg)) - return - } - mimetype = doc.GetMimetype() - } - - dataURL := dataurl.New(docdata, mimetype) - response := map[string]interface{}{"Mimetype": mimetype, "Data": dataURL.String()} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// React -func (s *server) React() http.HandlerFunc { - - type textStruct struct { - Phone string - Body string - Id string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - msgid := "" - var resp whatsmeow.SendResponse - - decoder := json.NewDecoder(r.Body) - var t textStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Phone == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Phone in Payload")) - return - } - - if t.Body == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Body in Payload")) - return - } - - recipient, ok := helpers.ParseJID(t.Phone) - if !ok { - log.Error().Msg(fmt.Sprintf("%s", err)) - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) - return - } - - if t.Id == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Id in Payload")) - return - } else { - msgid = t.Id - } - - fromMe := false - if strings.HasPrefix(msgid, "me:") { - fromMe = true - msgid = msgid[len("me:"):] - } - reaction := t.Body - 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(msgid), - }, - Text: proto.String(reaction), - GroupingKey: proto.String(reaction), - SenderTimestampMs: proto.Int64(time.Now().UnixMilli()), - }, - } - - resp, err = clientPointer[userid].SendMessage(context.Background(), recipient, msg) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New(fmt.Sprintf("Error sending message: %v", err))) - return - } - - log.Info().Str("timestamp", fmt.Sprintf("%d", resp.Timestamp)).Str("id", msgid).Msg("Message sent") - response := map[string]interface{}{"Details": "Sent", "Timestamp": resp.Timestamp, "Id": msgid} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - - return - } -} - -// Mark messages as read -func (s *server) MarkRead() http.HandlerFunc { - - type markReadStruct struct { - Id []string - Chat types.JID - Sender types.JID - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t markReadStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - if t.Chat.String() == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Chat in Payload")) - return - } - - if len(t.Id) < 1 { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Id in Payload")) - return - } - - err = clientPointer[userid].MarkRead(t.Id, time.Now(), t.Chat, t.Sender) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Failure marking messages as read")) - return - } - - response := map[string]interface{}{"Details": "Message(s) marked as read"} - responseJson, err := json.Marshal(response) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - return - } -} - -// List groups -func (s *server) ListGroups() http.HandlerFunc { - - type GroupCollection struct { - Groups []types.GroupInfo - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - resp, err := clientPointer[userid].GetJoinedGroups() - - if err != nil { - msg := fmt.Sprintf("Failed to get group list: %v", err) - log.Error().Msg(msg) - s.Respond(w, r, http.StatusInternalServerError, msg) - return - } - - gc := new(GroupCollection) - for _, info := range resp { - gc.Groups = append(gc.Groups, *info) - } - - responseJson, err := json.Marshal(gc) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - - return - } -} - -// Get group info -func (s *server) GetGroupInfo() http.HandlerFunc { - - type getGroupInfoStruct struct { - GroupJID string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t getGroupInfoStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - group, ok := helpers.ParseJID(t.GroupJID) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) - return - } - - resp, err := clientPointer[userid].GetGroupInfo(group) - - if err != nil { - msg := fmt.Sprintf("Failed to get group info: %v", err) - log.Error().Msg(msg) - s.Respond(w, r, http.StatusInternalServerError, msg) - return - } - - responseJson, err := json.Marshal(resp) - - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - - return - } -} - -// Get group invite link -func (s *server) GetGroupInviteLink() http.HandlerFunc { - - type getGroupInfoStruct struct { - GroupJID string - Reset bool - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t getGroupInfoStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - group, ok := helpers.ParseJID(t.GroupJID) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) - return - } - - resp, err := clientPointer[userid].GetGroupInviteLink(group, t.Reset) - - if err != nil { - log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to get group invite link") - msg := fmt.Sprintf("Failed to get group invite link: %v", err) - s.Respond(w, r, http.StatusInternalServerError, msg) - return - } - - response := map[string]interface{}{"InviteLink": resp} - responseJson, err := json.Marshal(response) - - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - - return - } -} - -// Set group photo -func (s *server) SetGroupPhoto() http.HandlerFunc { - - type setGroupPhotoStruct struct { - GroupJID string - Image string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t setGroupPhotoStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - group, ok := helpers.ParseJID(t.GroupJID) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) - return - } - - if t.Image == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Image in Payload")) - return - } - - var filedata []byte - - if t.Image[0:13] == "data:image/jp" { - dataURL, err := dataurl.DecodeString(t.Image) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode base64 encoded data from payload")) - return - } else { - filedata = dataURL.Data - } - } else { - s.Respond(w, r, http.StatusBadRequest, errors.New("Image data should start with \"data:image/jpeg;base64,\"")) - return - } - - picture_id, err := clientPointer[userid].SetGroupPhoto(group, filedata) - - if err != nil { - log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to set group photo") - msg := fmt.Sprintf("Failed to set group photo: %v", err) - s.Respond(w, r, http.StatusInternalServerError, msg) - return - } - - response := map[string]interface{}{"Details": "Group Photo set successfully", "PictureID": picture_id} - responseJson, err := json.Marshal(response) - - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - - return - } -} - -// Set group name -func (s *server) SetGroupName() http.HandlerFunc { - - type setGroupNameStruct struct { - GroupJID string - Name string - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - userid, _ := strconv.Atoi(txtid) - - if clientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) - return - } - - decoder := json.NewDecoder(r.Body) - var t setGroupNameStruct - err := decoder.Decode(&t) - if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) - return - } - - group, ok := helpers.ParseJID(t.GroupJID) - if !ok { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not parse Group JID")) - return - } - - if t.Name == "" { - s.Respond(w, r, http.StatusBadRequest, errors.New("Missing Name in Payload")) - return - } - - err = clientPointer[userid].SetGroupName(group, t.Name) - - if err != nil { - log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Failed to set group name") - msg := fmt.Sprintf("Failed to set group name: %v", err) - s.Respond(w, r, http.StatusInternalServerError, msg) - return - } - - response := map[string]interface{}{"Details": "Group Name set successfully"} - responseJson, err := json.Marshal(response) - - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - } else { - s.Respond(w, r, http.StatusOK, string(responseJson)) - } - - return - } -} - -// Writes JSON response to API clients -func (s *server) Respond(w http.ResponseWriter, r *http.Request, status int, data interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - - dataenvelope := map[string]interface{}{"code": status} - if err, ok := data.(error); ok { - dataenvelope["error"] = err.Error() - dataenvelope["success"] = false - } else { - mydata := make(map[string]interface{}) - err = json.Unmarshal([]byte(data.(string)), &mydata) - if err != nil { - log.Error().Str("error", fmt.Sprintf("%v", err)).Msg("Error unmarshalling JSON") - } - dataenvelope["data"] = mydata - dataenvelope["success"] = true - } - data = dataenvelope - - if err := json.NewEncoder(w).Encode(data); err != nil { - panic("respond: " + err.Error()) - } -} diff --git a/main.go b/main.go index b43236ce..5b251fd6 100755 --- a/main.go +++ b/main.go @@ -22,10 +22,6 @@ import ( _ "modernc.org/sqlite" ) -type server struct { - *controllerBase.Controller -} - var ( address = flag.String("address", "0.0.0.0", "Bind IP Address") port = flag.String("port", "8080", "Listen Port") @@ -99,16 +95,15 @@ func main() { panic(err) } - s := &server{ - &controllerBase.Controller{ - Router: mux.NewRouter(), - Db: db, - ExPath: exPath, - }, + s := &controllerBase.Controller{ + Router: mux.NewRouter(), + Db: db, + ExPath: exPath, } - s.routes() + + routes(s) - s.connectOnStartup() + s.ConnectOnStartup() srv := &http.Server{ Addr: *address + ":" + *port, @@ -149,3 +144,39 @@ func main() { } log.Info().Msg("Server Exited Properly") } + +func CreateAdminUser(db *sql.DB, token *string) { + tx, err := db.Begin() + if err != nil { + log.Fatal().Err(err).Msg("Could not begin transaction") + os.Exit(1) + } + defer tx.Rollback() + + rows, err := tx.Query("SELECT id FROM users WHERE name='admin' LIMIT 1") + if err != nil { + log.Fatal().Err(err).Msg("Could not query users table") + os.Exit(1) + } + + if !rows.Next() { + sqlStmtInsert := fmt.Sprintf("INSERT INTO users (name, token) values ('admin', '%s')", *token) + _, err = tx.Exec(sqlStmtInsert) + if err != nil { + log.Fatal().Err(err).Msg("Could not insert admin user") + os.Exit(1) + } + } else { + sqlStmtUpdate := fmt.Sprintf("UPDATE users SET token='%s' WHERE name='admin'", *token) + _, err = tx.Exec(sqlStmtUpdate) + if err != nil { + log.Fatal().Err(err).Msg("Could not update admin user") + os.Exit(1) + } + } + + if err := tx.Commit(); err != nil { + log.Fatal().Err(err).Msg("Could not commit transaction") + os.Exit(1) + } +} \ No newline at end of file diff --git a/routes.go b/routes.go index f71c255a..9bc2eeac 100644 --- a/routes.go +++ b/routes.go @@ -6,6 +6,7 @@ import ( "path/filepath" "time" "wuzapi/controllers/chat" + controllerBase "wuzapi/controllers/controller_base" "wuzapi/controllers/group" "wuzapi/controllers/session" "wuzapi/controllers/user" @@ -19,7 +20,7 @@ import ( type Middleware = alice.Constructor -func (s *server) routes() { +func routes(s *controllerBase.Controller) { ex, err := os.Executable() if err != nil { @@ -35,7 +36,7 @@ func (s *server) routes() { } c := alice.New() - c = c.Append(s.authalice) + c = c.Append(s.AuthAlice) c = c.Append(hlog.NewHandler(log)) c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { @@ -53,22 +54,22 @@ func (s *server) routes() { c = c.Append(hlog.RefererHandler("referer")) c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id")) - sessionController := &session.SessionController{Controller: s.Controller} + sessionController := &session.SessionController{Controller: s} sessionController.SignRoutes(c) - webhookController := &webhook.WebhookController{Controller: s.Controller} + webhookController := &webhook.WebhookController{Controller: s} webhookController.SignRoutes(c) - chatController := &chat.ChatController{Controller: s.Controller} + chatController := &chat.ChatController{Controller: s} chatController.SignRoutes(c) - chatMessageController := &chat.ChatMessageController{Controller: s.Controller} + chatMessageController := &chat.ChatMessageController{Controller: s} chatMessageController.SignRoutes(c) - userController := &user.UserController{Controller: s.Controller} + userController := &user.UserController{Controller: s} userController.SignRoutes(c) - groupController := &group.GroupController{Controller: s.Controller} + groupController := &group.GroupController{Controller: s} groupController.SignRoutes(c) s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir(exPath + "/static/"))) diff --git a/wmiau.go b/wmiau.go deleted file mode 100644 index 3b59367f..00000000 --- a/wmiau.go +++ /dev/null @@ -1,269 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "encoding/base64" - "errors" - "fmt" - "os" - "strconv" - "strings" - "time" - "wuzapi/internal/helpers" - internalTypes "wuzapi/internal/types" - - "github.com/go-resty/resty/v2" - "github.com/mdp/qrterminal/v3" - "github.com/patrickmn/go-cache" - "github.com/skip2/go-qrcode" - "go.mau.fi/whatsmeow" - waProto "go.mau.fi/whatsmeow/binary/proto" - "go.mau.fi/whatsmeow/store" - _ "modernc.org/sqlite" - - // "go.mau.fi/whatsmeow/store/sqlstore" - - waLog "go.mau.fi/whatsmeow/util/log" - //"google.golang.org/protobuf/proto" -) - -// var wlog waLog.Logger -var clientPointer = make(map[int]*whatsmeow.Client) -var clientHttp = make(map[int]*resty.Client) -var historySyncID int32 - -// Connects to Whatsapp Websocket on server startup if last state was connected -func (s *server) connectOnStartup() { - rows, err := s.Db.Query("SELECT id,token,jid,webhook,events FROM users WHERE connected=1") - if err != nil { - log.Error().Err(err).Msg("DB Problem") - return - } - defer rows.Close() - for rows.Next() { - txtid := "" - token := "" - jid := "" - webhook := "" - events := "" - err = rows.Scan(&txtid, &token, &jid, &webhook, &events) - if err != nil { - log.Error().Err(err).Msg("DB Problem") - return - } else { - log.Info().Str("token", token).Msg("Connect to Whatsapp on startup") - v := internalTypes.Values{M: map[string]string{ - "Id": txtid, - "Jid": jid, - "Webhook": webhook, - "Token": token, - "Events": events, - }} - userinfocache.Set(token, v, cache.NoExpiration) - userid, _ := strconv.Atoi(txtid) - // Gets and set subscription to webhook events - eventarray := strings.Split(events, ",") - - var subscribedEvents []string - if len(eventarray) < 1 { - if !helpers.Find(subscribedEvents, "All") { - subscribedEvents = append(subscribedEvents, "All") - } - } else { - for _, arg := range eventarray { - if !helpers.Find(messageTypes, arg) { - log.Warn().Str("Type", arg).Msg("Message type discarded") - continue - } - if !helpers.Find(subscribedEvents, arg) { - subscribedEvents = append(subscribedEvents, arg) - } - } - } - eventstring := strings.Join(subscribedEvents, ",") - log.Info().Str("events", eventstring).Str("jid", jid).Msg("Attempt to connect") - killchannel[userid] = make(chan bool) - go s.startClient(userid, jid, token, subscribedEvents) - } - } - err = rows.Err() - if err != nil { - log.Error().Err(err).Msg("DB Problem") - } -} - -func (s *server) startClient(userID int, textjid string, token string, subscriptions []string) { - - log.Info().Str("userid", strconv.Itoa(userID)).Str("jid", textjid).Msg("Starting websocket connection to Whatsapp") - - var deviceStore *store.Device - var err error - - if clientPointer[userID] != nil { - isConnected := clientPointer[userID].IsConnected() - if isConnected == true { - return - } - } - - /* container is initialized on main to have just one connection and avoid sqlite locks - - dbDirectory := "dbdata" - _, err = os.Stat(dbDirectory) - if os.IsNotExist(err) { - errDir := os.MkdirAll(dbDirectory, 0751) - if errDir != nil { - panic("Could not create dbdata directory") - } - } - - var container *sqlstore.Container - - if(*waDebug!="") { - dbLog := waLog.Stdout("Database", *waDebug, true) - container, err = sqlstore.New("sqlite", "file:./dbdata/main.db?_foreign_keys=on", dbLog) - } else { - container, err = sqlstore.New("sqlite", "file:./dbdata/main.db?_foreign_keys=on", nil) - } - if err != nil { - panic(err) - } - */ - - if textjid != "" { - jid, _ := helpers.ParseJID(textjid) - // If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead. - //deviceStore, err := container.GetFirstDevice() - deviceStore, err = container.GetDevice(jid) - if err != nil { - panic(err) - } - } else { - log.Warn().Msg("No jid found. Creating new device") - deviceStore = container.NewDevice() - } - - if deviceStore == nil { - log.Warn().Msg("No store found. Creating new one") - deviceStore = container.NewDevice() - } - - //store.CompanionProps.PlatformType = waProto.CompanionProps_CHROME.Enum() - //store.CompanionProps.Os = proto.String("Mac OS") - - osName := "Mac OS 10" - store.DeviceProps.PlatformType = waProto.DeviceProps_UNKNOWN.Enum() - store.DeviceProps.Os = &osName - - clientLog := waLog.Stdout("Client", *waDebug, true) - var client *whatsmeow.Client - if *waDebug != "" { - client = whatsmeow.NewClient(deviceStore, clientLog) - } else { - client = whatsmeow.NewClient(deviceStore, nil) - } - clientPointer[userID] = client - - mycli := helpers.MyClient{ - WAClient: client, - EventHandlerID: 1, - UserID: userID, - Token: token, - Subscriptions: subscriptions, - Db: s.Db, - UserInfoCache: userinfocache, - KillChannel: killchannel, - ClientHttp: clientHttp, - } - - mycli.EventHandlerID = mycli.WAClient.AddEventHandler(mycli.MyEventHandler) - clientHttp[userID] = resty.New() - clientHttp[userID].SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) - if *waDebug == "DEBUG" { - clientHttp[userID].SetDebug(true) - } - clientHttp[userID].SetTimeout(5 * time.Second) - clientHttp[userID].SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) - - if client.Store.ID == nil { - // No ID stored, new login - - qrChan, err := client.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.Error().Err(err).Msg("Failed to get QR channel") - } - } else { - err = client.Connect() // Si no conectamos no se puede generar QR - if err != nil { - panic(err) - } - for evt := range qrChan { - if evt.Event == "code" { - // Display QR code in terminal (useful for testing/developing) - if *logType != "json" { - qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) - fmt.Println("QR code:\n", evt.Code) - } - // Store encoded/embeded base64 QR on database for retrieval with the /qr endpoint - image, _ := qrcode.Encode(evt.Code, qrcode.Medium, 256) - base64qrcode := "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) - sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.Db.Exec(sqlStmt, base64qrcode, userID) - if err != nil { - log.Error().Err(err).Msg(sqlStmt) - } - } else if evt.Event == "timeout" { - // Clear QR code from DB on timeout - sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.Db.Exec(sqlStmt, "", userID) - if err != nil { - log.Error().Err(err).Msg(sqlStmt) - } - log.Warn().Msg("QR timeout killing channel") - delete(clientPointer, userID) - killchannel[userID] <- true - } else if evt.Event == "success" { - log.Info().Msg("QR pairing ok!") - // Clear QR code after pairing - sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.Db.Exec(sqlStmt, "", userID) - if err != nil { - log.Error().Err(err).Msg(sqlStmt) - } - } else { - log.Info().Str("event", evt.Event).Msg("Login event") - } - } - } - - } else { - // Already logged in, just connect - log.Info().Msg("Already logged in, just connect") - err = client.Connect() - if err != nil { - panic(err) - } - } - - // Keep connected client live until disconnected/killed - for { - select { - case <-killchannel[userID]: - log.Info().Str("userid", strconv.Itoa(userID)).Msg("Received kill signal") - client.Disconnect() - delete(clientPointer, userID) - sqlStmt := `UPDATE users SET connected=0 WHERE id=?` - _, err := s.Db.Exec(sqlStmt, userID) - if err != nil { - log.Error().Err(err).Msg(sqlStmt) - } - return - default: - time.Sleep(1000 * time.Millisecond) - //log.Info().Str("jid",textjid).Msg("Loop the loop") - } - } -} From fd492dbc1dfc683ca7351c95a13f0af6e8944e3a Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sun, 10 Sep 2023 11:22:39 -0300 Subject: [PATCH 15/24] Deleted duplicated methods --- .../controller_base/controller_base.go | 115 ------------------ 1 file changed, 115 deletions(-) diff --git a/controllers/controller_base/controller_base.go b/controllers/controller_base/controller_base.go index 13ae18f7..0e093c3a 100644 --- a/controllers/controller_base/controller_base.go +++ b/controllers/controller_base/controller_base.go @@ -417,118 +417,3 @@ func (s *Controller) Auth(handler http.HandlerFunc) http.HandlerFunc { handler(w, r.WithContext(ctx)) } } - -func (s *Controller) authalice(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - var ctx context.Context - userid := 0 - txtid := "" - webhook := "" - jid := "" - events := "" - - // Get token from headers or uri parameters - token := r.Header.Get("token") - if token == "" { - token = strings.Join(r.URL.Query()["token"], "") - } - - myuserinfo, found := s.UserInfoCache.Get(token) - if !found { - log.Info().Msg("Looking for user information in DB") - // Checks DB from matching user and store user values in context - rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&txtid, &webhook, &jid, &events) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - userid, _ = strconv.Atoi(txtid) - v := internalTypes.Values{M: map[string]string{ - "Id": txtid, - "Jid": jid, - "Webhook": webhook, - "Token": token, - "Events": events, - }} - - s.UserInfoCache.Set(token, v, cache.NoExpiration) - ctx = context.WithValue(r.Context(), "userinfo", v) - } - } else { - ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) - userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) - } - - if userid == 0 { - s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) - return - } - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -// Middleware: Authenticate connections based on Token header/uri parameter -func (s *Controller) auth(handler http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - var ctx context.Context - userid := 0 - txtid := "" - webhook := "" - jid := "" - events := "" - - // Get token from headers or uri parameters - token := r.Header.Get("token") - if token == "" { - token = strings.Join(r.URL.Query()["token"], "") - } - - myuserinfo, found := s.UserInfoCache.Get(token) - if !found { - log.Info().Msg("Looking for user information in DB") - // Checks DB from matching user and store user values in context - rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&txtid, &webhook, &jid, &events) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - userid, _ = strconv.Atoi(txtid) - v := internalTypes.Values{M: map[string]string{ - "Id": txtid, - "Jid": jid, - "Webhook": webhook, - "Token": token, - "Events": events, - }} - - s.UserInfoCache.Set(token, v, cache.NoExpiration) - ctx = context.WithValue(r.Context(), "userinfo", v) - } - } else { - ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) - userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) - } - - if userid == 0 { - s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) - return - } - handler(w, r.WithContext(ctx)) - } -} From 082f6130212522664c03d47caea1aef8bb20dd01 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Mon, 11 Sep 2023 07:04:52 -0300 Subject: [PATCH 16/24] Dependency injection on ControllerBase fixed --- main.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 5b251fd6..4d3af7ca 100755 --- a/main.go +++ b/main.go @@ -13,9 +13,11 @@ import ( "time" controllerBase "wuzapi/controllers/controller_base" + "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/store/sqlstore" waLog "go.mau.fi/whatsmeow/util/log" + "github.com/go-resty/resty/v2" "github.com/gorilla/mux" "github.com/patrickmn/go-cache" "github.com/rs/zerolog" @@ -96,11 +98,18 @@ func main() { } s := &controllerBase.Controller{ - Router: mux.NewRouter(), - Db: db, - ExPath: exPath, + Router: mux.NewRouter(), + Db: db, + ExPath: exPath, + UserInfoCache: userinfocache, + KillChannel: killchannel, + ClientPointer: make(map[int]*whatsmeow.Client), + Container: container, + ClientHttp: make(map[int]*resty.Client), + WaDebug: waDebug, + LogType: logType, } - + routes(s) s.ConnectOnStartup() @@ -179,4 +188,4 @@ func CreateAdminUser(db *sql.DB, token *string) { log.Fatal().Err(err).Msg("Could not commit transaction") os.Exit(1) } -} \ No newline at end of file +} From 89cabd397277d66a24cc723f3b4df11600d75129 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Mon, 11 Sep 2023 07:40:42 -0300 Subject: [PATCH 17/24] Implementing user repository --- repository/user_repository.go | 119 ++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 repository/user_repository.go diff --git a/repository/user_repository.go b/repository/user_repository.go new file mode 100644 index 00000000..dc2a846a --- /dev/null +++ b/repository/user_repository.go @@ -0,0 +1,119 @@ +package repository + +import ( + "database/sql" + "fmt" + "strconv" + internalTypes "wuzapi/internal/types" +) + +type User struct { + Id int + Name string + Token string + Webhook string + Jid string + QrCode string + Connected *int + Expiration *int + Events string +} + +func (u User) ToValues() internalTypes.Values { + return internalTypes.Values{M: map[string]string{ + "Id": fmt.Sprint(u.Id), + "Jid": u.Jid, + "Webhook": u.Webhook, + "Token": u.Token, + "Events": u.Events, + }} +} + +type UserRepository interface { + GetUserByToken(token string) (*User, error) + GetConnectedUsers() ([]*User, error) + SetQrCode(qrCode string, userId int) error + SetJid(jid string, userId int) error + DisconnectUser(userId int) error + ConnectUser(userId int) error +} + +type userRepository struct { + Db *sql.DB +} + +func (r *userRepository) ConnectUser(userId int) error { + sqlStmt := `UPDATE users SET connected=1 WHERE id=?` + _, err := r.Db.Exec(sqlStmt, userId) + return err +} + +func (r *userRepository) DisconnectUser(userId int) error { + sqlStmt := `UPDATE users SET connected=0 WHERE id=?` + _, err := r.Db.Exec(sqlStmt, userId) + return err +} + +func (r *userRepository) GetConnectedUsers() ([]*User, error) { + rows, err := r.Db.Query("SELECT id,token,jid,webhook,events FROM users WHERE connected=1") + if err != nil { + return nil, err + } + defer rows.Close() + + users := []*User{} + for rows.Next() { + txtid := "" + token := "" + jid := "" + webhook := "" + events := "" + err = rows.Scan(&txtid, &token, &jid, &webhook, &events) + if err != nil { + return nil, err + } + id, err := strconv.Atoi(txtid) + if err != nil { + return nil, err + } + users = append(users, &User{Id: id, Token: token, Jid: jid, Webhook: webhook, Events: events}) + } + return users, nil +} + +func (r *userRepository) SetJid(jid string, userId int) error { + sqlStmt := `UPDATE users SET jid=? WHERE id=?` + _, err := r.Db.Exec(sqlStmt, jid, userId) + return err +} + +func (r *userRepository) SetQrCode(qrCode string, userId int) error { + sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` + _, err := r.Db.Exec(sqlStmt, qrCode, userId) + return err +} + +func NewUserRepository(db *sql.DB) UserRepository { + return &userRepository{Db: db} +} + +func (r *userRepository) GetUserByToken(token string) (*User, error) { + rows, err := r.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + txtid := "" + jid := "" + webhook := "" + events := "" + err = rows.Scan(&txtid, &webhook, &jid, &events) + if err != nil { + return nil, err + } + userid, _ := strconv.Atoi(txtid) + return &User{Id: userid, Webhook: webhook, Events: events}, nil + } + return nil, nil +} From f49a93ab956d33c1e8b5c3aceb649e446401b5ef Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 11 Sep 2023 16:54:07 -0300 Subject: [PATCH 18/24] fix: resolve conflicts --- internal/helpers/wmiau.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/helpers/wmiau.go b/internal/helpers/wmiau.go index bef26e6e..1cf83d46 100644 --- a/internal/helpers/wmiau.go +++ b/internal/helpers/wmiau.go @@ -126,7 +126,7 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { case *events.PairSuccess: postmap["type"] = "PairSuccess" dowebhook = 1 - log.Info().Str("userid", strconv.Itoa(mycli.userID)).Str("token", mycli.token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success") + log.Info().Str("userid", strconv.Itoa(mycli.UserID)).Str("token", mycli.Token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success") jid := evt.ID sqlStmt := `UPDATE users SET jid=? WHERE id=?` From b444842b2f89d299d29eaca50b34e33b318d3a77 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Mon, 11 Sep 2023 20:05:38 -0300 Subject: [PATCH 19/24] resty.Client instance creation fixed --- controllers/controller_base/controller_base.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/controller_base/controller_base.go b/controllers/controller_base/controller_base.go index 0e093c3a..a15dfd86 100644 --- a/controllers/controller_base/controller_base.go +++ b/controllers/controller_base/controller_base.go @@ -190,6 +190,7 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc store.DeviceProps.PlatformType = waProto.DeviceProps_UNKNOWN.Enum() store.DeviceProps.Os = &osName + s.ClientHttp[userID] = resty.New() var client *whatsmeow.Client if *s.WaDebug == "DEBUG" { client = whatsmeow.NewClient( @@ -215,7 +216,6 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc Db: s.Db, } mycli.EventHandlerID = mycli.WAClient.AddEventHandler(mycli.MyEventHandler) - s.ClientHttp[userID] = resty.New() s.ClientHttp[userID].SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) s.ClientHttp[userID].SetTimeout(5 * time.Second) From 4c8227ae1c9138f6cf0ce446621e35b29369e25b Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Tue, 12 Sep 2023 07:19:17 -0300 Subject: [PATCH 20/24] User repository implemented --- .../controller_base/controller_base.go | 149 +++++------------- controllers/session/session_controller.go | 49 +++--- controllers/user/user_controller.go | 76 +++------ controllers/webhook/webhook_controller.go | 28 +--- internal/helpers/wmiau.go | 19 +-- main.go | 3 +- repository/user_repository.go | 124 +++++++++++++-- 7 files changed, 202 insertions(+), 246 deletions(-) diff --git a/controllers/controller_base/controller_base.go b/controllers/controller_base/controller_base.go index a15dfd86..1c73159b 100644 --- a/controllers/controller_base/controller_base.go +++ b/controllers/controller_base/controller_base.go @@ -3,7 +3,6 @@ package controllerBase import ( "context" "crypto/tls" - "database/sql" "encoding/base64" "encoding/json" "errors" @@ -15,6 +14,7 @@ import ( "time" "wuzapi/internal/helpers" internalTypes "wuzapi/internal/types" + "wuzapi/repository" "github.com/go-resty/resty/v2" "github.com/gorilla/mux" @@ -30,7 +30,7 @@ import ( ) type Controller struct { - Db *sql.DB + Repository repository.UserRepository Router *mux.Router ExPath string ClientPointer map[int]*whatsmeow.Client @@ -69,61 +69,36 @@ func (s *Controller) Respond(w http.ResponseWriter, r *http.Request, status int, // Connects to Whatsapp Websocket on server startup if last state was connected func (s *Controller) ConnectOnStartup() { - rows, err := s.Db.Query("SELECT id,token,jid,webhook,events FROM users WHERE connected=1") + users, err := s.Repository.GetConnectedUsers() if err != nil { log.Error().Err(err).Msg("DB Problem") return } - defer rows.Close() - for rows.Next() { - txtid := "" - token := "" - jid := "" - webhook := "" - events := "" - err = rows.Scan(&txtid, &token, &jid, &webhook, &events) - if err != nil { - log.Error().Err(err).Msg("DB Problem") - return + for _, user := range users { + s.UserInfoCache.Set(user.Token, user.ToValues(), cache.NoExpiration) + + eventarray := strings.Split(user.Events, ",") + + var subscribedEvents []string + if len(eventarray) < 1 { + if !helpers.Find(subscribedEvents, "All") { + subscribedEvents = append(subscribedEvents, "All") + } } else { - log.Info().Str("token", token).Msg("Connect to Whatsapp on startup") - v := internalTypes.Values{M: map[string]string{ - "Id": txtid, - "Jid": jid, - "Webhook": webhook, - "Token": token, - "Events": events, - }} - s.UserInfoCache.Set(token, v, cache.NoExpiration) - userid, _ := strconv.Atoi(txtid) - // Gets and set subscription to webhook events - eventarray := strings.Split(events, ",") - - var subscribedEvents []string - if len(eventarray) < 1 { - if !helpers.Find(subscribedEvents, "All") { - subscribedEvents = append(subscribedEvents, "All") + for _, arg := range eventarray { + if !helpers.Find(internalTypes.MessageTypes, arg) { + log.Warn().Str("Type", arg).Msg("Message type discarded") + continue } - } else { - for _, arg := range eventarray { - if !helpers.Find(internalTypes.MessageTypes, arg) { - log.Warn().Str("Type", arg).Msg("Message type discarded") - continue - } - if !helpers.Find(subscribedEvents, arg) { - subscribedEvents = append(subscribedEvents, arg) - } + if !helpers.Find(subscribedEvents, arg) { + subscribedEvents = append(subscribedEvents, arg) } } - eventstring := strings.Join(subscribedEvents, ",") - log.Info().Str("events", eventstring).Str("jid", jid).Msg("Attempt to connect") - s.KillChannel[userid] = make(chan bool) - go s.StartClient(userid, jid, token, subscribedEvents) } - } - err = rows.Err() - if err != nil { - log.Error().Err(err).Msg("DB Problem") + eventstring := strings.Join(subscribedEvents, ",") + log.Info().Str("events", eventstring).Str("jid", user.Jid).Msg("Attempt to connect") + s.KillChannel[user.Id] = make(chan bool) + go s.StartClient(user.Id, user.Jid, user.Token, subscribedEvents) } } @@ -213,7 +188,7 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc Subscriptions: subscriptions, UserInfoCache: s.UserInfoCache, KillChannel: s.KillChannel, - Db: s.Db, + Repository: s.Repository, } mycli.EventHandlerID = mycli.WAClient.AddEventHandler(mycli.MyEventHandler) s.ClientHttp[userID].SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) @@ -245,17 +220,15 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc // Store encoded/embeded base64 QR on database for retrieval with the /qr endpoint image, _ := qrcode.Encode(evt.Code, qrcode.Medium, 256) base64qrcode := "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) - sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.Db.Exec(sqlStmt, base64qrcode, userID) + err := s.Repository.SetQrCode(base64qrcode, userID) if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) } } else if evt.Event == "timeout" { // Clear QR code from DB on timeout - sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.Db.Exec(sqlStmt, "", userID) + err := s.Repository.SetQrCode("", userID) if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) } log.Warn().Msg("QR timeout killing channel") delete(s.ClientPointer, userID) @@ -263,10 +236,9 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc } else if evt.Event == "success" { log.Info().Msg("QR pairing ok!") // Clear QR code after pairing - sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := s.Db.Exec(sqlStmt, "", userID) + err := s.Repository.SetQrCode("", userID) if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) } } else { log.Info().Str("event", evt.Event).Msg("Login event") @@ -290,10 +262,9 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc log.Info().Str("userid", strconv.Itoa(userID)).Msg("Received kill signal") client.Disconnect() delete(s.ClientPointer, userID) - sqlStmt := `UPDATE users SET connected=0 WHERE id=?` - _, err := s.Db.Exec(sqlStmt, userID) + err := s.Repository.DisconnectUser(userID) if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) } return default: @@ -308,10 +279,6 @@ func (s *Controller) AuthAlice(next http.Handler) http.Handler { var ctx context.Context userid := 0 - txtid := "" - webhook := "" - jid := "" - events := "" // Get token from headers or uri parameters token := r.Header.Get("token") @@ -323,30 +290,14 @@ func (s *Controller) AuthAlice(next http.Handler) http.Handler { if !found { log.Info().Msg("Looking for user information in DB") // Checks DB from matching user and store user values in context - rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + user, err := s.Repository.GetUserByToken(token) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&txtid, &webhook, &jid, &events) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - userid, _ = strconv.Atoi(txtid) - v := internalTypes.Values{M: map[string]string{ - "Id": txtid, - "Jid": jid, - "Webhook": webhook, - "Token": token, - "Events": events, - }} - - s.UserInfoCache.Set(token, v, cache.NoExpiration) - ctx = context.WithValue(r.Context(), "userinfo", v) - } + v := user.ToValues() + s.UserInfoCache.Set(token, v, cache.NoExpiration) + ctx = context.WithValue(r.Context(), "userinfo", v) } else { ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) @@ -366,10 +317,6 @@ func (s *Controller) Auth(handler http.HandlerFunc) http.HandlerFunc { var ctx context.Context userid := 0 - txtid := "" - webhook := "" - jid := "" - events := "" // Get token from headers or uri parameters token := r.Header.Get("token") @@ -381,30 +328,14 @@ func (s *Controller) Auth(handler http.HandlerFunc) http.HandlerFunc { if !found { log.Info().Msg("Looking for user information in DB") // Checks DB from matching user and store user values in context - rows, err := s.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + user, err := s.Repository.GetUserByToken(token) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&txtid, &webhook, &jid, &events) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - userid, _ = strconv.Atoi(txtid) - v := internalTypes.Values{M: map[string]string{ - "Id": txtid, - "Jid": jid, - "Webhook": webhook, - "Token": token, - "Events": events, - }} - - s.UserInfoCache.Set(token, v, cache.NoExpiration) - ctx = context.WithValue(r.Context(), "userinfo", v) - } + v := user.ToValues() + s.UserInfoCache.Set(token, v, cache.NoExpiration) + ctx = context.WithValue(r.Context(), "userinfo", v) } else { ctx = context.WithValue(r.Context(), "userinfo", myuserinfo) userid, _ = strconv.Atoi(myuserinfo.(internalTypes.Values).Get("Id")) diff --git a/controllers/session/session_controller.go b/controllers/session/session_controller.go index 3a56db1a..51dc54e8 100644 --- a/controllers/session/session_controller.go +++ b/controllers/session/session_controller.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" "time" - "wuzapi/controllers/controller_base" + controllerBase "wuzapi/controllers/controller_base" "wuzapi/internal/helpers" internalTypes "wuzapi/internal/types" @@ -77,7 +77,7 @@ func (s *SessionController) Connect() http.HandlerFunc { } } eventstring = strings.Join(subscribedEvents, ",") - _, err = s.Db.Exec("UPDATE users SET events=? WHERE id=?", eventstring, userid) + err = s.Repository.SetEvents(eventstring, userid) if err != nil { log.Warn().Msg("Could not set events in users table") } @@ -134,7 +134,7 @@ func (s *SessionController) Disconnect() http.HandlerFunc { if s.ClientPointer[userid].IsLoggedIn() == true { log.Info().Str("jid", jid).Msg("Disconnection successfull") s.KillChannel[userid] <- true - _, err := s.Db.Exec("UPDATE users SET events=? WHERE id=?", "", userid) + err := s.Repository.SetEvents("", userid) if err != nil { log.Warn().Str("userid", txtid).Msg("Could not set events in users table") } @@ -246,35 +246,24 @@ func (s *SessionController) GetQR() http.HandlerFunc { if s.ClientPointer[userid] == nil { s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) return - } else { - if s.ClientPointer[userid].IsConnected() == false { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Not connected")) - return - } - rows, err := s.Db.Query("SELECT qrcode AS code FROM users WHERE id=? LIMIT 1", userid) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&code) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - } - err = rows.Err() - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - if s.ClientPointer[userid].IsLoggedIn() == true { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Already Loggedin")) - return - } } + if s.ClientPointer[userid].IsConnected() == false { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Not connected")) + return + } + + if s.ClientPointer[userid].IsLoggedIn() == true { + s.Respond(w, r, http.StatusInternalServerError, errors.New("Already Loggedin")) + return + } + + code, err := s.Repository.GetQrCode(userid) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + log.Info().Str("userid", txtid).Str("qrcode", code).Msg("Get QR successful") response := map[string]interface{}{"QRCode": fmt.Sprintf("%s", code)} responseJson, err := json.Marshal(response) diff --git a/controllers/user/user_controller.go b/controllers/user/user_controller.go index 03ae58b5..3b08b496 100644 --- a/controllers/user/user_controller.go +++ b/controllers/user/user_controller.go @@ -10,6 +10,7 @@ import ( controllerBase "wuzapi/controllers/controller_base" "wuzapi/internal/helpers" internalTypes "wuzapi/internal/types" + "wuzapi/repository" "github.com/justinas/alice" "github.com/rs/zerolog/log" @@ -47,41 +48,26 @@ func (s *UserController) CreateUser() http.HandlerFunc { return } - var userID int - err = s.Db.QueryRow("SELECT id FROM users WHERE token=? LIMIT 1", t.Token).Scan(&userID) + user, err := s.Repository.CreateUser(&repository.User{Name: t.Name, Token: t.Token}) if err == nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("User already exists")) + s.Respond(w, r, http.StatusBadRequest, err.Error()) return } else if err != sql.ErrNoRows { s.Respond(w, r, http.StatusInternalServerError, err) return } - result, err := s.Db.Exec("INSERT INTO users (name, token) VALUES (?, ?)", t.Name, t.Token) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - userIDInt64, err := result.LastInsertId() - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - userID = int(userIDInt64) - - user := struct { + userResponse := struct { ID int `json:"id"` Name string `json:"name"` Token string `json:"token"` }{ - ID: userID, - Name: t.Name, - Token: t.Token, + ID: user.Id, + Name: user.Name, + Token: user.Token, } - responseJSON, err := json.Marshal(user) + responseJSON, err := json.Marshal(userResponse) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err.Error()) return @@ -107,30 +93,7 @@ func (s *UserController) DeleteUser() http.HandlerFunc { return } - rows, err := s.Db.Query("SELECT name FROM users WHERE token=? LIMIT 1", t.Token) - if err != nil { - rows.Close() - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - for rows.Next() { - var name string - err = rows.Scan(&name) - if err != nil { - rows.Close() - s.Respond(w, r, http.StatusInternalServerError, err) - return - } - - if name == "admin" { - rows.Close() - s.Respond(w, r, http.StatusBadRequest, errors.New("Cannot delete admin user")) - return - } - } - - _, err = s.Db.Query(fmt.Sprintf("delete from users where token='%s'", t.Token)) + err = s.Repository.DeleteUserByToken(t.Token) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return @@ -161,20 +124,17 @@ func (s *UserController) GetUserByToken() http.HandlerFunc { return } - var user userResponse - err := s.Db.QueryRow("SELECT id, name, token, webhook, jid FROM users WHERE token=? LIMIT 1", t.Token).Scan( - &user.ID, &user.Name, &user.Token, &user.Webhook, &user.Jid, - ) - if err != nil { - if err == sql.ErrNoRows { - s.Respond(w, r, http.StatusNotFound, "User not found") - return - } - s.Respond(w, r, http.StatusInternalServerError, err) - return + user, err := s.Repository.GetUserByToken(t.Token) + + userResponse := &userResponse{ + ID: user.Id, + Name: user.Name, + Token: user.Token, + Webhook: user.Webhook, + Jid: user.Jid, } - responseJSON, err := json.Marshal(user) + responseJSON, err := json.Marshal(userResponse) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err.Error()) return diff --git a/controllers/webhook/webhook_controller.go b/controllers/webhook/webhook_controller.go index 48e1b5bf..bd0bc072 100644 --- a/controllers/webhook/webhook_controller.go +++ b/controllers/webhook/webhook_controller.go @@ -27,32 +27,16 @@ func (s *WebhookController) SignRoutes(c alice.Chain) { func (s *WebhookController) GetWebhook() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - webhook := "" - events := "" txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") - - rows, err := s.Db.Query("SELECT webhook,events FROM users WHERE id=? LIMIT 1", txtid) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("Could not get webhook: %v", err)) - return - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&webhook, &events) - if err != nil { - s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("Could not get webhook: %s", fmt.Sprintf("%s", err))) - return - } - } - err = rows.Err() + id, err := strconv.Atoi(txtid) if err != nil { - s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("Could not get webhook: %s", fmt.Sprintf("%s", err))) - return + s.Respond(w, r, http.StatusBadRequest, fmt.Errorf("Invalid Id")) } + user, err := s.Repository.GetUserById(id) - eventarray := strings.Split(events, ",") + eventarray := strings.Split(user.Events, ",") - response := map[string]interface{}{"webhook": webhook, "subscribe": eventarray} + response := map[string]interface{}{"webhook": user.Webhook, "subscribe": eventarray} responseJson, err := json.Marshal(response) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) @@ -83,7 +67,7 @@ func (s *WebhookController) SetWebhook() http.HandlerFunc { } var webhook = t.WebhookURL - _, err = s.Db.Exec("UPDATE users SET webhook=? WHERE id=?", webhook, userid) + err = s.Repository.SetWebhook(webhook, userid) if err != nil { s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("%s", err)) return diff --git a/internal/helpers/wmiau.go b/internal/helpers/wmiau.go index 1cf83d46..4038cbc2 100644 --- a/internal/helpers/wmiau.go +++ b/internal/helpers/wmiau.go @@ -1,7 +1,6 @@ package helpers import ( - "database/sql" "encoding/base64" "encoding/json" "fmt" @@ -12,6 +11,7 @@ import ( "strings" "sync/atomic" internalTypes "wuzapi/internal/types" + "wuzapi/repository" "wuzapi/webhook" "github.com/go-resty/resty/v2" @@ -72,7 +72,7 @@ type MyClient struct { UserID int Token string Subscriptions []string - Db *sql.DB + Repository repository.UserRepository UserInfoCache *cache.Cache KillChannel map[int](chan bool) ClientHttp map[int]*resty.Client @@ -117,10 +117,9 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { } else { log.Info().Msg("Marked self as available") } - sqlStmt := `UPDATE users SET connected=1 WHERE id=?` - _, err = mycli.Db.Exec(sqlStmt, mycli.UserID) + err = mycli.Repository.ConnectUser(mycli.UserID) if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) return } case *events.PairSuccess: @@ -129,10 +128,9 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { log.Info().Str("userid", strconv.Itoa(mycli.UserID)).Str("token", mycli.Token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success") jid := evt.ID - sqlStmt := `UPDATE users SET jid=? WHERE id=?` - _, err := mycli.Db.Exec(sqlStmt, jid, mycli.UserID) + err := mycli.Repository.SetJid(jid.String(), mycli.UserID) if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) return } @@ -335,10 +333,9 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { log.Info().Str("reason", evt.Reason.String()).Msg("Logged out") mycli.KillChannel[mycli.UserID] <- true - sqlStmt := `UPDATE users SET connected=0 WHERE id=?` - _, err := mycli.Db.Exec(sqlStmt, mycli.UserID) + err := mycli.Repository.DisconnectUser(mycli.UserID) if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) return } case *events.ChatPresence: diff --git a/main.go b/main.go index 4d3af7ca..3c24d937 100755 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "syscall" "time" controllerBase "wuzapi/controllers/controller_base" + "wuzapi/repository" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/store/sqlstore" @@ -99,7 +100,7 @@ func main() { s := &controllerBase.Controller{ Router: mux.NewRouter(), - Db: db, + Repository: repository.NewUserRepository(db), ExPath: exPath, UserInfoCache: userinfocache, KillChannel: killchannel, diff --git a/repository/user_repository.go b/repository/user_repository.go index dc2a846a..a1fab45d 100644 --- a/repository/user_repository.go +++ b/repository/user_repository.go @@ -2,6 +2,7 @@ package repository import ( "database/sql" + "errors" "fmt" "strconv" internalTypes "wuzapi/internal/types" @@ -30,27 +31,47 @@ func (u User) ToValues() internalTypes.Values { } type UserRepository interface { + CreateUser(user *User) (*User, error) GetUserByToken(token string) (*User, error) + GetUserById(userId int) (*User, error) GetConnectedUsers() ([]*User, error) + DeleteUserByToken(token string) error + GetQrCode(userId int) (string, error) SetQrCode(qrCode string, userId int) error SetJid(jid string, userId int) error - DisconnectUser(userId int) error + SetEvents(events string, userId int) error + SetWebhook(webhooks string, userId int) error ConnectUser(userId int) error + DisconnectUser(userId int) error } type userRepository struct { Db *sql.DB } +func NewUserRepository(db *sql.DB) UserRepository { + return &userRepository{Db: db} +} + +func (r *userRepository) CreateUser(user *User) (*User, error) { + if user, _ := r.GetUserByToken(user.Token); user != nil { + return nil, errors.New("User already exists") + } + + _, err := r.Db.Exec("INSERT INTO users (name, token) VALUES (?, ?)", user.Name, user.Token) + if err != nil { + return nil, err + } + return r.GetUserByToken(user.Token) +} + func (r *userRepository) ConnectUser(userId int) error { - sqlStmt := `UPDATE users SET connected=1 WHERE id=?` - _, err := r.Db.Exec(sqlStmt, userId) + _, err := r.Db.Exec("UPDATE users SET connected=1 WHERE id=?", userId) return err } func (r *userRepository) DisconnectUser(userId int) error { - sqlStmt := `UPDATE users SET connected=0 WHERE id=?` - _, err := r.Db.Exec(sqlStmt, userId) + _, err := r.Db.Exec("UPDATE users SET connected=0 WHERE id=?", userId) return err } @@ -81,24 +102,67 @@ func (r *userRepository) GetConnectedUsers() ([]*User, error) { return users, nil } +func (r *userRepository) DeleteUserByToken(token string) error { + rows, err := r.Db.Query("SELECT name FROM users WHERE token=? LIMIT 1", token) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var name string + err = rows.Scan(&name) + if err != nil { + return err + } + + if name == "admin" { + return errors.New("Cannot delete admin user") + } + } + + _, err = r.Db.Query(fmt.Sprintf("delete from users where token='%s'", token)) + return err +} + func (r *userRepository) SetJid(jid string, userId int) error { - sqlStmt := `UPDATE users SET jid=? WHERE id=?` - _, err := r.Db.Exec(sqlStmt, jid, userId) + _, err := r.Db.Exec("UPDATE users SET jid=? WHERE id=?", jid, userId) return err } -func (r *userRepository) SetQrCode(qrCode string, userId int) error { - sqlStmt := `UPDATE users SET qrcode=? WHERE id=?` - _, err := r.Db.Exec(sqlStmt, qrCode, userId) +func (r *userRepository) SetEvents(events string, userId int) error { + _, err := r.Db.Exec("UPDATE users SET events=? WHERE id=?", events, userId) return err } -func NewUserRepository(db *sql.DB) UserRepository { - return &userRepository{Db: db} +func (r *userRepository)SetWebhook(webhooks string, userId int) error { + _, err := r.Db.Exec("UPDATE users SET webhook=? WHERE id=?", webhooks, userId) + return err +} + +func (r *userRepository) GetQrCode(userId int) (string, error) { + var code string + rows, err := r.Db.Query("SELECT qrcode AS code FROM users WHERE id=? LIMIT 1", userId) + if err != nil { + return "", err + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&code) + if err != nil { + return "", err + } + } + return code, nil +} + +func (r *userRepository) SetQrCode(qrCode string, userId int) error { + _, err := r.Db.Exec("UPDATE users SET qrcode=? WHERE id=?", qrCode, userId) + return err } func (r *userRepository) GetUserByToken(token string) (*User, error) { - rows, err := r.Db.Query("SELECT id,webhook,jid,events FROM users WHERE token=? LIMIT 1", token) + rows, err := r.Db.Query("SELECT id,webhook,jid,events,name,token FROM users WHERE token=? LIMIT 1", token) if err != nil { return nil, err } @@ -108,12 +172,42 @@ func (r *userRepository) GetUserByToken(token string) (*User, error) { jid := "" webhook := "" events := "" - err = rows.Scan(&txtid, &webhook, &jid, &events) + name := "" + token := "" + err = rows.Scan(&txtid, &webhook, &jid, &events, &name, &token) if err != nil { return nil, err } userid, _ := strconv.Atoi(txtid) - return &User{Id: userid, Webhook: webhook, Events: events}, nil + return &User{Id: userid, Name: name, Token: token, Webhook: webhook, Events: events}, nil } return nil, nil } + +func (r *userRepository) GetUserById(userId int) (*User, error) { + + name := "" + token := "" + webhook := "" + jid := "" + qrcode := "" + events := "" + + rows, err := r.Db.Query("SELECT name,token,webhook,jid,qrcode,events FROM users WHERE id=? LIMIT 1", userId) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + err = rows.Scan(&name, &token, &webhook, &jid, &qrcode, &events) + if err != nil { + return nil, err + } + } + err = rows.Err() + if err != nil { + return nil, err + } + + return &User{Id: userId, Webhook: webhook, Events: events}, nil +} From 6bb21845e1bb6ccbee3058367712c4ae1048fc16 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sat, 16 Sep 2023 11:25:13 -0300 Subject: [PATCH 21/24] Implemented UserRepository with gorm --- go.mod | 25 ++---- go.sum | 52 +++-------- main.go | 81 ++++++----------- repository/user_repository.go | 165 +++++++++++----------------------- routes.go | 6 +- 5 files changed, 99 insertions(+), 230 deletions(-) diff --git a/go.mod b/go.mod index 704c4aa8..4a9c3e6c 100644 --- a/go.mod +++ b/go.mod @@ -13,33 +13,26 @@ require ( github.com/vincent-petithory/dataurl v1.0.0 go.mau.fi/whatsmeow v0.0.0-20230621213630-12cd3cdb2257 google.golang.org/protobuf v1.31.0 - modernc.org/sqlite v1.22.1 + gorm.io/driver/sqlite v1.5.3 +) + +require ( + github.com/google/go-cmp v0.5.9 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect ) require ( filippo.io/edwards25519 v1.0.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rs/xid v1.5.0 // indirect go.mau.fi/libsignal v0.1.0 // indirect golang.org/x/crypto v0.10.0 // indirect - golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.9.0 // indirect - golang.org/x/tools v0.8.0 // indirect - lukechampine.com/uint128 v1.3.0 // indirect - modernc.org/cc/v3 v3.40.0 // indirect - modernc.org/ccgo/v3 v3.16.13 // indirect - modernc.org/libc v1.22.5 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/opt v0.1.3 // indirect - modernc.org/strutil v1.1.3 // indirect - modernc.org/token v1.1.0 // indirect + gorm.io/gorm v1.25.4 rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index f8b6e49e..b79666a2 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,23 @@ filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -29,17 +27,14 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ= github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -55,12 +50,9 @@ go.mau.fi/whatsmeow v0.0.0-20230621213630-12cd3cdb2257 h1:KjrcgNvNIvpKVeUwKQNn7V go.mau.fi/whatsmeow v0.0.0-20230621213630-12cd3cdb2257/go.mod h1:+ObGpFE6cbbY4hKc1FmQH9MVfqaemmlXGXSnwDvCOyE= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -73,35 +65,13 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= -golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= -lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= -modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= -modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= -modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= -modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= -modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= -modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= -modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= -modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= -modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g= +gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= +gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/main.go b/main.go index 3c24d937..eb3fb776 100755 --- a/main.go +++ b/main.go @@ -2,9 +2,9 @@ package main import ( "context" - "database/sql" "flag" "fmt" + "log" "net/http" "os" "os/signal" @@ -14,15 +14,15 @@ import ( controllerBase "wuzapi/controllers/controller_base" "wuzapi/repository" - "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/store/sqlstore" - waLog "go.mau.fi/whatsmeow/util/log" - "github.com/go-resty/resty/v2" "github.com/gorilla/mux" "github.com/patrickmn/go-cache" "github.com/rs/zerolog" - _ "modernc.org/sqlite" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/store/sqlstore" + waLog "go.mau.fi/whatsmeow/util/log" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) var ( @@ -37,7 +37,7 @@ var ( killchannel = make(map[int](chan bool)) userinfocache = cache.New(5*time.Minute, 10*time.Minute) - log zerolog.Logger + logger zerolog.Logger ) func init() { @@ -45,10 +45,10 @@ func init() { flag.Parse() if *logType == "json" { - log = zerolog.New(os.Stdout).With().Timestamp().Str("role", filepath.Base(os.Args[0])).Logger() + logger = zerolog.New(os.Stdout).With().Timestamp().Str("role", filepath.Base(os.Args[0])).Logger() } else { output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339} - log = zerolog.New(output).With().Timestamp().Str("role", filepath.Base(os.Args[0])).Logger() + logger = zerolog.New(output).With().Timestamp().Str("role", filepath.Base(os.Args[0])).Logger() } } @@ -73,20 +73,18 @@ func main() { *token = "1234ABC" } - db, err := sql.Open("sqlite", exPath+"/dbdata/users.db") + db, err := gorm.Open(sqlite.Open(exPath+"/dbdata/users.db"), &gorm.Config{}) if err != nil { - log.Fatal().Err(err).Msg("Could not open/create " + exPath + "/dbdata/users.db") - os.Exit(1) + log.Fatal(err) } - defer db.Close() - sqlStmt := `CREATE TABLE IF NOT EXISTS users (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, token TEXT NOT NULL, webhook TEXT NOT NULL default "", jid TEXT NOT NULL default "", qrcode TEXT NOT NULL default "", connected INTEGER, expiration INTEGER, events TEXT NOT NULL default "All");` - _, err = db.Exec(sqlStmt) + err = db.AutoMigrate(&repository.User{}) if err != nil { - panic(fmt.Sprintf("%q: %s\n", err, sqlStmt)) + log.Fatal(err) } - CreateAdminUser(db, token) + userRepository := repository.NewUserRepository(db) + CreateAdminUser(*token, userRepository) if *waDebug != "" { dbLog := waLog.Stdout("Database", *waDebug, true) @@ -100,7 +98,7 @@ func main() { s := &controllerBase.Controller{ Router: mux.NewRouter(), - Repository: repository.NewUserRepository(db), + Repository: userRepository, ExPath: exPath, UserInfoCache: userinfocache, KillChannel: killchannel, @@ -127,20 +125,20 @@ func main() { if *sslcert != "" { if err := srv.ListenAndServeTLS(*sslcert, *sslprivkey); err != nil && err != http.ErrServerClosed { //log.Fatalf("listen: %s\n", err) - log.Fatal().Err(err).Msg("Startup failed") + logger.Fatal().Err(err).Msg("Startup failed") } } else { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { //log.Fatalf("listen: %s\n", err) - log.Fatal().Err(err).Msg("Startup failed") + logger.Fatal().Err(err).Msg("Startup failed") } } }() //wlog.Infof("Server Started. Listening on %s:%s", *address, *port) - log.Info().Str("address", *address).Str("port", *port).Msg("Server Started") + logger.Info().Str("address", *address).Str("port", *port).Msg("Server Started") <-done - log.Info().Msg("Server Stoped") + logger.Info().Msg("Server Stoped") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer func() { @@ -149,44 +147,15 @@ func main() { }() if err := srv.Shutdown(ctx); err != nil { - log.Error().Str("error", fmt.Sprintf("%+v", err)).Msg("Server Shutdown Failed") + logger.Error().Str("error", fmt.Sprintf("%+v", err)).Msg("Server Shutdown Failed") os.Exit(1) } - log.Info().Msg("Server Exited Properly") + logger.Info().Msg("Server Exited Properly") } -func CreateAdminUser(db *sql.DB, token *string) { - tx, err := db.Begin() +func CreateAdminUser(token string, userRepository repository.UserRepository) { + err := userRepository.CreateAdminUser(token) if err != nil { - log.Fatal().Err(err).Msg("Could not begin transaction") - os.Exit(1) - } - defer tx.Rollback() - - rows, err := tx.Query("SELECT id FROM users WHERE name='admin' LIMIT 1") - if err != nil { - log.Fatal().Err(err).Msg("Could not query users table") - os.Exit(1) - } - - if !rows.Next() { - sqlStmtInsert := fmt.Sprintf("INSERT INTO users (name, token) values ('admin', '%s')", *token) - _, err = tx.Exec(sqlStmtInsert) - if err != nil { - log.Fatal().Err(err).Msg("Could not insert admin user") - os.Exit(1) - } - } else { - sqlStmtUpdate := fmt.Sprintf("UPDATE users SET token='%s' WHERE name='admin'", *token) - _, err = tx.Exec(sqlStmtUpdate) - if err != nil { - log.Fatal().Err(err).Msg("Could not update admin user") - os.Exit(1) - } - } - - if err := tx.Commit(); err != nil { - log.Fatal().Err(err).Msg("Could not commit transaction") - os.Exit(1) + log.Fatal(err) } } diff --git a/repository/user_repository.go b/repository/user_repository.go index a1fab45d..eed97ec2 100644 --- a/repository/user_repository.go +++ b/repository/user_repository.go @@ -1,11 +1,11 @@ package repository import ( - "database/sql" "errors" "fmt" - "strconv" internalTypes "wuzapi/internal/types" + + "gorm.io/gorm" ) type User struct { @@ -32,6 +32,8 @@ func (u User) ToValues() internalTypes.Values { type UserRepository interface { CreateUser(user *User) (*User, error) + CreateAdminUser(token string) (error) + GetAdminUser() (*User, error) GetUserByToken(token string) (*User, error) GetUserById(userId int) (*User, error) GetConnectedUsers() ([]*User, error) @@ -46,168 +48,103 @@ type UserRepository interface { } type userRepository struct { - Db *sql.DB + db *gorm.DB } -func NewUserRepository(db *sql.DB) UserRepository { - return &userRepository{Db: db} +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{db: db} } func (r *userRepository) CreateUser(user *User) (*User, error) { - if user, _ := r.GetUserByToken(user.Token); user != nil { + if foundUser, _ := r.GetUserByToken(user.Token); foundUser != nil { return nil, errors.New("User already exists") } - _, err := r.Db.Exec("INSERT INTO users (name, token) VALUES (?, ?)", user.Name, user.Token) + err := r.db.Create(user).Error if err != nil { return nil, err } return r.GetUserByToken(user.Token) } +func (r *userRepository) CreateAdminUser(token string) error { + if user, _ := r.GetAdminUser(); user != nil { + user.Token = token + return r.db.Updates(user).Error + } + return r.db.Create(&User{Name: "admin", Token: token}).Error +} + func (r *userRepository) ConnectUser(userId int) error { - _, err := r.Db.Exec("UPDATE users SET connected=1 WHERE id=?", userId) - return err + return r.db.Update("connected", 1).Error } func (r *userRepository) DisconnectUser(userId int) error { - _, err := r.Db.Exec("UPDATE users SET connected=0 WHERE id=?", userId) - return err + return r.db.Update("connected", 0).Error } func (r *userRepository) GetConnectedUsers() ([]*User, error) { - rows, err := r.Db.Query("SELECT id,token,jid,webhook,events FROM users WHERE connected=1") + var users []*User + + err := r.db.Find(&users, "connected = 1").Error + if err != nil { return nil, err } - defer rows.Close() - - users := []*User{} - for rows.Next() { - txtid := "" - token := "" - jid := "" - webhook := "" - events := "" - err = rows.Scan(&txtid, &token, &jid, &webhook, &events) - if err != nil { - return nil, err - } - id, err := strconv.Atoi(txtid) - if err != nil { - return nil, err - } - users = append(users, &User{Id: id, Token: token, Jid: jid, Webhook: webhook, Events: events}) - } return users, nil } func (r *userRepository) DeleteUserByToken(token string) error { - rows, err := r.Db.Query("SELECT name FROM users WHERE token=? LIMIT 1", token) + var user *User + err := r.db.Find(&user, "token = ?", token).Error if err != nil { return err } - defer rows.Close() - - for rows.Next() { - var name string - err = rows.Scan(&name) - if err != nil { - return err - } - - if name == "admin" { - return errors.New("Cannot delete admin user") - } + if user == nil { + return fmt.Errorf("user not found for token %s", token) } - - _, err = r.Db.Query(fmt.Sprintf("delete from users where token='%s'", token)) - return err + err = r.db.Delete(&user).Error + if err != nil { + return err + } + return nil } func (r *userRepository) SetJid(jid string, userId int) error { - _, err := r.Db.Exec("UPDATE users SET jid=? WHERE id=?", jid, userId) - return err + return r.db.Where(userId).Update("jid", jid).Error } func (r *userRepository) SetEvents(events string, userId int) error { - _, err := r.Db.Exec("UPDATE users SET events=? WHERE id=?", events, userId) - return err + return r.db.Where(userId).Update("events", events).Error } -func (r *userRepository)SetWebhook(webhooks string, userId int) error { - _, err := r.Db.Exec("UPDATE users SET webhook=? WHERE id=?", webhooks, userId) - return err +func (r *userRepository) SetWebhook(webhooks string, userId int) error { + return r.db.Where(userId).Update("webhooks", webhooks).Error +} + +func (r *userRepository) SetQrCode(qrCode string, userId int) error { + return r.db.Where(userId).Update("qr_code", qrCode).Error } func (r *userRepository) GetQrCode(userId int) (string, error) { - var code string - rows, err := r.Db.Query("SELECT qrcode AS code FROM users WHERE id=? LIMIT 1", userId) + user, err := r.GetUserById(userId) if err != nil { return "", err } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&code) - if err != nil { - return "", err - } - } - return code, nil + return user.QrCode, nil } -func (r *userRepository) SetQrCode(qrCode string, userId int) error { - _, err := r.Db.Exec("UPDATE users SET qrcode=? WHERE id=?", qrCode, userId) - return err +func (r *userRepository) GetUserByToken(token string) (user *User, err error) { + err = r.db.Find(&user, "token like ?", token).Error + return } -func (r *userRepository) GetUserByToken(token string) (*User, error) { - rows, err := r.Db.Query("SELECT id,webhook,jid,events,name,token FROM users WHERE token=? LIMIT 1", token) - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - txtid := "" - jid := "" - webhook := "" - events := "" - name := "" - token := "" - err = rows.Scan(&txtid, &webhook, &jid, &events, &name, &token) - if err != nil { - return nil, err - } - userid, _ := strconv.Atoi(txtid) - return &User{Id: userid, Name: name, Token: token, Webhook: webhook, Events: events}, nil - } - return nil, nil +func (r *userRepository) GetAdminUser() (user *User, err error) { + err = r.db.Find(&user, "name like 'admin'").Error + return } -func (r *userRepository) GetUserById(userId int) (*User, error) { - - name := "" - token := "" - webhook := "" - jid := "" - qrcode := "" - events := "" - - rows, err := r.Db.Query("SELECT name,token,webhook,jid,qrcode,events FROM users WHERE id=? LIMIT 1", userId) - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - err = rows.Scan(&name, &token, &webhook, &jid, &qrcode, &events) - if err != nil { - return nil, err - } - } - err = rows.Err() - if err != nil { - return nil, err - } - - return &User{Id: userId, Webhook: webhook, Events: events}, nil +func (r *userRepository) GetUserById(userId int) (user *User, err error) { + err = r.db.Find(&user, userId).Error + return } diff --git a/routes.go b/routes.go index 9bc2eeac..c4ed201a 100644 --- a/routes.go +++ b/routes.go @@ -29,15 +29,15 @@ func routes(s *controllerBase.Controller) { exPath := filepath.Dir(ex) if *logType == "json" { - log = zerolog.New(os.Stdout).With().Timestamp().Str("role", filepath.Base(os.Args[0])).Str("host", *address).Logger() + logger = zerolog.New(os.Stdout).With().Timestamp().Str("role", filepath.Base(os.Args[0])).Str("host", *address).Logger() } else { output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339} - log = zerolog.New(output).With().Timestamp().Str("role", filepath.Base(os.Args[0])).Str("host", *address).Logger() + logger = zerolog.New(output).With().Timestamp().Str("role", filepath.Base(os.Args[0])).Str("host", *address).Logger() } c := alice.New() c = c.Append(s.AuthAlice) - c = c.Append(hlog.NewHandler(log)) + c = c.Append(hlog.NewHandler(logger)) c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { hlog.FromRequest(r).Info(). From ce101a9ea86423e8cd86116f6a333a045aeef531 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sat, 16 Sep 2023 22:42:54 -0300 Subject: [PATCH 22/24] Repository refactored to use database wrapper entity --- .idea/.gitignore | 8 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 ++ .idea/wuzapi.iml | 9 ++ .../controller_base/controller_base.go | 76 ++++++++++------- controllers/session/session_controller.go | 63 ++++++++------ controllers/user/user_controller.go | 2 +- controllers/webhook/webhook_controller.go | 8 +- internal/helpers/helpers.go | 2 +- internal/helpers/wmiau.go | 58 +++++++++---- main.go | 2 +- repository/user.go | 84 +++++++++++++++++++ repository/user_repository.go | 61 ++------------ 13 files changed, 259 insertions(+), 128 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/wuzapi.iml create mode 100644 repository/user.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..5cf6887a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/wuzapi.iml b/.idea/wuzapi.iml new file mode 100644 index 00000000..5e764c4f --- /dev/null +++ b/.idea/wuzapi.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/controllers/controller_base/controller_base.go b/controllers/controller_base/controller_base.go index 1c73159b..80cee40c 100644 --- a/controllers/controller_base/controller_base.go +++ b/controllers/controller_base/controller_base.go @@ -34,7 +34,7 @@ type Controller struct { Router *mux.Router ExPath string ClientPointer map[int]*whatsmeow.Client - KillChannel map[int](chan bool) + KillChannel map[int]chan bool UserInfoCache *cache.Cache Container *sqlstore.Container WaDebug *string @@ -42,8 +42,8 @@ type Controller struct { LogType *string } -// Writes JSON response to API clients -func (s *Controller) Respond(w http.ResponseWriter, r *http.Request, status int, data interface{}) { +// Respond Writes JSON response to API clients +func (s *Controller) Respond(w http.ResponseWriter, _ *http.Request, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) @@ -67,7 +67,7 @@ func (s *Controller) Respond(w http.ResponseWriter, r *http.Request, status int, } } -// Connects to Whatsapp Websocket on server startup if last state was connected +// ConnectOnStartup Connects to Whatsapp Websocket on server startup if last state was connected func (s *Controller) ConnectOnStartup() { users, err := s.Repository.GetConnectedUsers() if err != nil { @@ -98,11 +98,16 @@ func (s *Controller) ConnectOnStartup() { eventstring := strings.Join(subscribedEvents, ",") log.Info().Str("events", eventstring).Str("jid", user.Jid).Msg("Attempt to connect") s.KillChannel[user.Id] = make(chan bool) - go s.StartClient(user.Id, user.Jid, user.Token, subscribedEvents) + go func(user *repository.User) { + err := s.StartClient(user.Id, user.Jid, user.Token, subscribedEvents) + if err != nil { + log.Fatal().Err(err).Msg("Could not start client") + } + }(user) } } -func (s *Controller) StartClient(userID int, textjid string, token string, subscriptions []string) { +func (s *Controller) StartClient(userID int, textjid string, token string, subscriptions []string) error { log.Info().Str("userid", strconv.Itoa(userID)).Str("jid", textjid).Msg("Starting websocket connection to Whatsapp") @@ -112,10 +117,15 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc if s.ClientPointer[userID] != nil { isConnected := s.ClientPointer[userID].IsConnected() if isConnected == true { - return + return nil } } + user, err := repository.NewUser(s.Repository, userID) + if err != nil { + return err + } + /* container is initialized on main to have just one connection and avoid sqlite locks dbDirectory := "dbdata" @@ -180,15 +190,19 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc s.ClientPointer[userID] = client - mycli := helpers.MyClient{ - WAClient: client, - EventHandlerID: 1, - UserID: userID, - Token: token, - Subscriptions: subscriptions, - UserInfoCache: s.UserInfoCache, - KillChannel: s.KillChannel, - Repository: s.Repository, + mycli, err := helpers.NewClient( + client, + 1, + userID, + token, + subscriptions, + s.Repository, + s.UserInfoCache, + s.KillChannel, + s.ClientHttp, + ) + if err != nil { + return err } mycli.EventHandlerID = mycli.WAClient.AddEventHandler(mycli.MyEventHandler) s.ClientHttp[userID].SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) @@ -220,15 +234,15 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc // Store encoded/embeded base64 QR on database for retrieval with the /qr endpoint image, _ := qrcode.Encode(evt.Code, qrcode.Medium, 256) base64qrcode := "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) - err := s.Repository.SetQrCode(base64qrcode, userID) + err := user.SetQrCode(base64qrcode) if err != nil { - log.Error().Err(err).Msg(err.Error()) + return err } } else if evt.Event == "timeout" { // Clear QR code from DB on timeout - err := s.Repository.SetQrCode("", userID) + err := user.SetQrCode("") if err != nil { - log.Error().Err(err).Msg(err.Error()) + return err } log.Warn().Msg("QR timeout killing channel") delete(s.ClientPointer, userID) @@ -236,9 +250,9 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc } else if evt.Event == "success" { log.Info().Msg("QR pairing ok!") // Clear QR code after pairing - err := s.Repository.SetQrCode("", userID) + err := user.SetQrCode("") if err != nil { - log.Error().Err(err).Msg(err.Error()) + return err } } else { log.Info().Str("event", evt.Event).Msg("Login event") @@ -251,7 +265,7 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc log.Info().Msg("Already logged in, just connect") err = client.Connect() if err != nil { - panic(err) + return err } } @@ -262,11 +276,15 @@ func (s *Controller) StartClient(userID int, textjid string, token string, subsc log.Info().Str("userid", strconv.Itoa(userID)).Msg("Received kill signal") client.Disconnect() delete(s.ClientPointer, userID) - err := s.Repository.DisconnectUser(userID) + user, err := repository.NewUser(s.Repository, userID) if err != nil { - log.Error().Err(err).Msg(err.Error()) + return err } - return + err = user.Disconnect() + if err != nil { + return err + } + return nil default: time.Sleep(1000 * time.Millisecond) //log.Info().Str("jid",textjid).Msg("Loop the loop") @@ -304,14 +322,14 @@ func (s *Controller) AuthAlice(next http.Handler) http.Handler { } if userid == 0 { - s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) + s.Respond(w, r, http.StatusUnauthorized, errors.New("unauthorized")) return } next.ServeHTTP(w, r.WithContext(ctx)) }) } -// Middleware: Authenticate connections based on Token header/uri parameter +// Auth Middleware: Authenticate connections based on Token header/uri parameter func (s *Controller) Auth(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -342,7 +360,7 @@ func (s *Controller) Auth(handler http.HandlerFunc) http.HandlerFunc { } if userid == 0 { - s.Respond(w, r, http.StatusUnauthorized, errors.New("Unauthorized")) + s.Respond(w, r, http.StatusUnauthorized, errors.New("unauthorized")) return } handler(w, r.WithContext(ctx)) diff --git a/controllers/session/session_controller.go b/controllers/session/session_controller.go index 51dc54e8..a8dbfc15 100644 --- a/controllers/session/session_controller.go +++ b/controllers/session/session_controller.go @@ -11,6 +11,7 @@ import ( controllerBase "wuzapi/controllers/controller_base" "wuzapi/internal/helpers" internalTypes "wuzapi/internal/types" + "wuzapi/repository" "github.com/justinas/alice" "github.com/patrickmn/go-cache" @@ -29,7 +30,7 @@ func (s *SessionController) SignRoutes(c alice.Chain) { s.Router.Handle("/session/qr", c.Then(s.GetQR())).Methods("GET") } -// Connects to Whatsapp Servers +// Connect Connects to Whatsapp Servers func (s *SessionController) Connect() http.HandlerFunc { type connectStruct struct { @@ -51,12 +52,12 @@ func (s *SessionController) Connect() http.HandlerFunc { var t connectStruct err := decoder.Decode(&t) if err != nil { - s.Respond(w, r, http.StatusBadRequest, errors.New("Could not decode Payload")) + s.Respond(w, r, http.StatusBadRequest, errors.New("could not decode Payload")) return } if s.ClientPointer[userid] != nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Already Connected")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("already Connected")) return } else { @@ -77,7 +78,11 @@ func (s *SessionController) Connect() http.HandlerFunc { } } eventstring = strings.Join(subscribedEvents, ",") - err = s.Repository.SetEvents(eventstring, userid) + user, err := repository.NewUser(s.Repository, userid) + if err != nil { + log.Warn().Str("userid", txtid).Msg("Could not find user with the given ID") + } + err = user.SetEvents("") if err != nil { log.Warn().Msg("Could not set events in users table") } @@ -87,7 +92,12 @@ func (s *SessionController) Connect() http.HandlerFunc { log.Info().Str("jid", jid).Msg("Attempt to connect") s.KillChannel[userid] = make(chan bool) - go s.StartClient(userid, jid, token, subscribedEvents) + go func() { + err := s.StartClient(userid, jid, token, subscribedEvents) + if err != nil { + log.Warn().Msg("Could not start client...") + } + }() if t.Immediate == false { log.Warn().Msg("Waiting 10 seconds") @@ -95,11 +105,11 @@ func (s *SessionController) Connect() http.HandlerFunc { if s.ClientPointer[userid] != nil { if !s.ClientPointer[userid].IsConnected() { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to Connect")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("failed to Connect")) return } } else { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Failed to Connect")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("failed to Connect")) return } } @@ -117,7 +127,7 @@ func (s *SessionController) Connect() http.HandlerFunc { } } -// Disconnects from Whatsapp websocket, does not log out device +// Disconnect Disconnects from Whatsapp websocket, does not log out device func (s *SessionController) Disconnect() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -127,14 +137,18 @@ func (s *SessionController) Disconnect() http.HandlerFunc { userid, _ := strconv.Atoi(txtid) if s.ClientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("no session")) return } if s.ClientPointer[userid].IsConnected() == true { if s.ClientPointer[userid].IsLoggedIn() == true { log.Info().Str("jid", jid).Msg("Disconnection successfull") s.KillChannel[userid] <- true - err := s.Repository.SetEvents("", userid) + user, err := repository.NewUser(s.Repository, userid) + if err != nil { + log.Warn().Str("userid", txtid).Msg("Could not find user with the given ID") + } + err = user.SetEvents("") if err != nil { log.Warn().Str("userid", txtid).Msg("Could not set events in users table") } @@ -151,18 +165,18 @@ func (s *SessionController) Disconnect() http.HandlerFunc { return } else { log.Warn().Str("jid", jid).Msg("Ignoring disconnect as it was not connected") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Cannot disconnect because it is not logged in")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("cannot disconnect because it is not logged in")) return } } else { log.Warn().Str("jid", jid).Msg("Ignoring disconnect as it was not connected") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Cannot disconnect because it is not logged in")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("cannot disconnect because it is not logged in")) return } } } -// Logs out device from Whatsapp (requires to scan QR next time) +// Logout Logs out device from Whatsapp (requires to scan QR next time) func (s *SessionController) Logout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -171,14 +185,14 @@ func (s *SessionController) Logout() http.HandlerFunc { userid, _ := strconv.Atoi(txtid) if s.ClientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("no session")) return } else { if s.ClientPointer[userid].IsLoggedIn() == true && s.ClientPointer[userid].IsConnected() == true { err := s.ClientPointer[userid].Logout() if err != nil { log.Error().Str("jid", jid).Msg("Could not perform logout") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not perform logout")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("could not perform logout")) return } else { log.Info().Str("jid", jid).Msg("Logged out") @@ -187,11 +201,11 @@ func (s *SessionController) Logout() http.HandlerFunc { } else { if s.ClientPointer[userid].IsConnected() == true { log.Warn().Str("jid", jid).Msg("Ignoring logout as it was not logged in") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not disconnect as it was not logged in")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("could not disconnect as it was not logged in")) return } else { log.Warn().Str("jid", jid).Msg("Ignoring logout as it was not connected") - s.Respond(w, r, http.StatusInternalServerError, errors.New("Could not disconnect as it was not connected")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("could not disconnect as it was not connected")) return } } @@ -208,7 +222,7 @@ func (s *SessionController) Logout() http.HandlerFunc { } } -// Gets Connected and LoggedIn Status +// GetStatus Gets Connected and LoggedIn Status func (s *SessionController) GetStatus() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -217,7 +231,7 @@ func (s *SessionController) GetStatus() http.HandlerFunc { userid, _ := strconv.Atoi(txtid) if s.ClientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("no session")) return } @@ -235,7 +249,7 @@ func (s *SessionController) GetStatus() http.HandlerFunc { } } -// Gets QR code encoded in Base64 +// GetQR Gets QR code encoded in Base64 func (s *SessionController) GetQR() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -244,26 +258,25 @@ func (s *SessionController) GetQR() http.HandlerFunc { code := "" if s.ClientPointer[userid] == nil { - s.Respond(w, r, http.StatusInternalServerError, errors.New("No session")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("no session")) return } if s.ClientPointer[userid].IsConnected() == false { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Not connected")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("not connected")) return } if s.ClientPointer[userid].IsLoggedIn() == true { - s.Respond(w, r, http.StatusInternalServerError, errors.New("Already Loggedin")) + s.Respond(w, r, http.StatusInternalServerError, errors.New("already Loggedin")) return } - code, err := s.Repository.GetQrCode(userid) if err != nil { s.Respond(w, r, http.StatusInternalServerError, err) return } - + log.Info().Str("userid", txtid).Str("qrcode", code).Msg("Get QR successful") response := map[string]interface{}{"QRCode": fmt.Sprintf("%s", code)} responseJson, err := json.Marshal(response) diff --git a/controllers/user/user_controller.go b/controllers/user/user_controller.go index 3b08b496..ee7b382e 100644 --- a/controllers/user/user_controller.go +++ b/controllers/user/user_controller.go @@ -99,7 +99,7 @@ func (s *UserController) DeleteUser() http.HandlerFunc { return } - s.Respond(w, r, http.StatusOK, "User deleted") + s.Respond(w, r, http.StatusOK, "UserDb deleted") return } } diff --git a/controllers/webhook/webhook_controller.go b/controllers/webhook/webhook_controller.go index bd0bc072..fb56aadd 100644 --- a/controllers/webhook/webhook_controller.go +++ b/controllers/webhook/webhook_controller.go @@ -9,6 +9,7 @@ import ( controllerBase "wuzapi/controllers/controller_base" "wuzapi/internal/helpers" internalTypes "wuzapi/internal/types" + "wuzapi/repository" "github.com/justinas/alice" "github.com/patrickmn/go-cache" @@ -67,7 +68,12 @@ func (s *WebhookController) SetWebhook() http.HandlerFunc { } var webhook = t.WebhookURL - err = s.Repository.SetWebhook(webhook, userid) + user, err := repository.NewUser(s.Repository, userid) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("%s", err)) + return + } + err = user.SetWebhook(webhook) if err != nil { s.Respond(w, r, http.StatusInternalServerError, fmt.Errorf("%s", err)) return diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index ae92f32d..5c48409d 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -15,7 +15,7 @@ func Find(slice []string, val string) bool { return false } -// Update entry in User map +// Updates entry in User map func UpdateUserInfo(values interface{}, field string, value string) interface{} { log.Debug().Str("field", field).Str("value", value).Msg("User info updated") values.(internalTypes.Values).M[field] = value diff --git a/internal/helpers/wmiau.go b/internal/helpers/wmiau.go index 4038cbc2..4432051f 100644 --- a/internal/helpers/wmiau.go +++ b/internal/helpers/wmiau.go @@ -69,17 +69,44 @@ var historySyncID int32 type MyClient struct { WAClient *whatsmeow.Client EventHandlerID uint32 - UserID int + user *repository.UserDb Token string Subscriptions []string - Repository repository.UserRepository UserInfoCache *cache.Cache KillChannel map[int](chan bool) ClientHttp map[int]*resty.Client } +func NewClient( + WAClient *whatsmeow.Client, + EventHandlerID uint32, + UserID int, + Token string, + Subscriptions []string, + Repository repository.UserRepository, + UserInfoCache *cache.Cache, + KillChannel map[int](chan bool), + ClientHttp map[int]*resty.Client, +) (*MyClient, error) { + + user, err := repository.NewUser(Repository, UserID) + if err != nil { + return nil, err + } + return &MyClient{ + WAClient: WAClient, + EventHandlerID: EventHandlerID, + user: user, + Token: Token, + Subscriptions: Subscriptions, + UserInfoCache: UserInfoCache, + KillChannel: KillChannel, + ClientHttp: ClientHttp, + }, nil +} + func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { - txtid := strconv.Itoa(mycli.UserID) + txtid := strconv.Itoa(mycli.user.Id) postmap := make(map[string]interface{}) postmap["event"] = rawEvt dowebhook := 0 @@ -117,7 +144,7 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { } else { log.Info().Msg("Marked self as available") } - err = mycli.Repository.ConnectUser(mycli.UserID) + err = mycli.user.Connect() if err != nil { log.Error().Err(err).Msg(err.Error()) return @@ -125,10 +152,11 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { case *events.PairSuccess: postmap["type"] = "PairSuccess" dowebhook = 1 - log.Info().Str("userid", strconv.Itoa(mycli.UserID)).Str("token", mycli.Token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success") + log.Info().Str("userid", strconv.Itoa(mycli.user.Id)).Str("token", mycli.Token).Str("ID", evt.ID.String()).Str("BusinessName", evt.BusinessName).Str("Platform", evt.Platform).Msg("QR Pair Success") jid := evt.ID - err := mycli.Repository.SetJid(jid.String(), mycli.UserID) + mycli.user.Jid = jid.String() + err := mycli.user.SetJid(jid.String()) if err != nil { log.Error().Err(err).Msg(err.Error()) return @@ -142,7 +170,7 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { token := myuserinfo.(internalTypes.Values).Get("Token") v := UpdateUserInfo(myuserinfo, "Jid", fmt.Sprintf("%s", jid)) mycli.UserInfoCache.Set(token, v, cache.NoExpiration) - log.Info().Str("jid", jid.String()).Str("userid", txtid).Str("token", token).Msg("User information set") + log.Info().Str("jid", jid.String()).Str("userid", txtid).Str("token", token).Msg("UserDb information set") } case *events.StreamReplaced: log.Info().Msg("Received StreamReplaced event") @@ -285,13 +313,13 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { if evt.Unavailable { postmap["state"] = "offline" if evt.LastSeen.IsZero() { - log.Info().Str("from", evt.From.String()).Msg("User is now offline") + log.Info().Str("from", evt.From.String()).Msg("UserDb is now offline") } else { - log.Info().Str("from", evt.From.String()).Str("lastSeen", fmt.Sprintf("%d", evt.LastSeen)).Msg("User is now offline") + log.Info().Str("from", evt.From.String()).Str("lastSeen", fmt.Sprintf("%d", evt.LastSeen)).Msg("UserDb is now offline") } } else { postmap["state"] = "online" - log.Info().Str("from", evt.From.String()).Msg("User is now online") + log.Info().Str("from", evt.From.String()).Msg("UserDb is now online") } case *events.HistorySync: postmap["type"] = "HistorySync" @@ -332,8 +360,8 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { dowebhook = 1 log.Info().Str("reason", evt.Reason.String()).Msg("Logged out") - mycli.KillChannel[mycli.UserID] <- true - err := mycli.Repository.DisconnectUser(mycli.UserID) + mycli.KillChannel[mycli.user.Id] <- true + err := mycli.user.Disconnect() if err != nil { log.Error().Err(err).Msg(err.Error()) return @@ -390,15 +418,15 @@ func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { data := make(map[string]string) data["data"] = string(values) data["token"] = mycli.Token - go webhook.CallHook(webhookurl, data, mycli.UserID) + go webhook.CallHook(webhookurl, data, mycli.user.Id) } else { data := make(map[string]string) data["data"] = string(values) data["token"] = mycli.Token - go webhook.CallHookFile(webhookurl, data, mycli.UserID, path) + go webhook.CallHookFile(webhookurl, data, mycli.user.Id, path) } } else { - log.Warn().Str("userid", strconv.Itoa(mycli.UserID)).Msg("No webhook set for user") + log.Warn().Str("userid", strconv.Itoa(mycli.user.Id)).Msg("No webhook set for user") } } } diff --git a/main.go b/main.go index eb3fb776..4e6fa2fa 100755 --- a/main.go +++ b/main.go @@ -78,7 +78,7 @@ func main() { log.Fatal(err) } - err = db.AutoMigrate(&repository.User{}) + err = db.AutoMigrate(&repository.UserDb{}) if err != nil { log.Fatal(err) } diff --git a/repository/user.go b/repository/user.go new file mode 100644 index 00000000..a7c41c6c --- /dev/null +++ b/repository/user.go @@ -0,0 +1,84 @@ +package repository + +import ( + "fmt" + internalTypes "wuzapi/internal/types" +) + +type User struct { + Id int + Name string + Token string + Webhook string + Jid string + QrCode string + Connected int + Expiration int + Events string +} + +type UserDb struct { + *User + repository UserRepository +} + +func (u *User) ToValues() internalTypes.Values { + return internalTypes.Values{M: map[string]string{ + "Id": fmt.Sprint(u.Id), + "Jid": u.Jid, + "Webhook": u.Webhook, + "Token": u.Token, + "Events": u.Events, + }} +} + +func (u *UserDb) loadUser() (err error) { + u.User, err = u.repository.GetUserById(u.Id) + return +} + +func (u *UserDb) SaveChanges() error { + return u.repository.Updates(u.User) +} + +func (u *UserDb) Connect() error { + u.Connected = 1 + return u.SaveChanges() +} + +func (u *UserDb) SetJid(jid string) error { + u.Jid = jid + return u.SaveChanges() +} + +func (u *UserDb) Disconnect() error { + u.Connected = 0 + return u.SaveChanges() +} + +func (u *UserDb) SetQrCode(qrcode string) error { + u.QrCode = qrcode + return u.SaveChanges() +} + +func (u *UserDb) SetEvents(events string) error { + u.Events = events + return u.SaveChanges() +} + +func (u *UserDb) SetWebhook(webhook string) error { + u.Webhook = webhook + return u.SaveChanges() +} + +func NewUser(repository UserRepository, userId int) (*UserDb, error) { + user := &UserDb{ + User: &User{Id: userId}, + repository: repository, + } + err := user.loadUser() + if err != nil { + return nil, err + } + return user, nil +} diff --git a/repository/user_repository.go b/repository/user_repository.go index eed97ec2..c3e5ff68 100644 --- a/repository/user_repository.go +++ b/repository/user_repository.go @@ -3,48 +3,19 @@ package repository import ( "errors" "fmt" - internalTypes "wuzapi/internal/types" - "gorm.io/gorm" ) -type User struct { - Id int - Name string - Token string - Webhook string - Jid string - QrCode string - Connected *int - Expiration *int - Events string -} - -func (u User) ToValues() internalTypes.Values { - return internalTypes.Values{M: map[string]string{ - "Id": fmt.Sprint(u.Id), - "Jid": u.Jid, - "Webhook": u.Webhook, - "Token": u.Token, - "Events": u.Events, - }} -} - type UserRepository interface { CreateUser(user *User) (*User, error) - CreateAdminUser(token string) (error) + CreateAdminUser(token string) error GetAdminUser() (*User, error) GetUserByToken(token string) (*User, error) GetUserById(userId int) (*User, error) GetConnectedUsers() ([]*User, error) DeleteUserByToken(token string) error GetQrCode(userId int) (string, error) - SetQrCode(qrCode string, userId int) error - SetJid(jid string, userId int) error - SetEvents(events string, userId int) error - SetWebhook(webhooks string, userId int) error - ConnectUser(userId int) error - DisconnectUser(userId int) error + Updates(user *User) error } type userRepository struct { @@ -57,7 +28,7 @@ func NewUserRepository(db *gorm.DB) UserRepository { func (r *userRepository) CreateUser(user *User) (*User, error) { if foundUser, _ := r.GetUserByToken(user.Token); foundUser != nil { - return nil, errors.New("User already exists") + return nil, errors.New("UserDb already exists") } err := r.db.Create(user).Error @@ -75,12 +46,8 @@ func (r *userRepository) CreateAdminUser(token string) error { return r.db.Create(&User{Name: "admin", Token: token}).Error } -func (r *userRepository) ConnectUser(userId int) error { - return r.db.Update("connected", 1).Error -} - -func (r *userRepository) DisconnectUser(userId int) error { - return r.db.Update("connected", 0).Error +func (r *userRepository) Updates(user *User) error { + return r.db.Updates(user).Error } func (r *userRepository) GetConnectedUsers() ([]*User, error) { @@ -95,7 +62,7 @@ func (r *userRepository) GetConnectedUsers() ([]*User, error) { } func (r *userRepository) DeleteUserByToken(token string) error { - var user *User + var user *UserDb err := r.db.Find(&user, "token = ?", token).Error if err != nil { return err @@ -110,22 +77,6 @@ func (r *userRepository) DeleteUserByToken(token string) error { return nil } -func (r *userRepository) SetJid(jid string, userId int) error { - return r.db.Where(userId).Update("jid", jid).Error -} - -func (r *userRepository) SetEvents(events string, userId int) error { - return r.db.Where(userId).Update("events", events).Error -} - -func (r *userRepository) SetWebhook(webhooks string, userId int) error { - return r.db.Where(userId).Update("webhooks", webhooks).Error -} - -func (r *userRepository) SetQrCode(qrCode string, userId int) error { - return r.db.Where(userId).Update("qr_code", qrCode).Error -} - func (r *userRepository) GetQrCode(userId int) (string, error) { user, err := r.GetUserById(userId) if err != nil { From 4ee16a4fcad6a132275471fd5347d049235db503 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Sat, 16 Sep 2023 22:43:54 -0300 Subject: [PATCH 23/24] Added .idea directory to .gitignore --- .gitignore | 1 + .idea/.gitignore | 8 -------- .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ .idea/wuzapi.iml | 9 --------- 5 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/wuzapi.iml diff --git a/.gitignore b/.gitignore index ab7448d3..77a5f1c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dbdata/ files/ wuzapi +.idea/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b81..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 5cf6887a..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/wuzapi.iml b/.idea/wuzapi.iml deleted file mode 100644 index 5e764c4f..00000000 --- a/.idea/wuzapi.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file From a6b5014715caf1a999234c71d6e64c92175901c5 Mon Sep 17 00:00:00 2001 From: Guilherme Gomes Date: Mon, 18 Sep 2023 00:47:53 -0300 Subject: [PATCH 24/24] Admin user creation fix --- go.mod | 9 +++++++++ go.sum | 19 +++++++++++++++++++ main.go | 4 ++-- repository/user_repository.go | 4 ++-- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 4a9c3e6c..9600c7f9 100644 --- a/go.mod +++ b/go.mod @@ -17,10 +17,19 @@ require ( ) require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/sqlite v1.9.0 // indirect github.com/google/go-cmp v0.5.9 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) require ( diff --git a/go.sum b/go.sum index b79666a2..1b081a73 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs= +github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -8,6 +14,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -35,6 +43,9 @@ github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXl github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -73,5 +84,13 @@ gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g= gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/main.go b/main.go index 4e6fa2fa..af22de4c 100755 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( controllerBase "wuzapi/controllers/controller_base" "wuzapi/repository" + "github.com/glebarez/sqlite" "github.com/go-resty/resty/v2" "github.com/gorilla/mux" "github.com/patrickmn/go-cache" @@ -21,7 +22,6 @@ import ( "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/store/sqlstore" waLog "go.mau.fi/whatsmeow/util/log" - "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -78,7 +78,7 @@ func main() { log.Fatal(err) } - err = db.AutoMigrate(&repository.UserDb{}) + err = db.AutoMigrate(&repository.User{}) if err != nil { log.Fatal(err) } diff --git a/repository/user_repository.go b/repository/user_repository.go index c3e5ff68..9af27fc9 100644 --- a/repository/user_repository.go +++ b/repository/user_repository.go @@ -39,9 +39,9 @@ func (r *userRepository) CreateUser(user *User) (*User, error) { } func (r *userRepository) CreateAdminUser(token string) error { - if user, _ := r.GetAdminUser(); user != nil { + if user, _ := r.GetAdminUser(); user.Id != 0 { user.Token = token - return r.db.Updates(user).Error + return r.db.Model(&User{}).Where("name like 'admin'").Updates(user).Error } return r.db.Create(&User{Name: "admin", Token: token}).Error }