This is still pulling from our generic district mock, but it's still better than what we had. This also includes adding static content hosting for the bootstrap content on the public domain.
150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package sync
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type SMSWebhookBody struct {
|
|
Data SMSWebhookData `json:"data"`
|
|
}
|
|
type SMSWebhookData struct {
|
|
ID int64 `json:"id"`
|
|
EventType string `json:"event_type"`
|
|
RecordType string `json:"record_type"`
|
|
Payload SMSMessagePayload `json:"payload"`
|
|
}
|
|
|
|
type SMSMessagePayload struct {
|
|
ID int64 `json:"id"`
|
|
RecordType string `json:"record_type"`
|
|
From SMSContact `json:"from"`
|
|
To []SMSContact `json:"to"`
|
|
Text string `json:"text"`
|
|
ReceivedAt string `json:"received_at"`
|
|
Type string `json:"type"`
|
|
Media []MMSMedia `json:"media"`
|
|
}
|
|
|
|
type MMSMedia struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// Contact represents a phone contact
|
|
type SMSContact struct {
|
|
PhoneNumber string `json:"phone_number"`
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
func handleSMSMessage(data *SMSWebhookData) error {
|
|
log.Info().Int64("ID", data.ID).Str("event_type", data.EventType).Str("record_type", data.RecordType).Str("from", data.Payload.From.PhoneNumber).Str("msg", data.Payload.Text).Str("receieved", data.Payload.ReceivedAt).Msg("Got SMS Message")
|
|
|
|
for _, media := range data.Payload.Media {
|
|
filePath, err := downloadMedia(media.URL)
|
|
if err != nil {
|
|
fmt.Errorf("Failed to download media from %s: %w", filePath, err)
|
|
continue
|
|
}
|
|
fmt.Printf("Downloaded media to: %s\n", filePath)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DownloadMedia downloads a media file from the given URL to a temporary location
|
|
// and returns the path to the downloaded file
|
|
func downloadMedia(mediaURL string) (string, error) {
|
|
// Make GET request to the media URL
|
|
resp, err := http.Get(mediaURL)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to download media: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("failed to download media: status code %d", resp.StatusCode)
|
|
}
|
|
|
|
// Extract filename from URL or headers
|
|
filename := getFilenameFromURL(mediaURL, resp)
|
|
|
|
// Create temporary file with proper extension
|
|
tmpDir := os.TempDir()
|
|
timestamp := time.Now().UnixNano()
|
|
tmpFilePath := filepath.Join(tmpDir, fmt.Sprintf("media_%d_%s", timestamp, filename))
|
|
|
|
// Create the file
|
|
out, err := os.Create(tmpFilePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create temporary file: %w", err)
|
|
}
|
|
defer out.Close()
|
|
|
|
// Write the response body to the file
|
|
_, err = io.Copy(out, resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to save media file: %w", err)
|
|
}
|
|
|
|
return tmpFilePath, nil
|
|
}
|
|
|
|
// getFilenameFromURL extracts filename from URL or Content-Disposition header
|
|
func getFilenameFromURL(mediaURL string, resp *http.Response) string {
|
|
// First try Content-Disposition header
|
|
contentDisp := resp.Header.Get("Content-Disposition")
|
|
if contentDisp != "" {
|
|
if strings.Contains(contentDisp, "filename=") {
|
|
parts := strings.Split(contentDisp, "filename=")
|
|
if len(parts) > 1 {
|
|
filename := strings.Trim(parts[1], "\"' ")
|
|
if filename != "" {
|
|
return sanitizeFilename(filename)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to URL path
|
|
urlPath := path.Base(mediaURL)
|
|
if urlPath != "" && urlPath != "." && urlPath != "/" {
|
|
return sanitizeFilename(urlPath)
|
|
}
|
|
|
|
// Default to generic name with extension based on Content-Type
|
|
contentType := resp.Header.Get("Content-Type")
|
|
ext := ".bin"
|
|
|
|
switch {
|
|
case strings.Contains(contentType, "image/jpeg"):
|
|
ext = ".jpg"
|
|
case strings.Contains(contentType, "image/png"):
|
|
ext = ".png"
|
|
case strings.Contains(contentType, "image/gif"):
|
|
ext = ".gif"
|
|
case strings.Contains(contentType, "video/mp4"):
|
|
ext = ".mp4"
|
|
case strings.Contains(contentType, "audio/mpeg"):
|
|
ext = ".mp3"
|
|
}
|
|
|
|
return fmt.Sprintf("media%s", ext)
|
|
}
|
|
|
|
// sanitizeFilename removes potentially unsafe characters from filename
|
|
func sanitizeFilename(name string) string {
|
|
// Replace unsafe characters with underscore
|
|
unsafe := []string{"/", "\\", "?", "%", "*", ":", "|", "\"", "<", ">"}
|
|
result := name
|
|
for _, c := range unsafe {
|
|
result = strings.ReplaceAll(result, c, "_")
|
|
}
|
|
return result
|
|
}
|