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/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/controllers/controller_base/controller_base.go b/controllers/controller_base/controller_base.go new file mode 100644 index 00000000..80cee40c --- /dev/null +++ b/controllers/controller_base/controller_base.go @@ -0,0 +1,368 @@ +package controllerBase + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "strconv" + "strings" + "time" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + "wuzapi/repository" + + "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 { + Repository repository.UserRepository + 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 +} + +// 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) + + 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()) + } +} + +// 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 { + 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 { + 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", user.Jid).Msg("Attempt to connect") + s.KillChannel[user.Id] = make(chan bool) + 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) error { + + 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 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" + _, 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 + + s.ClientHttp[userID] = resty.New() + 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, 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)) + + 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) + err := user.SetQrCode(base64qrcode) + if err != nil { + return err + } + } else if evt.Event == "timeout" { + // Clear QR code from DB on timeout + err := user.SetQrCode("") + if err != nil { + return err + } + 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 + err := user.SetQrCode("") + if err != nil { + return err + } + } 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 { + return 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) + user, err := repository.NewUser(s.Repository, userID) + if err != nil { + return err + } + 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") + } + } +} + +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 + + // 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 + user, err := s.Repository.GetUserByToken(token) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + 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")) + } + + if userid == 0 { + s.Respond(w, r, http.StatusUnauthorized, errors.New("unauthorized")) + return + } + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// 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) { + + var ctx context.Context + userid := 0 + + // 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 + user, err := s.Repository.GetUserByToken(token) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + 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")) + } + + if userid == 0 { + s.Respond(w, r, http.StatusUnauthorized, errors.New("unauthorized")) + return + } + handler(w, r.WithContext(ctx)) + } +} 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/controllers/session/session_controller.go b/controllers/session/session_controller.go new file mode 100644 index 00000000..a8dbfc15 --- /dev/null +++ b/controllers/session/session_controller.go @@ -0,0 +1,290 @@ +package session + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + controllerBase "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + "wuzapi/repository" + + "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") +} + +// Connect 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, ",") + 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") + } + 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 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") + 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 + } + } +} + +// Disconnect 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 + 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") + } + 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 + } + } +} + +// 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) { + + 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 + } +} + +// GetStatus 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 + } +} + +// GetQR 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 + } + + 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) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + } else { + s.Respond(w, r, http.StatusOK, string(responseJson)) + } + return + } +} diff --git a/controllers/user/user_controller.go b/controllers/user/user_controller.go new file mode 100644 index 00000000..ee7b382e --- /dev/null +++ b/controllers/user/user_controller.go @@ -0,0 +1,379 @@ +package user + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + controllerBase "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + "wuzapi/repository" + + "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/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("POST") + 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 + } + + user, err := s.Repository.CreateUser(&repository.User{Name: t.Name, Token: t.Token}) + if err == nil { + s.Respond(w, r, http.StatusBadRequest, err.Error()) + return + } else if err != sql.ErrNoRows { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + userResponse := struct { + ID int `json:"id"` + Name string `json:"name"` + Token string `json:"token"` + }{ + ID: user.Id, + Name: user.Name, + Token: user.Token, + } + + responseJSON, err := json.Marshal(userResponse) + 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 + } + + err = s.Repository.DeleteUserByToken(t.Token) + if err != nil { + s.Respond(w, r, http.StatusInternalServerError, err) + return + } + + s.Respond(w, r, http.StatusOK, "UserDb 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 + } + + 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(userResponse) + 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 { + + 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/controllers/webhook/webhook_controller.go b/controllers/webhook/webhook_controller.go new file mode 100644 index 00000000..fb56aadd --- /dev/null +++ b/controllers/webhook/webhook_controller.go @@ -0,0 +1,94 @@ +package webhook + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + controllerBase "wuzapi/controllers/controller_base" + "wuzapi/internal/helpers" + internalTypes "wuzapi/internal/types" + "wuzapi/repository" + + "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) { + + txtid := r.Context().Value("userinfo").(internalTypes.Values).Get("Id") + id, err := strconv.Atoi(txtid) + if err != nil { + s.Respond(w, r, http.StatusBadRequest, fmt.Errorf("Invalid Id")) + } + user, err := s.Repository.GetUserById(id) + + eventarray := strings.Split(user.Events, ",") + + response := map[string]interface{}{"webhook": user.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 + + 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 + } + + 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/go.mod b/go.mod index 704c4aa8..9600c7f9 100644 --- a/go.mod +++ b/go.mod @@ -13,33 +13,35 @@ 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 ( - filippo.io/edwards25519 v1.0.0 // indirect 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 ( + filippo.io/edwards25519 v1.0.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..1b081a73 100644 --- a/go.sum +++ b/go.sum @@ -3,23 +3,29 @@ filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5E 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= 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/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= 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,14 +35,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= @@ -55,12 +61,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 +76,21 @@ 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= +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/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= +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/handlers.go b/handlers.go deleted file mode 100644 index 988206c1..00000000 --- a/handlers.go +++ /dev/null @@ -1,3010 +0,0 @@ -package main - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fmt" - "net/http" - "os" - "strconv" - "strings" - "time" - - "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" -) - -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 { - 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 := Values{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.(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 := Values{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.(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").(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") - 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 !Find(subscribedEvents, "All") { - subscribedEvents = append(subscribedEvents, "All") - } - } else { - for _, arg := range t.Subscribe { - if !Find(messageTypes, arg) { - log.Warn().Str("Type", arg).Msg("Message type discarded") - continue - } - if !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 := 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").(Values).Get("Id") - jid := r.Context().Value("userinfo").(Values).Get("Jid") - token := r.Context().Value("userinfo").(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 := 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").(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").(Values).Get("Id") - token := r.Context().Value("userinfo").(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 := 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").(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").(Values).Get("Id") - jid := r.Context().Value("userinfo").(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").(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 { - Phone string - Document string - FileName string - Id string - ContextInfo waProto.ContextInfo - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(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 := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) - 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 { - Phone string - Audio string - Caption string - Id string - ContextInfo waProto.ContextInfo - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(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 := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) - 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 { - Phone string - Image string - Caption string - Id string - ContextInfo waProto.ContextInfo - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(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 := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) - 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 { - Phone string - Sticker string - Id string - PngThumbnail []byte - ContextInfo waProto.ContextInfo - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(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 := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) - 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 { - 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").(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 := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) - 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 { - Phone string - Id string - Name string - Vcard string - ContextInfo waProto.ContextInfo - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(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 := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) - 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 { - 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").(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 := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) - 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").(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 := 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").(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 := 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 { - Phone string - Body string - Id string - ContextInfo waProto.ContextInfo - } - - return func(w http.ResponseWriter, r *http.Request) { - - txtid := r.Context().Value("userinfo").(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 := validateMessageFields(t.Phone, t.ContextInfo.StanzaId, t.ContextInfo.Participant) - 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").(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 := 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) - } -} - -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 { - - 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").(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").(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 := 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").(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 := 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").(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").(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 := 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").(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").(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").(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").(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").(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 := 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").(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").(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").(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 := 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").(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 := 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").(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 := 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").(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 := 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()) - } -} - -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/helpers.go b/helpers.go deleted file mode 100644 index d21a5012..00000000 --- a/helpers.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -func Find(slice []string, val string) bool { - for _, item := range slice { - if item == val { - return true - } - } - return false -} - -// 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 - 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/internal/helpers/helpers.go b/internal/helpers/helpers.go new file mode 100644 index 00000000..5c48409d --- /dev/null +++ b/internal/helpers/helpers.go @@ -0,0 +1,23 @@ +package helpers + +import ( + internalTypes "wuzapi/internal/types" + + "github.com/rs/zerolog/log" +) + +func Find(slice []string, val string) bool { + for _, item := range slice { + if item == val { + return true + } + } + return false +} + +// 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 + return values +} diff --git a/wmiau.go b/internal/helpers/wmiau.go similarity index 54% rename from wmiau.go rename to internal/helpers/wmiau.go index 157f2437..4432051f 100644 --- a/wmiau.go +++ b/internal/helpers/wmiau.go @@ -1,12 +1,8 @@ -package main +package helpers import ( - "context" - "crypto/tls" - "database/sql" "encoding/base64" "encoding/json" - "errors" "fmt" "mime" "os" @@ -14,100 +10,21 @@ import ( "strconv" "strings" "sync/atomic" - "time" + internalTypes "wuzapi/internal/types" + "wuzapi/repository" + "wuzapi/webhook" "github.com/go-resty/resty/v2" - "github.com/mdp/qrterminal/v3" "github.com/patrickmn/go-cache" + "github.com/rs/zerolog/log" "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" ) -// var wlog waLog.Logger -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") - 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 := Values{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 !Find(subscribedEvents, "All") { - subscribedEvents = append(subscribedEvents, "All") - } - } else { - for _, arg := range eventarray { - if !Find(messageTypes, arg) { - log.Warn().Str("Type", arg).Msg("Message type discarded") - continue - } - if !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 parseJID(arg string) (types.JID, bool) { +func ParseJID(arg string) (types.JID, bool) { if arg == "" { return types.NewJID("", types.DefaultUserServer), false } @@ -147,171 +64,49 @@ func parseJID(arg string) (types.JID, bool) { } } -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, _ := 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 := MyClient{client, 1, userID, token, subscriptions, s.db} - 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") - } - } - } +var historySyncID int32 - } else { - // Already logged in, just connect - log.Info().Msg("Already logged in, just connect") - err = client.Connect() - if err != nil { - panic(err) - } - } +type MyClient struct { + WAClient *whatsmeow.Client + EventHandlerID uint32 + user *repository.UserDb + Token string + Subscriptions []string + UserInfoCache *cache.Cache + KillChannel map[int](chan bool) + ClientHttp map[int]*resty.Client +} - // 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") - } +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) +func (mycli *MyClient) MyEventHandler(rawEvt interface{}) { + txtid := strconv.Itoa(mycli.user.Id) postmap := make(map[string]interface{}) postmap["event"] = rawEvt dowebhook := 0 @@ -349,33 +144,33 @@ 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.user.Connect() if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) return } 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 - sqlStmt := `UPDATE users SET jid=? WHERE id=?` - _, err := mycli.db.Exec(sqlStmt, jid, mycli.userID) + mycli.user.Jid = jid.String() + err := mycli.user.SetJid(jid.String()) if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) return } - myuserinfo, found := userinfocache.Get(mycli.token) + myuserinfo, found := mycli.UserInfoCache.Get(mycli.Token) if !found { log.Warn().Msg("No user info cached on pairing?") } else { - txtid := myuserinfo.(Values).Get("Id") - token := myuserinfo.(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") + 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("UserDb information set") } case *events.StreamReplaced: log.Info().Msg("Received StreamReplaced event") @@ -518,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" @@ -565,11 +360,10 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { dowebhook = 1 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) + mycli.KillChannel[mycli.user.Id] <- true + err := mycli.user.Disconnect() if err != nil { - log.Error().Err(err).Msg(sqlStmt) + log.Error().Err(err).Msg(err.Error()) return } case *events.ChatPresence: @@ -604,14 +398,14 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { if dowebhook == 1 { // call webhook webhookurl := "" - myuserinfo, found := userinfocache.Get(mycli.token) + 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") + 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") { + 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 } @@ -619,19 +413,20 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { 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["data"] = string(values) - data["token"] = mycli.token - go callHook(webhookurl, data, mycli.userID) + data["token"] = mycli.Token + go webhook.CallHook(webhookurl, data, mycli.user.Id) } else { data := make(map[string]string) data["data"] = string(values) - data["token"] = mycli.token - go callHookFile(webhookurl, data, mycli.userID, path) + data["token"] = mycli.Token + 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/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/main.go b/main.go index 1d27b387..af22de4c 100755 --- a/main.go +++ b/main.go @@ -2,31 +2,29 @@ package main import ( "context" - "database/sql" "flag" "fmt" + "log" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" + controllerBase "wuzapi/controllers/controller_base" + "wuzapi/repository" - "go.mau.fi/whatsmeow/store/sqlstore" - waLog "go.mau.fi/whatsmeow/util/log" - + "github.com/glebarez/sqlite" + "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/gorm" ) -type server struct { - db *sql.DB - router *mux.Router - exPath string -} - var ( address = flag.String("address", "0.0.0.0", "Bind IP Address") port = flag.String("port", "8080", "Listen Port") @@ -39,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() { @@ -47,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() } } @@ -75,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,18 +96,26 @@ func main() { panic(err) } - s := &server{ - router: mux.NewRouter(), - db: db, - exPath: exPath, + s := &controllerBase.Controller{ + Router: mux.NewRouter(), + Repository: userRepository, + ExPath: exPath, + UserInfoCache: userinfocache, + KillChannel: killchannel, + ClientPointer: make(map[int]*whatsmeow.Client), + Container: container, + ClientHttp: make(map[int]*resty.Client), + WaDebug: waDebug, + LogType: logType, } - s.routes() - s.connectOnStartup() + routes(s) + + s.ConnectOnStartup() srv := &http.Server{ Addr: *address + ":" + *port, - Handler: s.router, + Handler: s.Router, } done := make(chan os.Signal, 1) @@ -121,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() { @@ -143,8 +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(token string, userRepository repository.UserRepository) { + err := userRepository.CreateAdminUser(token) + if err != nil { + log.Fatal(err) + } } 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 +} 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 new file mode 100644 index 00000000..9af27fc9 --- /dev/null +++ b/repository/user_repository.go @@ -0,0 +1,101 @@ +package repository + +import ( + "errors" + "fmt" + "gorm.io/gorm" +) + +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) + DeleteUserByToken(token string) error + GetQrCode(userId int) (string, error) + Updates(user *User) error +} + +type userRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{db: db} +} + +func (r *userRepository) CreateUser(user *User) (*User, error) { + if foundUser, _ := r.GetUserByToken(user.Token); foundUser != nil { + return nil, errors.New("UserDb already exists") + } + + 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.Id != 0 { + user.Token = token + return r.db.Model(&User{}).Where("name like 'admin'").Updates(user).Error + } + return r.db.Create(&User{Name: "admin", Token: token}).Error +} + +func (r *userRepository) Updates(user *User) error { + return r.db.Updates(user).Error +} + +func (r *userRepository) GetConnectedUsers() ([]*User, error) { + var users []*User + + err := r.db.Find(&users, "connected = 1").Error + + if err != nil { + return nil, err + } + return users, nil +} + +func (r *userRepository) DeleteUserByToken(token string) error { + var user *UserDb + err := r.db.Find(&user, "token = ?", token).Error + if err != nil { + return err + } + if user == nil { + return fmt.Errorf("user not found for token %s", token) + } + err = r.db.Delete(&user).Error + if err != nil { + return err + } + return nil +} + +func (r *userRepository) GetQrCode(userId int) (string, error) { + user, err := r.GetUserById(userId) + if err != nil { + return "", err + } + return user.QrCode, nil +} + +func (r *userRepository) GetUserByToken(token string) (user *User, err error) { + err = r.db.Find(&user, "token like ?", token).Error + return +} + +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 *User, err error) { + err = r.db.Find(&user, userId).Error + return +} diff --git a/routes.go b/routes.go index 477ddf90..c4ed201a 100644 --- a/routes.go +++ b/routes.go @@ -5,6 +5,13 @@ import ( "os" "path/filepath" "time" + "wuzapi/controllers/chat" + controllerBase "wuzapi/controllers/controller_base" + "wuzapi/controllers/group" + "wuzapi/controllers/session" + "wuzapi/controllers/user" + "wuzapi/controllers/webhook" + internalTypes "wuzapi/internal/types" "github.com/justinas/alice" "github.com/rs/zerolog" @@ -13,7 +20,7 @@ import ( type Middleware = alice.Constructor -func (s *server) routes() { +func routes(s *controllerBase.Controller) { ex, err := os.Executable() if err != nil { @@ -22,15 +29,15 @@ func (s *server) routes() { 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(s.AuthAlice) + 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(). @@ -39,7 +46,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")) @@ -47,48 +54,23 @@ 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} + 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} + webhookController.SignRoutes(c) - 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} + 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("POST") - 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} + 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") + userController := &user.UserController{Controller: s} + userController.SignRoutes(c) - 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} + groupController.SignRoutes(c) - s.router.PathPrefix("/").Handler(http.FileServer(http.Dir(exPath + "/static/"))) + s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir(exPath + "/static/"))) } 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) +}