Lint fixes and testing workflow actions
Some checks failed
CI / Lint (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Test (1.23) (push) Has been cancelled
CI / Test (1.22) (push) Has been cancelled

This commit is contained in:
2025-12-29 10:24:18 +02:00
parent 767a9e211f
commit a3eca09502
20 changed files with 532 additions and 119 deletions

103
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: CI
run-name: "CI Pipeline"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.22', '1.23']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Cache Go modules
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Run unit tests
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
flags: unittests
name: codecov-umbrella
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Install golangci-lint
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Run linter
run: make lint
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Download dependencies
run: go mod download
- name: Build binary
run: make build
- name: Verify binaries exist
run: |
if [ ! -f bin/whatshook-server ]; then
echo "Error: Server binary not found at bin/whatshook-server"
exit 1
fi
if [ ! -f bin/whatshook-cli ]; then
echo "Error: CLI binary not found at bin/whatshook-cli"
exit 1
fi
echo "Build successful!"
ls -lh bin/
- name: Check mod tidiness
run: |
go mod tidy
git diff --exit-code go.mod go.sum

116
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: Release
run-name: "Making Release"
on:
push:
tags:
- 'v*.*.*'
jobs:
build-and-release:
name: Build and Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Get version from tag
id: get_version
run: |
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
echo "Version: ${GITHUB_REF#refs/tags/}"
- name: Build binaries for multiple platforms
run: |
mkdir -p dist
# Linux AMD64
GOOS=linux GOARCH=amd64 go build -o dist/relspec-linux-amd64 -ldflags "-X main.version=${{ steps.get_version.outputs.VERSION }}" ./cmd/relspec
# Linux ARM64
GOOS=linux GOARCH=arm64 go build -o dist/relspec-linux-arm64 -ldflags "-X main.version=${{ steps.get_version.outputs.VERSION }}" ./cmd/relspec
# macOS AMD64
GOOS=darwin GOARCH=amd64 go build -o dist/relspec-darwin-amd64 -ldflags "-X main.version=${{ steps.get_version.outputs.VERSION }}" ./cmd/relspec
# macOS ARM64 (Apple Silicon)
GOOS=darwin GOARCH=arm64 go build -o dist/relspec-darwin-arm64 -ldflags "-X main.version=${{ steps.get_version.outputs.VERSION }}" ./cmd/relspec
# Windows AMD64
GOOS=windows GOARCH=amd64 go build -o dist/relspec-windows-amd64.exe -ldflags "-X main.version=${{ steps.get_version.outputs.VERSION }}" ./cmd/relspec
# Create checksums
cd dist
sha256sum * > checksums.txt
cd ..
- name: Generate release notes
id: release_notes
run: |
# Get the previous tag
previous_tag=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -z "$previous_tag" ]; then
# No previous tag, get all commits
commits=$(git log --pretty=format:"- %s (%h)" --no-merges)
else
# Get commits since the previous tag
commits=$(git log "${previous_tag}..HEAD" --pretty=format:"- %s (%h)" --no-merges)
fi
# Create release notes
cat > release_notes.md << EOF
# Release ${{ steps.get_version.outputs.VERSION }}
## Changes
${commits}
## Installation
Download the appropriate binary for your platform:
- **Linux (AMD64)**: \`relspec-linux-amd64\`
- **Linux (ARM64)**: \`relspec-linux-arm64\`
- **macOS (Intel)**: \`relspec-darwin-amd64\`
- **macOS (Apple Silicon)**: \`relspec-darwin-arm64\`
- **Windows (AMD64)**: \`relspec-windows-amd64.exe\`
Make the binary executable (Linux/macOS):
\`\`\`bash
chmod +x relspec-*
\`\`\`
Verify the download with the provided checksums.
EOF
- name: Create Release
uses: softprops/action-gh-release@v1
with:
body_path: release_notes.md
files: |
dist/relspec-linux-amd64
dist/relspec-linux-arm64
dist/relspec-darwin-amd64
dist/relspec-darwin-arm64
dist/relspec-windows-amd64.exe
dist/checksums.txt
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
run: |
echo "Release ${{ steps.get_version.outputs.VERSION }} created successfully!"
echo "Binaries built for:"
echo " - Linux (amd64, arm64)"
echo " - macOS (amd64, arm64)"
echo " - Windows (amd64)"

114
.golangci.json Normal file
View File

@@ -0,0 +1,114 @@
{
"formatters": {
"enable": [
"gofmt",
"goimports"
],
"exclusions": {
"generated": "lax",
"paths": [
"third_party$",
"builtin$",
"examples$"
]
},
"settings": {
"gofmt": {
"simplify": true
},
"goimports": {
"local-prefixes": [
"git.warky.dev/wdevs/relspecgo"
]
}
}
},
"issues": {
"max-issues-per-linter": 0,
"max-same-issues": 0
},
"linters": {
"enable": [
"gocritic",
"misspell",
"revive"
],
"exclusions": {
"generated": "lax",
"paths": [
"third_party$",
"builtin$",
"examples$",
"mocks?",
"tests?"
],
"rules": [
{
"linters": [
"dupl",
"errcheck",
"gocritic",
"gosec"
],
"path": "_test\\.go"
},
{
"linters": [
"errcheck"
],
"text": "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
},
{
"path": "_test\\.go",
"text": "cognitive complexity|cyclomatic complexity"
}
]
},
"settings": {
"errcheck": {
"check-blank": false,
"check-type-assertions": false
},
"gocritic": {
"enabled-checks": [
"boolExprSimplify",
"builtinShadow",
"emptyFallthrough",
"equalFold",
"indexAlloc",
"initClause",
"methodExprCall",
"nilValReturn",
"rangeExprCopy",
"rangeValCopy",
"stringXbytes",
"typeAssertChain",
"unlabelStmt",
"unnamedResult",
"unnecessaryBlock",
"weakCond",
"yodaStyleExpr"
],
"disabled-checks": [
"ifElseChain"
]
},
"revive": {
"rules": [
{
"disabled": true,
"name": "exported"
},
{
"disabled": true,
"name": "package-comments"
}
]
}
}
},
"run": {
"tests": true
},
"version": "2"
}

View File

@@ -1,4 +1,4 @@
.PHONY: build clean test run-server run-cli help
.PHONY: build clean test lint lintfix run-server run-cli help
# Build both server and CLI
build:
@@ -29,10 +29,11 @@ clean:
@rm -f bin/whatshook*
@echo "Clean complete!"
# Run tests
# Run tests with coverage
test:
@echo "Running tests..."
@go test ./...
@go test -v -coverprofile=coverage.out -covermode=atomic ./...
@echo "Coverage report saved to coverage.out"
# Run server (requires config.json)
run-server:
@@ -49,6 +50,51 @@ deps:
@go mod tidy
@echo "Dependencies installed!"
release-version: ## Create and push a release with specific version (use: make release-version VERSION=v1.2.3)
@if [ -z "$(VERSION)" ]; then \
echo "Error: VERSION is required. Usage: make release-version VERSION=v1.2.3"; \
exit 1; \
fi
@version="$(VERSION)"; \
if ! echo "$$version" | grep -q "^v"; then \
version="v$$version"; \
fi; \
echo "Creating release: $$version"; \
latest_tag=$$(git describe --tags --abbrev=0 2>/dev/null || echo ""); \
if [ -z "$$latest_tag" ]; then \
commit_logs=$$(git log --pretty=format:"- %s" --no-merges); \
else \
commit_logs=$$(git log "$${latest_tag}..HEAD" --pretty=format:"- %s" --no-merges); \
fi; \
if [ -z "$$commit_logs" ]; then \
tag_message="Release $$version"; \
else \
tag_message="Release $$version\n\n$$commit_logs"; \
fi; \
git tag -a "$$version" -m "$$tag_message"; \
git push origin "$$version"; \
echo "Tag $$version created and pushed to remote repository."
lint: ## Run linter
@echo "Running linter..."
@if command -v golangci-lint > /dev/null; then \
golangci-lint run --config=.golangci.json; \
else \
echo "golangci-lint not installed. Install with: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
exit 1; \
fi
lintfix: ## Run linter
@echo "Running linter..."
@if command -v golangci-lint > /dev/null; then \
golangci-lint run --config=.golangci.json --fix; \
else \
echo "golangci-lint not installed. Install with: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
exit 1; \
fi
# Help
help:
@echo "WhatsHooked Makefile"
@@ -58,7 +104,9 @@ help:
@echo " make build-server - Build server only"
@echo " make build-cli - Build CLI only"
@echo " make clean - Remove build artifacts (preserves bin directory)"
@echo " make test - Run tests"
@echo " make test - Run tests with coverage"
@echo " make lint - Run linter"
@echo " make lintfix - Run linter with auto-fix"
@echo " make run-server - Run server (requires config.json)"
@echo " make run-cli ARGS='health' - Run CLI with arguments"
@echo " make deps - Install dependencies"

View File

@@ -66,13 +66,19 @@ func addAccount(client *Client) {
var account config.WhatsAppConfig
fmt.Print("Account ID: ")
fmt.Scanln(&account.ID)
if _, err := fmt.Scanln(&account.ID); err != nil {
checkError(fmt.Errorf("error reading account ID: %v", err))
}
fmt.Print("Phone Number (with country code): ")
fmt.Scanln(&account.PhoneNumber)
if _, err := fmt.Scanln(&account.PhoneNumber); err != nil {
checkError(fmt.Errorf("error reading phone number: %v", err))
}
fmt.Print("Session Path: ")
fmt.Scanln(&account.SessionPath)
if _, err := fmt.Scanln(&account.SessionPath); err != nil {
checkError(fmt.Errorf("error reading session path: %v", err))
}
resp, err := client.Post("/api/accounts/add", account)
checkError(err)

View File

@@ -95,16 +95,25 @@ func addHook(client *Client) {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Hook ID: ")
fmt.Scanln(&hook.ID)
if _, err := fmt.Scanln(&hook.ID); err != nil {
checkError(fmt.Errorf("error reading hook ID: %v", err))
}
fmt.Print("Hook Name: ")
fmt.Scanln(&hook.Name)
if _, err := fmt.Scanln(&hook.Name); err != nil {
checkError(fmt.Errorf("error reading hook name: %v", err))
}
fmt.Print("Webhook URL: ")
fmt.Scanln(&hook.URL)
if _, err := fmt.Scanln(&hook.URL); err != nil {
checkError(fmt.Errorf("error reading webhook URL: %v", err))
}
fmt.Print("HTTP Method (POST): ")
fmt.Scanln(&hook.Method)
if _, err := fmt.Scanln(&hook.Method); err == nil {
// Successfully read input
fmt.Printf("Selected Method %s", hook.Method)
}
if hook.Method == "" {
hook.Method = "POST"
}

View File

@@ -70,10 +70,14 @@ func sendMessage(client *Client) {
}
fmt.Print("Account ID: ")
fmt.Scanln(&req.AccountID)
if _, err := fmt.Scanln(&req.AccountID); err != nil {
checkError(fmt.Errorf("error reading account ID: %v", err))
}
fmt.Print("Recipient (phone number or JID, e.g., 0834606792 or 1234567890@s.whatsapp.net): ")
fmt.Scanln(&req.To)
if _, err := fmt.Scanln(&req.To); err != nil {
checkError(fmt.Errorf("error reading recipient: %v", err))
}
fmt.Print("Message text: ")
reader := os.Stdin
@@ -101,10 +105,14 @@ func sendImage(client *Client, filePath string) {
}
fmt.Print("Account ID: ")
fmt.Scanln(&req.AccountID)
if _, err := fmt.Scanln(&req.AccountID); err != nil {
checkError(fmt.Errorf("error reading account ID: %v", err))
}
fmt.Print("Recipient (phone number): ")
fmt.Scanln(&req.To)
if _, err := fmt.Scanln(&req.To); err != nil {
checkError(fmt.Errorf("error reading recipient: %v", err))
}
fmt.Print("Caption (optional): ")
reader := os.Stdin
@@ -151,10 +159,14 @@ func sendVideo(client *Client, filePath string) {
}
fmt.Print("Account ID: ")
fmt.Scanln(&req.AccountID)
if _, err := fmt.Scanln(&req.AccountID); err != nil {
checkError(fmt.Errorf("error reading account ID: %v", err))
}
fmt.Print("Recipient (phone number): ")
fmt.Scanln(&req.To)
if _, err := fmt.Scanln(&req.To); err != nil {
checkError(fmt.Errorf("error reading recipient: %v", err))
}
fmt.Print("Caption (optional): ")
reader := os.Stdin
@@ -204,10 +216,14 @@ func sendDocument(client *Client, filePath string) {
}
fmt.Print("Account ID: ")
fmt.Scanln(&req.AccountID)
if _, err := fmt.Scanln(&req.AccountID); err != nil {
checkError(fmt.Errorf("error reading account ID: %v", err))
}
fmt.Print("Recipient (phone number): ")
fmt.Scanln(&req.To)
if _, err := fmt.Scanln(&req.To); err != nil {
checkError(fmt.Errorf("error reading recipient: %v", err))
}
fmt.Print("Caption (optional): ")
reader := os.Stdin

View File

@@ -28,12 +28,12 @@ type ServerConfig struct {
// WhatsAppConfig holds configuration for a WhatsApp account
type WhatsAppConfig struct {
ID string `json:"id"`
Type string `json:"type"` // "whatsmeow" or "business-api"
PhoneNumber string `json:"phone_number"`
SessionPath string `json:"session_path,omitempty"`
ShowQR bool `json:"show_qr,omitempty"`
BusinessAPI *BusinessAPIConfig `json:"business_api,omitempty"`
ID string `json:"id"`
Type string `json:"type"` // "whatsmeow" or "business-api"
PhoneNumber string `json:"phone_number"`
SessionPath string `json:"session_path,omitempty"`
ShowQR bool `json:"show_qr,omitempty"`
BusinessAPI *BusinessAPIConfig `json:"business_api,omitempty"`
}
// BusinessAPIConfig holds configuration for WhatsApp Business API
@@ -72,7 +72,7 @@ type DatabaseConfig struct {
// MediaConfig holds media storage and delivery configuration
type MediaConfig struct {
DataPath string `json:"data_path"`
Mode string `json:"mode"` // "base64", "link", or "both"
Mode string `json:"mode"` // "base64", "link", or "both"
BaseURL string `json:"base_url,omitempty"` // Base URL for media links
}

View File

@@ -35,10 +35,10 @@ const (
// Event represents an event in the system
type Event struct {
Type EventType `json:"type"`
Timestamp time.Time `json:"timestamp"`
Data map[string]any `json:"data"`
Context context.Context `json:"-"`
Type EventType `json:"type"`
Timestamp time.Time `json:"timestamp"`
Data map[string]any `json:"data"`
Context context.Context `json:"-"`
}
// Subscriber is a function that handles events

View File

@@ -12,7 +12,7 @@ import (
// Accounts returns the list of all configured WhatsApp accounts
func (h *Handlers) Accounts(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(h.config.WhatsApp)
writeJSON(w, h.config.WhatsApp)
}
// AddAccount adds a new WhatsApp account to the system
@@ -43,5 +43,5 @@ func (h *Handlers) AddAccount(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
writeJSON(w, map[string]string{"status": "ok"})
}

View File

@@ -77,7 +77,7 @@ func (h *Handlers) businessAPIWebhookVerify(w http.ResponseWriter, r *http.Reque
if mode == "subscribe" && token == accountConfig.VerifyToken {
logging.Info("Webhook verification successful", "account_id", accountID)
w.WriteHeader(http.StatusOK)
w.Write([]byte(challenge))
writeBytes(w, []byte(challenge))
return
}
@@ -130,7 +130,7 @@ func (h *Handlers) businessAPIWebhookEvent(w http.ResponseWriter, r *http.Reques
// Return 200 OK to acknowledge receipt
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
writeBytes(w, []byte("OK"))
}
// extractAccountIDFromPath extracts the account ID from the URL path

View File

@@ -1,10 +1,12 @@
package handlers
import (
"encoding/json"
"net/http"
"git.warky.dev/wdevs/whatshooked/pkg/config"
"git.warky.dev/wdevs/whatshooked/pkg/hooks"
"git.warky.dev/wdevs/whatshooked/pkg/logging"
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
)
@@ -54,3 +56,17 @@ func (h *Handlers) WithAuthConfig(cfg *AuthConfig) *Handlers {
h.authConfig = cfg
return h
}
// writeJSON is a helper that writes JSON response and logs errors
func writeJSON(w http.ResponseWriter, v interface{}) {
if err := json.NewEncoder(w).Encode(v); err != nil {
logging.Error("Failed to encode JSON response", "error", err)
}
}
// writeBytes is a helper that writes bytes and logs errors
func writeBytes(w http.ResponseWriter, data []byte) {
if _, err := w.Write(data); err != nil {
logging.Error("Failed to write response", "error", err)
}
}

View File

@@ -1,11 +1,10 @@
package handlers
import (
"encoding/json"
"net/http"
)
// Health handles health check requests
func (h *Handlers) Health(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
writeJSON(w, map[string]string{"status": "ok"})
}

View File

@@ -12,7 +12,7 @@ import (
func (h *Handlers) Hooks(w http.ResponseWriter, r *http.Request) {
hooks := h.hookMgr.ListHooks()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(hooks)
writeJSON(w, hooks)
}
// AddHook adds a new hook to the system
@@ -39,7 +39,7 @@ func (h *Handlers) AddHook(w http.ResponseWriter, r *http.Request) {
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
writeJSON(w, map[string]string{"status": "ok"})
}
// RemoveHook removes a hook from the system
@@ -70,5 +70,5 @@ func (h *Handlers) RemoveHook(w http.ResponseWriter, r *http.Request) {
}
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
writeJSON(w, map[string]string{"status": "ok"})
}

View File

@@ -40,7 +40,7 @@ func (h *Handlers) SendMessage(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
writeJSON(w, map[string]string{"status": "ok"})
}
// SendImage sends an image via WhatsApp
@@ -87,7 +87,7 @@ func (h *Handlers) SendImage(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
writeJSON(w, map[string]string{"status": "ok"})
}
// SendVideo sends a video via WhatsApp
@@ -134,7 +134,7 @@ func (h *Handlers) SendVideo(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
writeJSON(w, map[string]string{"status": "ok"})
}
// SendDocument sends a document via WhatsApp
@@ -185,5 +185,5 @@ func (h *Handlers) SendDocument(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
writeJSON(w, map[string]string{"status": "ok"})
}

View File

@@ -340,15 +340,3 @@ func jidToPhoneNumber(jid types.JID) string {
return phone
}
// phoneNumberToJID converts an E.164 phone number to WhatsApp JID
func phoneNumberToJID(phoneNumber string) types.JID {
// Remove + if present
phone := strings.TrimPrefix(phoneNumber, "+")
// Create JID
return types.JID{
User: phone,
Server: types.DefaultUserServer, // "s.whatsapp.net"
}
}

View File

@@ -32,8 +32,8 @@ func (c *Client) HandleWebhook(r *http.Request) error {
// Process each entry
for _, entry := range payload.Entry {
for _, change := range entry.Changes {
c.processChange(change)
for i := range entry.Changes {
c.processChange(entry.Changes[i])
}
}
@@ -198,10 +198,8 @@ func (c *Client) parseTimestamp(ts string) time.Time {
}
// processMediaData processes media based on the configured mode
func (c *Client) processMediaData(messageID string, data []byte, mimeType string, mediaBase64 *string) (string, string) {
func (c *Client) processMediaData(messageID string, data []byte, mimeType string, mediaBase64 *string) (filename string, mediaURL string) {
mode := c.mediaConfig.Mode
var filename string
var mediaURL string
// Generate filename
ext := getExtensionFromMimeType(mimeType)
@@ -262,23 +260,23 @@ func (c *Client) generateMediaURL(messageID, filename string) string {
// getExtensionFromMimeType returns the file extension for a given MIME type
func getExtensionFromMimeType(mimeType string) string {
extensions := map[string]string{
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"video/mp4": ".mp4",
"video/mpeg": ".mpeg",
"video/webm": ".webm",
"video/3gpp": ".3gp",
"application/pdf": ".pdf",
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"video/mp4": ".mp4",
"video/mpeg": ".mpeg",
"video/webm": ".webm",
"video/3gpp": ".3gp",
"application/pdf": ".pdf",
"application/msword": ".doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
"application/vnd.ms-excel": ".xls",
"application/vnd.ms-excel": ".xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
"text/plain": ".txt",
"text/plain": ".txt",
"application/json": ".json",
"audio/mpeg": ".mp3",
"audio/ogg": ".ogg",
"audio/mpeg": ".mp3",
"audio/ogg": ".ogg",
}
if ext, ok := extensions[mimeType]; ok {

View File

@@ -82,7 +82,7 @@ func (c *Client) uploadMedia(ctx context.Context, data []byte, mimeType string)
}
// downloadMedia downloads media from the Business API using the media ID
func (c *Client) downloadMedia(ctx context.Context, mediaID string) ([]byte, string, error) {
func (c *Client) downloadMedia(ctx context.Context, mediaID string) (data []byte, mimeType string, err error) {
// Step 1: Get the media URL
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
c.config.APIVersion,
@@ -129,10 +129,11 @@ func (c *Client) downloadMedia(ctx context.Context, mediaID string) ([]byte, str
return nil, "", fmt.Errorf("failed to download media, status %d", downloadResp.StatusCode)
}
data, err := io.ReadAll(downloadResp.Body)
data, err = io.ReadAll(downloadResp.Body)
if err != nil {
return nil, "", fmt.Errorf("failed to read media data: %w", err)
}
return data, mediaResp.MimeType, nil
mimeType = mediaResp.MimeType
return
}

View File

@@ -2,13 +2,13 @@ package businessapi
// SendMessageRequest represents a request to send a text message via Business API
type SendMessageRequest struct {
MessagingProduct string `json:"messaging_product"` // Always "whatsapp"
RecipientType string `json:"recipient_type,omitempty"` // "individual"
To string `json:"to"` // Phone number in E.164 format
Type string `json:"type"` // "text", "image", "video", "document"
Text *TextObject `json:"text,omitempty"`
Image *MediaObject `json:"image,omitempty"`
Video *MediaObject `json:"video,omitempty"`
MessagingProduct string `json:"messaging_product"` // Always "whatsapp"
RecipientType string `json:"recipient_type,omitempty"` // "individual"
To string `json:"to"` // Phone number in E.164 format
Type string `json:"type"` // "text", "image", "video", "document"
Text *TextObject `json:"text,omitempty"`
Image *MediaObject `json:"image,omitempty"`
Video *MediaObject `json:"video,omitempty"`
Document *DocumentObject `json:"document,omitempty"`
}
@@ -19,14 +19,14 @@ type TextObject struct {
// MediaObject represents media (image/video) message
type MediaObject struct {
ID string `json:"id,omitempty"` // Media ID (from upload)
ID string `json:"id,omitempty"` // Media ID (from upload)
Link string `json:"link,omitempty"` // Or direct URL
Caption string `json:"caption,omitempty"`
}
// DocumentObject represents a document message
type DocumentObject struct {
ID string `json:"id,omitempty"` // Media ID (from upload)
ID string `json:"id,omitempty"` // Media ID (from upload)
Link string `json:"link,omitempty"` // Or direct URL
Caption string `json:"caption,omitempty"`
Filename string `json:"filename,omitempty"`
@@ -51,11 +51,11 @@ type MediaUploadResponse struct {
// MediaURLResponse represents the response when getting media URL
type MediaURLResponse struct {
URL string `json:"url"` // CDN URL to download media
MimeType string `json:"mime_type"`
SHA256 string `json:"sha256"`
FileSize int64 `json:"file_size"`
ID string `json:"id"`
URL string `json:"url"` // CDN URL to download media
MimeType string `json:"mime_type"`
SHA256 string `json:"sha256"`
FileSize int64 `json:"file_size"`
ID string `json:"id"`
MessagingProduct string `json:"messaging_product"`
}
@@ -90,11 +90,11 @@ type WebhookChange struct {
// WebhookValue contains the actual webhook data
type WebhookValue struct {
MessagingProduct string `json:"messaging_product"`
Metadata WebhookMetadata `json:"metadata"`
Contacts []WebhookContact `json:"contacts,omitempty"`
Messages []WebhookMessage `json:"messages,omitempty"`
Statuses []WebhookStatus `json:"statuses,omitempty"`
MessagingProduct string `json:"messaging_product"`
Metadata WebhookMetadata `json:"metadata"`
Contacts []WebhookContact `json:"contacts,omitempty"`
Messages []WebhookMessage `json:"messages,omitempty"`
Statuses []WebhookStatus `json:"statuses,omitempty"`
}
// WebhookMetadata contains metadata about the phone number
@@ -116,15 +116,15 @@ type WebhookProfile struct {
// WebhookMessage represents a message in the webhook
type WebhookMessage struct {
From string `json:"from"` // Sender phone number
ID string `json:"id"` // Message ID
Timestamp string `json:"timestamp"` // Unix timestamp as string
Type string `json:"type"` // "text", "image", "video", "document", etc.
Text *WebhookText `json:"text,omitempty"`
Image *WebhookMediaMessage `json:"image,omitempty"`
Video *WebhookMediaMessage `json:"video,omitempty"`
From string `json:"from"` // Sender phone number
ID string `json:"id"` // Message ID
Timestamp string `json:"timestamp"` // Unix timestamp as string
Type string `json:"type"` // "text", "image", "video", "document", etc.
Text *WebhookText `json:"text,omitempty"`
Image *WebhookMediaMessage `json:"image,omitempty"`
Video *WebhookMediaMessage `json:"video,omitempty"`
Document *WebhookDocumentMessage `json:"document,omitempty"`
Context *WebhookContext `json:"context,omitempty"` // Reply context
Context *WebhookContext `json:"context,omitempty"` // Reply context
}
// WebhookText represents a text message
@@ -158,20 +158,20 @@ type WebhookContext struct {
// WebhookStatus represents a message status update
type WebhookStatus struct {
ID string `json:"id"` // Message ID
Status string `json:"status"` // "sent", "delivered", "read", "failed"
Timestamp string `json:"timestamp"` // Unix timestamp as string
RecipientID string `json:"recipient_id"`
Conversation *WebhookConversation `json:"conversation,omitempty"`
Pricing *WebhookPricing `json:"pricing,omitempty"`
Errors []WebhookError `json:"errors,omitempty"`
ID string `json:"id"` // Message ID
Status string `json:"status"` // "sent", "delivered", "read", "failed"
Timestamp string `json:"timestamp"` // Unix timestamp as string
RecipientID string `json:"recipient_id"`
Conversation *WebhookConversation `json:"conversation,omitempty"`
Pricing *WebhookPricing `json:"pricing,omitempty"`
Errors []WebhookError `json:"errors,omitempty"`
}
// WebhookConversation contains conversation details
type WebhookConversation struct {
ID string `json:"id"`
ExpirationTimestamp string `json:"expiration_timestamp,omitempty"`
Origin WebhookOrigin `json:"origin"`
ID string `json:"id"`
ExpirationTimestamp string `json:"expiration_timestamp,omitempty"`
Origin WebhookOrigin `json:"origin"`
}
// WebhookOrigin contains conversation origin

View File

@@ -395,7 +395,7 @@ func (c *Client) handleEvent(evt interface{}) {
// Extract message content based on type
var text string
var messageType string = "text"
var messageType = "text"
var mimeType string
var filename string
var mediaBase64 string
@@ -516,12 +516,13 @@ func (c *Client) handleEvent(evt interface{}) {
case *waEvents.Receipt:
// Handle delivery and read receipts
if v.Type == types.ReceiptTypeDelivered {
switch v.Type {
case types.ReceiptTypeDelivered:
for _, messageID := range v.MessageIDs {
logging.Debug("Message delivered", "account_id", c.id, "message_id", messageID, "from", v.Sender.String())
c.eventBus.Publish(events.MessageDeliveredEvent(ctx, c.id, messageID, v.Sender.String(), v.Timestamp))
}
} else if v.Type == types.ReceiptTypeRead {
case types.ReceiptTypeRead:
for _, messageID := range v.MessageIDs {
logging.Debug("Message read", "account_id", c.id, "message_id", messageID, "from", v.Sender.String())
c.eventBus.Publish(events.MessageReadEvent(ctx, c.id, messageID, v.Sender.String(), v.Timestamp))
@@ -561,10 +562,8 @@ func (c *Client) startKeepAlive() {
}
// processMediaData processes media based on the configured mode
func (c *Client) processMediaData(messageID string, data []byte, mimeType string, mediaBase64 *string) (string, string) {
func (c *Client) processMediaData(messageID string, data []byte, mimeType string, mediaBase64 *string) (filename string, mediaURL string) {
mode := c.mediaConfig.Mode
var filename string
var mediaURL string
// Generate filename
ext := getExtensionFromMimeType(mimeType)