Server refactor completed
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,4 +45,5 @@ sessions/
|
|||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
/server
|
||||||
|
|||||||
6
TODO.md
6
TODO.md
@@ -1,9 +1,9 @@
|
|||||||
# Todo List
|
# Todo List
|
||||||
|
|
||||||
## General todo
|
## General todo
|
||||||
- [ ] Docker Server Support with docker-compose.yml (Basic Config from .ENV file)
|
- [✔️] Docker Server Support with docker-compose.yml (Basic Config from .ENV file)
|
||||||
- [ ] Authentication options for cli
|
- [✔️] Authentication options for cli
|
||||||
- [ ] **Refactor** the code to make it more readable and maintainable. (Split server, hooks and routes. Split CLI into commands etc. Common connection code.)
|
- [✔️] **Refactor** the code to make it more readable and maintainable. (Split server, hooks and routes. Split CLI into commands etc. Common connection code.)
|
||||||
- [ ] Whatsapp Business API support add
|
- [ ] Whatsapp Business API support add
|
||||||
- [ ] Optional Postgres server connection for Whatsmeo
|
- [ ] Optional Postgres server connection for Whatsmeo
|
||||||
- [ ] Optional Postgres server,database for event saving and hook registration
|
- [ ] Optional Postgres server,database for event saving and hook registration
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -23,42 +21,84 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configPath = flag.String("config", "config.json", "Path to configuration file")
|
configPath = flag.String("config", "", "Path to configuration file (optional, defaults to user home directory)")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
whatsappMgr *whatsapp.Manager
|
configPath string
|
||||||
hookMgr *hooks.Manager
|
whatsappMgr *whatsapp.Manager
|
||||||
httpServer *http.Server
|
hookMgr *hooks.Manager
|
||||||
eventBus *events.EventBus
|
httpServer *http.Server
|
||||||
|
eventBus *events.EventBus
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveConfigPath determines the config file path to use
|
||||||
|
// Priority: 1) provided path (if exists), 2) config.json in current dir, 3) .whatshooked/config.json in user home
|
||||||
|
func resolveConfigPath(providedPath string) (string, error) {
|
||||||
|
// If a path was explicitly provided, check if it exists
|
||||||
|
if providedPath != "" {
|
||||||
|
if _, err := os.Stat(providedPath); err == nil {
|
||||||
|
return providedPath, nil
|
||||||
|
}
|
||||||
|
// Directory doesn't exist, fall through to default locations
|
||||||
|
logging.Info("Provided config path directory does not exist, using default locations", "path", providedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for config.json in current directory
|
||||||
|
currentDirConfig := "config.json"
|
||||||
|
if _, err := os.Stat(currentDirConfig); err == nil {
|
||||||
|
return currentDirConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to user home directory
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create .whatshooked directory if it doesn't exist
|
||||||
|
configDir := filepath.Join(homeDir, ".whatshooked")
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(configDir, "config.json"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Load configuration
|
// Resolve config path
|
||||||
cfg, err := config.Load(*configPath)
|
cfgPath, err := resolveConfigPath(*configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to resolve config path: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.Load(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to load config from %s: %v\n", cfgPath, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize logging
|
// Initialize logging
|
||||||
logging.Init(cfg.LogLevel)
|
logging.Init(cfg.LogLevel)
|
||||||
logging.Info("Starting WhatsHooked server")
|
logging.Info("Starting WhatsHooked server", "config_path", cfgPath)
|
||||||
|
|
||||||
// Create event bus
|
// Create event bus
|
||||||
eventBus := events.NewEventBus()
|
eventBus := events.NewEventBus()
|
||||||
|
|
||||||
// Create server with config update callback
|
// Create server with config update callback
|
||||||
srv := &Server{
|
srv := &Server{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
eventBus: eventBus,
|
configPath: cfgPath,
|
||||||
whatsappMgr: whatsapp.NewManager(eventBus, cfg.Media, cfg, *configPath, func(updatedCfg *config.Config) error {
|
eventBus: eventBus,
|
||||||
return config.Save(*configPath, updatedCfg)
|
whatsappMgr: whatsapp.NewManager(eventBus, cfg.Media, cfg, cfgPath, func(updatedCfg *config.Config) error {
|
||||||
|
return config.Save(cfgPath, updatedCfg)
|
||||||
}),
|
}),
|
||||||
hookMgr: hooks.NewManager(eventBus),
|
hookMgr: hooks.NewManager(eventBus),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load hooks
|
// Load hooks
|
||||||
@@ -152,422 +192,3 @@ func (s *Server) handleHookResponse(event events.Event) {
|
|||||||
logging.Info("Message sent from hook response", "account_id", targetAccountID, "to", resp.To)
|
logging.Info("Message sent from hook response", "account_id", targetAccountID, "to", resp.To)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// authMiddleware validates authentication credentials
|
|
||||||
func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Check if any authentication is configured
|
|
||||||
hasAuth := s.config.Server.Username != "" || s.config.Server.Password != "" || s.config.Server.AuthKey != ""
|
|
||||||
|
|
||||||
if !hasAuth {
|
|
||||||
// No authentication configured, allow access
|
|
||||||
next(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticated := false
|
|
||||||
|
|
||||||
// Check for API key authentication (x-api-key header or Authorization bearer token)
|
|
||||||
if s.config.Server.AuthKey != "" {
|
|
||||||
// Check x-api-key header
|
|
||||||
apiKey := r.Header.Get("x-api-key")
|
|
||||||
if apiKey == s.config.Server.AuthKey {
|
|
||||||
authenticated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Authorization header for bearer token
|
|
||||||
if !authenticated {
|
|
||||||
authHeader := r.Header.Get("Authorization")
|
|
||||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
|
||||||
token := authHeader[7:]
|
|
||||||
if token == s.config.Server.AuthKey {
|
|
||||||
authenticated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for username/password authentication (HTTP Basic Auth)
|
|
||||||
if !authenticated && s.config.Server.Username != "" && s.config.Server.Password != "" {
|
|
||||||
username, password, ok := r.BasicAuth()
|
|
||||||
if ok && username == s.config.Server.Username && password == s.config.Server.Password {
|
|
||||||
authenticated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !authenticated {
|
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="WhatsHooked Server"`)
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startHTTPServer starts the HTTP server for CLI communication
|
|
||||||
func (s *Server) startHTTPServer() {
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
// Health check (no auth required)
|
|
||||||
mux.HandleFunc("/health", s.handleHealth)
|
|
||||||
|
|
||||||
// Hook management (with auth)
|
|
||||||
mux.HandleFunc("/api/hooks", s.authMiddleware(s.handleHooks))
|
|
||||||
mux.HandleFunc("/api/hooks/add", s.authMiddleware(s.handleAddHook))
|
|
||||||
mux.HandleFunc("/api/hooks/remove", s.authMiddleware(s.handleRemoveHook))
|
|
||||||
|
|
||||||
// Account management (with auth)
|
|
||||||
mux.HandleFunc("/api/accounts", s.authMiddleware(s.handleAccounts))
|
|
||||||
mux.HandleFunc("/api/accounts/add", s.authMiddleware(s.handleAddAccount))
|
|
||||||
|
|
||||||
// Send messages (with auth)
|
|
||||||
mux.HandleFunc("/api/send", s.authMiddleware(s.handleSendMessage))
|
|
||||||
mux.HandleFunc("/api/send/image", s.authMiddleware(s.handleSendImage))
|
|
||||||
mux.HandleFunc("/api/send/video", s.authMiddleware(s.handleSendVideo))
|
|
||||||
mux.HandleFunc("/api/send/document", s.authMiddleware(s.handleSendDocument))
|
|
||||||
|
|
||||||
// Serve media files (with auth)
|
|
||||||
mux.HandleFunc("/api/media/", s.authMiddleware(s.handleServeMedia))
|
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", s.config.Server.Host, s.config.Server.Port)
|
|
||||||
s.httpServer = &http.Server{
|
|
||||||
Addr: addr,
|
|
||||||
Handler: mux,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
logging.Info("Starting HTTP server",
|
|
||||||
"host", s.config.Server.Host,
|
|
||||||
"port", s.config.Server.Port,
|
|
||||||
"address", addr,
|
|
||||||
)
|
|
||||||
logging.Info("HTTP server endpoints available",
|
|
||||||
"health", "/health",
|
|
||||||
"hooks", "/api/hooks",
|
|
||||||
"accounts", "/api/accounts",
|
|
||||||
"send", "/api/send",
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
logging.Error("HTTP server error", "error", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTP Handlers
|
|
||||||
|
|
||||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleHooks(w http.ResponseWriter, r *http.Request) {
|
|
||||||
hooks := s.hookMgr.ListHooks()
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(hooks)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleAddHook(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var hook config.Hook
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&hook); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.hookMgr.AddHook(hook)
|
|
||||||
|
|
||||||
// Update config
|
|
||||||
s.config.Hooks = s.hookMgr.ListHooks()
|
|
||||||
if err := config.Save(*configPath, s.config); err != nil {
|
|
||||||
logging.Error("Failed to save config", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleRemoveHook(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.hookMgr.RemoveHook(req.ID); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update config
|
|
||||||
s.config.Hooks = s.hookMgr.ListHooks()
|
|
||||||
if err := config.Save(*configPath, s.config); err != nil {
|
|
||||||
logging.Error("Failed to save config", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleAccounts(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(s.config.WhatsApp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleAddAccount(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var account config.WhatsAppConfig
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&account); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to the account
|
|
||||||
if err := s.whatsappMgr.Connect(context.Background(), account); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update config
|
|
||||||
s.config.WhatsApp = append(s.config.WhatsApp, account)
|
|
||||||
if err := config.Save(*configPath, s.config); err != nil {
|
|
||||||
logging.Error("Failed to save config", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleSendMessage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
AccountID string `json:"account_id"`
|
|
||||||
To string `json:"to"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format phone number to JID format
|
|
||||||
formattedJID := utils.FormatPhoneToJID(req.To, s.config.Server.DefaultCountryCode)
|
|
||||||
|
|
||||||
jid, err := types.ParseJID(formattedJID)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid JID", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.whatsappMgr.SendTextMessage(r.Context(), req.AccountID, jid, req.Text); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleSendImage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
AccountID string `json:"account_id"`
|
|
||||||
To string `json:"to"`
|
|
||||||
Caption string `json:"caption"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
ImageData string `json:"image_data"` // base64 encoded
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 image data
|
|
||||||
imageData, err := base64.StdEncoding.DecodeString(req.ImageData)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid base64 image data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format phone number to JID format
|
|
||||||
formattedJID := utils.FormatPhoneToJID(req.To, s.config.Server.DefaultCountryCode)
|
|
||||||
jid, err := types.ParseJID(formattedJID)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid JID", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default mime type if not provided
|
|
||||||
if req.MimeType == "" {
|
|
||||||
req.MimeType = "image/jpeg"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.whatsappMgr.SendImage(r.Context(), req.AccountID, jid, imageData, req.MimeType, req.Caption); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleSendVideo(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
AccountID string `json:"account_id"`
|
|
||||||
To string `json:"to"`
|
|
||||||
Caption string `json:"caption"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
VideoData string `json:"video_data"` // base64 encoded
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 video data
|
|
||||||
videoData, err := base64.StdEncoding.DecodeString(req.VideoData)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid base64 video data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format phone number to JID format
|
|
||||||
formattedJID := utils.FormatPhoneToJID(req.To, s.config.Server.DefaultCountryCode)
|
|
||||||
jid, err := types.ParseJID(formattedJID)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid JID", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default mime type if not provided
|
|
||||||
if req.MimeType == "" {
|
|
||||||
req.MimeType = "video/mp4"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.whatsappMgr.SendVideo(r.Context(), req.AccountID, jid, videoData, req.MimeType, req.Caption); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleSendDocument(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
AccountID string `json:"account_id"`
|
|
||||||
To string `json:"to"`
|
|
||||||
Caption string `json:"caption"`
|
|
||||||
MimeType string `json:"mime_type"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
DocumentData string `json:"document_data"` // base64 encoded
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 document data
|
|
||||||
documentData, err := base64.StdEncoding.DecodeString(req.DocumentData)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid base64 document data", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format phone number to JID format
|
|
||||||
formattedJID := utils.FormatPhoneToJID(req.To, s.config.Server.DefaultCountryCode)
|
|
||||||
jid, err := types.ParseJID(formattedJID)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid JID", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default values if not provided
|
|
||||||
if req.MimeType == "" {
|
|
||||||
req.MimeType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
if req.Filename == "" {
|
|
||||||
req.Filename = "document"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.whatsappMgr.SendDocument(r.Context(), req.AccountID, jid, documentData, req.MimeType, req.Filename, req.Caption); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleServeMedia(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Expected path format: /api/media/{accountID}/{filename}
|
|
||||||
path := r.URL.Path[len("/api/media/"):]
|
|
||||||
|
|
||||||
// Split path into accountID and filename
|
|
||||||
var accountID, filename string
|
|
||||||
for i, ch := range path {
|
|
||||||
if ch == '/' {
|
|
||||||
accountID = path[:i]
|
|
||||||
filename = path[i+1:]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if accountID == "" || filename == "" {
|
|
||||||
http.Error(w, "Invalid media path", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct full file path
|
|
||||||
filePath := filepath.Join(s.config.Media.DataPath, accountID, filename)
|
|
||||||
|
|
||||||
// Security check: ensure the resolved path is within the media directory
|
|
||||||
mediaDir := filepath.Join(s.config.Media.DataPath, accountID)
|
|
||||||
absFilePath, err := filepath.Abs(filePath)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
absMediaDir, err := filepath.Abs(mediaDir)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Invalid media directory", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file path is within media directory (prevent directory traversal)
|
|
||||||
if len(absFilePath) < len(absMediaDir) || absFilePath[:len(absMediaDir)] != absMediaDir {
|
|
||||||
http.Error(w, "Access denied", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve the file
|
|
||||||
http.ServeFile(w, r, absFilePath)
|
|
||||||
}
|
|
||||||
|
|||||||
57
cmd/server/middleware.go
Normal file
57
cmd/server/middleware.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// authMiddleware validates authentication credentials
|
||||||
|
func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Check if any authentication is configured
|
||||||
|
hasAuth := s.config.Server.Username != "" || s.config.Server.Password != "" || s.config.Server.AuthKey != ""
|
||||||
|
|
||||||
|
if !hasAuth {
|
||||||
|
// No authentication configured, allow access
|
||||||
|
next(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticated := false
|
||||||
|
|
||||||
|
// Check for API key authentication (x-api-key header or Authorization bearer token)
|
||||||
|
if s.config.Server.AuthKey != "" {
|
||||||
|
// Check x-api-key header
|
||||||
|
apiKey := r.Header.Get("x-api-key")
|
||||||
|
if apiKey == s.config.Server.AuthKey {
|
||||||
|
authenticated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Authorization header for bearer token
|
||||||
|
if !authenticated {
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||||
|
token := authHeader[7:]
|
||||||
|
if token == s.config.Server.AuthKey {
|
||||||
|
authenticated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for username/password authentication (HTTP Basic Auth)
|
||||||
|
if !authenticated && s.config.Server.Username != "" && s.config.Server.Password != "" {
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if ok && username == s.config.Server.Username && password == s.config.Server.Password {
|
||||||
|
authenticated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authenticated {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="WhatsHooked Server"`)
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
cmd/server/routes.go
Normal file
65
cmd/server/routes.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupRoutes configures all HTTP routes for the server
|
||||||
|
func (s *Server) setupRoutes() *http.ServeMux {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// Health check (no auth required)
|
||||||
|
mux.HandleFunc("/health", s.handleHealth)
|
||||||
|
|
||||||
|
// Hook management (with auth)
|
||||||
|
mux.HandleFunc("/api/hooks", s.authMiddleware(s.handleHooks))
|
||||||
|
mux.HandleFunc("/api/hooks/add", s.authMiddleware(s.handleAddHook))
|
||||||
|
mux.HandleFunc("/api/hooks/remove", s.authMiddleware(s.handleRemoveHook))
|
||||||
|
|
||||||
|
// Account management (with auth)
|
||||||
|
mux.HandleFunc("/api/accounts", s.authMiddleware(s.handleAccounts))
|
||||||
|
mux.HandleFunc("/api/accounts/add", s.authMiddleware(s.handleAddAccount))
|
||||||
|
|
||||||
|
// Send messages (with auth)
|
||||||
|
mux.HandleFunc("/api/send", s.authMiddleware(s.handleSendMessage))
|
||||||
|
mux.HandleFunc("/api/send/image", s.authMiddleware(s.handleSendImage))
|
||||||
|
mux.HandleFunc("/api/send/video", s.authMiddleware(s.handleSendVideo))
|
||||||
|
mux.HandleFunc("/api/send/document", s.authMiddleware(s.handleSendDocument))
|
||||||
|
|
||||||
|
// Serve media files (with auth)
|
||||||
|
mux.HandleFunc("/api/media/", s.handleServeMedia)
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
// startHTTPServer starts the HTTP server for CLI communication
|
||||||
|
func (s *Server) startHTTPServer() {
|
||||||
|
mux := s.setupRoutes()
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", s.config.Server.Host, s.config.Server.Port)
|
||||||
|
s.httpServer = &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
logging.Info("Starting HTTP server",
|
||||||
|
"host", s.config.Server.Host,
|
||||||
|
"port", s.config.Server.Port,
|
||||||
|
"address", addr,
|
||||||
|
)
|
||||||
|
logging.Info("HTTP server endpoints available",
|
||||||
|
"health", "/health",
|
||||||
|
"hooks", "/api/hooks",
|
||||||
|
"accounts", "/api/accounts",
|
||||||
|
"send", "/api/send",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logging.Error("HTTP server error", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
45
cmd/server/routes_accounts.go
Normal file
45
cmd/server/routes_accounts.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleAccounts returns the list of all configured WhatsApp accounts
|
||||||
|
func (s *Server) handleAccounts(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(s.config.WhatsApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAddAccount adds a new WhatsApp account to the system
|
||||||
|
func (s *Server) handleAddAccount(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var account config.WhatsAppConfig
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&account); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the account
|
||||||
|
if err := s.whatsappMgr.Connect(context.Background(), account); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config
|
||||||
|
s.config.WhatsApp = append(s.config.WhatsApp, account)
|
||||||
|
if err := config.Save(s.configPath, s.config); err != nil {
|
||||||
|
logging.Error("Failed to save config", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
11
cmd/server/routes_health.go
Normal file
11
cmd/server/routes_health.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleHealth handles health check requests
|
||||||
|
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
70
cmd/server/routes_hooks.go
Normal file
70
cmd/server/routes_hooks.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleHooks returns the list of all configured hooks
|
||||||
|
func (s *Server) handleHooks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
hooks := s.hookMgr.ListHooks()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(hooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAddHook adds a new hook to the system
|
||||||
|
func (s *Server) handleAddHook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var hook config.Hook
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&hook); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.hookMgr.AddHook(hook)
|
||||||
|
|
||||||
|
// Update config
|
||||||
|
s.config.Hooks = s.hookMgr.ListHooks()
|
||||||
|
if err := config.Save(s.configPath, s.config); err != nil {
|
||||||
|
logging.Error("Failed to save config", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRemoveHook removes a hook from the system
|
||||||
|
func (s *Server) handleRemoveHook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.hookMgr.RemoveHook(req.ID); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update config
|
||||||
|
s.config.Hooks = s.hookMgr.ListHooks()
|
||||||
|
if err := config.Save(s.configPath, s.config); err != nil {
|
||||||
|
logging.Error("Failed to save config", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
52
cmd/server/routes_media.go
Normal file
52
cmd/server/routes_media.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleServeMedia serves media files with path traversal protection
|
||||||
|
func (s *Server) handleServeMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Expected path format: /api/media/{accountID}/{filename}
|
||||||
|
path := r.URL.Path[len("/api/media/"):]
|
||||||
|
|
||||||
|
// Split path into accountID and filename
|
||||||
|
var accountID, filename string
|
||||||
|
for i, ch := range path {
|
||||||
|
if ch == '/' {
|
||||||
|
accountID = path[:i]
|
||||||
|
filename = path[i+1:]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if accountID == "" || filename == "" {
|
||||||
|
http.Error(w, "Invalid media path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct full file path
|
||||||
|
filePath := filepath.Join(s.config.Media.DataPath, accountID, filename)
|
||||||
|
|
||||||
|
// Security check: ensure the resolved path is within the media directory
|
||||||
|
mediaDir := filepath.Join(s.config.Media.DataPath, accountID)
|
||||||
|
absFilePath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
absMediaDir, err := filepath.Abs(mediaDir)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid media directory", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file path is within media directory (prevent directory traversal)
|
||||||
|
if len(absFilePath) < len(absMediaDir) || absFilePath[:len(absMediaDir)] != absMediaDir {
|
||||||
|
http.Error(w, "Access denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the file
|
||||||
|
http.ServeFile(w, r, absFilePath)
|
||||||
|
}
|
||||||
189
cmd/server/routes_send.go
Normal file
189
cmd/server/routes_send.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/internal/utils"
|
||||||
|
"go.mau.fi/whatsmeow/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleSendMessage sends a text message via WhatsApp
|
||||||
|
func (s *Server) handleSendMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format phone number to JID format
|
||||||
|
formattedJID := utils.FormatPhoneToJID(req.To, s.config.Server.DefaultCountryCode)
|
||||||
|
|
||||||
|
jid, err := types.ParseJID(formattedJID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid JID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.whatsappMgr.SendTextMessage(r.Context(), req.AccountID, jid, req.Text); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSendImage sends an image via WhatsApp
|
||||||
|
func (s *Server) handleSendImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Caption string `json:"caption"`
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
ImageData string `json:"image_data"` // base64 encoded
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 image data
|
||||||
|
imageData, err := base64.StdEncoding.DecodeString(req.ImageData)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid base64 image data", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format phone number to JID format
|
||||||
|
formattedJID := utils.FormatPhoneToJID(req.To, s.config.Server.DefaultCountryCode)
|
||||||
|
jid, err := types.ParseJID(formattedJID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid JID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default mime type if not provided
|
||||||
|
if req.MimeType == "" {
|
||||||
|
req.MimeType = "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.whatsappMgr.SendImage(r.Context(), req.AccountID, jid, imageData, req.MimeType, req.Caption); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSendVideo sends a video via WhatsApp
|
||||||
|
func (s *Server) handleSendVideo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Caption string `json:"caption"`
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
VideoData string `json:"video_data"` // base64 encoded
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 video data
|
||||||
|
videoData, err := base64.StdEncoding.DecodeString(req.VideoData)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid base64 video data", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format phone number to JID format
|
||||||
|
formattedJID := utils.FormatPhoneToJID(req.To, s.config.Server.DefaultCountryCode)
|
||||||
|
jid, err := types.ParseJID(formattedJID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid JID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default mime type if not provided
|
||||||
|
if req.MimeType == "" {
|
||||||
|
req.MimeType = "video/mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.whatsappMgr.SendVideo(r.Context(), req.AccountID, jid, videoData, req.MimeType, req.Caption); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSendDocument sends a document via WhatsApp
|
||||||
|
func (s *Server) handleSendDocument(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Caption string `json:"caption"`
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
DocumentData string `json:"document_data"` // base64 encoded
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 document data
|
||||||
|
documentData, err := base64.StdEncoding.DecodeString(req.DocumentData)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid base64 document data", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format phone number to JID format
|
||||||
|
formattedJID := utils.FormatPhoneToJID(req.To, s.config.Server.DefaultCountryCode)
|
||||||
|
jid, err := types.ParseJID(formattedJID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid JID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values if not provided
|
||||||
|
if req.MimeType == "" {
|
||||||
|
req.MimeType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
if req.Filename == "" {
|
||||||
|
req.Filename = "document"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.whatsappMgr.SendDocument(r.Context(), req.AccountID, jid, documentData, req.MimeType, req.Filename, req.Caption); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user