Lint fixes and testing workflow actions
This commit is contained in:
103
.github/workflows/ci.yml
vendored
Normal file
103
.github/workflows/ci.yml
vendored
Normal 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
116
.github/workflows/release.yml
vendored
Normal 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
114
.golangci.json
Normal 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"
|
||||||
|
}
|
||||||
56
Makefile
56
Makefile
@@ -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 both server and CLI
|
||||||
build:
|
build:
|
||||||
@@ -29,10 +29,11 @@ clean:
|
|||||||
@rm -f bin/whatshook*
|
@rm -f bin/whatshook*
|
||||||
@echo "Clean complete!"
|
@echo "Clean complete!"
|
||||||
|
|
||||||
# Run tests
|
# Run tests with coverage
|
||||||
test:
|
test:
|
||||||
@echo "Running tests..."
|
@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 (requires config.json)
|
||||||
run-server:
|
run-server:
|
||||||
@@ -49,6 +50,51 @@ deps:
|
|||||||
@go mod tidy
|
@go mod tidy
|
||||||
@echo "Dependencies installed!"
|
@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
|
||||||
help:
|
help:
|
||||||
@echo "WhatsHooked Makefile"
|
@echo "WhatsHooked Makefile"
|
||||||
@@ -58,7 +104,9 @@ help:
|
|||||||
@echo " make build-server - Build server only"
|
@echo " make build-server - Build server only"
|
||||||
@echo " make build-cli - Build CLI only"
|
@echo " make build-cli - Build CLI only"
|
||||||
@echo " make clean - Remove build artifacts (preserves bin directory)"
|
@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-server - Run server (requires config.json)"
|
||||||
@echo " make run-cli ARGS='health' - Run CLI with arguments"
|
@echo " make run-cli ARGS='health' - Run CLI with arguments"
|
||||||
@echo " make deps - Install dependencies"
|
@echo " make deps - Install dependencies"
|
||||||
|
|||||||
@@ -66,13 +66,19 @@ func addAccount(client *Client) {
|
|||||||
var account config.WhatsAppConfig
|
var account config.WhatsAppConfig
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
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.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.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)
|
resp, err := client.Post("/api/accounts/add", account)
|
||||||
checkError(err)
|
checkError(err)
|
||||||
|
|||||||
@@ -95,16 +95,25 @@ func addHook(client *Client) {
|
|||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
fmt.Print("Hook ID: ")
|
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.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.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.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 == "" {
|
if hook.Method == "" {
|
||||||
hook.Method = "POST"
|
hook.Method = "POST"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,10 +70,14 @@ func sendMessage(client *Client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
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.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: ")
|
fmt.Print("Message text: ")
|
||||||
reader := os.Stdin
|
reader := os.Stdin
|
||||||
@@ -101,10 +105,14 @@ func sendImage(client *Client, filePath string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
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.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): ")
|
fmt.Print("Caption (optional): ")
|
||||||
reader := os.Stdin
|
reader := os.Stdin
|
||||||
@@ -151,10 +159,14 @@ func sendVideo(client *Client, filePath string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
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.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): ")
|
fmt.Print("Caption (optional): ")
|
||||||
reader := os.Stdin
|
reader := os.Stdin
|
||||||
@@ -204,10 +216,14 @@ func sendDocument(client *Client, filePath string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
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.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): ")
|
fmt.Print("Caption (optional): ")
|
||||||
reader := os.Stdin
|
reader := os.Stdin
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ type ServerConfig struct {
|
|||||||
|
|
||||||
// WhatsAppConfig holds configuration for a WhatsApp account
|
// WhatsAppConfig holds configuration for a WhatsApp account
|
||||||
type WhatsAppConfig struct {
|
type WhatsAppConfig struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"` // "whatsmeow" or "business-api"
|
Type string `json:"type"` // "whatsmeow" or "business-api"
|
||||||
PhoneNumber string `json:"phone_number"`
|
PhoneNumber string `json:"phone_number"`
|
||||||
SessionPath string `json:"session_path,omitempty"`
|
SessionPath string `json:"session_path,omitempty"`
|
||||||
ShowQR bool `json:"show_qr,omitempty"`
|
ShowQR bool `json:"show_qr,omitempty"`
|
||||||
BusinessAPI *BusinessAPIConfig `json:"business_api,omitempty"`
|
BusinessAPI *BusinessAPIConfig `json:"business_api,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BusinessAPIConfig holds configuration for WhatsApp Business API
|
// BusinessAPIConfig holds configuration for WhatsApp Business API
|
||||||
@@ -72,7 +72,7 @@ type DatabaseConfig struct {
|
|||||||
// MediaConfig holds media storage and delivery configuration
|
// MediaConfig holds media storage and delivery configuration
|
||||||
type MediaConfig struct {
|
type MediaConfig struct {
|
||||||
DataPath string `json:"data_path"`
|
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
|
BaseURL string `json:"base_url,omitempty"` // Base URL for media links
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ const (
|
|||||||
|
|
||||||
// Event represents an event in the system
|
// Event represents an event in the system
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Type EventType `json:"type"`
|
Type EventType `json:"type"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
Data map[string]any `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
Context context.Context `json:"-"`
|
Context context.Context `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscriber is a function that handles events
|
// Subscriber is a function that handles events
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
// Accounts returns the list of all configured WhatsApp accounts
|
// Accounts returns the list of all configured WhatsApp accounts
|
||||||
func (h *Handlers) Accounts(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) Accounts(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
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
|
// 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)
|
w.WriteHeader(http.StatusCreated)
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
writeJSON(w, map[string]string{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func (h *Handlers) businessAPIWebhookVerify(w http.ResponseWriter, r *http.Reque
|
|||||||
if mode == "subscribe" && token == accountConfig.VerifyToken {
|
if mode == "subscribe" && token == accountConfig.VerifyToken {
|
||||||
logging.Info("Webhook verification successful", "account_id", accountID)
|
logging.Info("Webhook verification successful", "account_id", accountID)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(challenge))
|
writeBytes(w, []byte(challenge))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ func (h *Handlers) businessAPIWebhookEvent(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
// Return 200 OK to acknowledge receipt
|
// Return 200 OK to acknowledge receipt
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("OK"))
|
writeBytes(w, []byte("OK"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractAccountIDFromPath extracts the account ID from the URL path
|
// extractAccountIDFromPath extracts the account ID from the URL path
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/hooks"
|
"git.warky.dev/wdevs/whatshooked/pkg/hooks"
|
||||||
|
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
||||||
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
|
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,3 +56,17 @@ func (h *Handlers) WithAuthConfig(cfg *AuthConfig) *Handlers {
|
|||||||
h.authConfig = cfg
|
h.authConfig = cfg
|
||||||
return h
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Health handles health check requests
|
// Health handles health check requests
|
||||||
func (h *Handlers) Health(w http.ResponseWriter, r *http.Request) {
|
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"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func (h *Handlers) Hooks(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) Hooks(w http.ResponseWriter, r *http.Request) {
|
||||||
hooks := h.hookMgr.ListHooks()
|
hooks := h.hookMgr.ListHooks()
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(hooks)
|
writeJSON(w, hooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddHook adds a new hook to the system
|
// 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)
|
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
|
// 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"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func (h *Handlers) SendMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
writeJSON(w, map[string]string{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendImage sends an image via WhatsApp
|
// SendImage sends an image via WhatsApp
|
||||||
@@ -87,7 +87,7 @@ func (h *Handlers) SendImage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
writeJSON(w, map[string]string{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendVideo sends a video via WhatsApp
|
// SendVideo sends a video via WhatsApp
|
||||||
@@ -134,7 +134,7 @@ func (h *Handlers) SendVideo(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
writeJSON(w, map[string]string{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendDocument sends a document via WhatsApp
|
// SendDocument sends a document via WhatsApp
|
||||||
@@ -185,5 +185,5 @@ func (h *Handlers) SendDocument(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
writeJSON(w, map[string]string{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,15 +340,3 @@ func jidToPhoneNumber(jid types.JID) string {
|
|||||||
|
|
||||||
return phone
|
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ func (c *Client) HandleWebhook(r *http.Request) error {
|
|||||||
|
|
||||||
// Process each entry
|
// Process each entry
|
||||||
for _, entry := range payload.Entry {
|
for _, entry := range payload.Entry {
|
||||||
for _, change := range entry.Changes {
|
for i := range entry.Changes {
|
||||||
c.processChange(change)
|
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
|
// 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
|
mode := c.mediaConfig.Mode
|
||||||
var filename string
|
|
||||||
var mediaURL string
|
|
||||||
|
|
||||||
// Generate filename
|
// Generate filename
|
||||||
ext := getExtensionFromMimeType(mimeType)
|
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
|
// getExtensionFromMimeType returns the file extension for a given MIME type
|
||||||
func getExtensionFromMimeType(mimeType string) string {
|
func getExtensionFromMimeType(mimeType string) string {
|
||||||
extensions := map[string]string{
|
extensions := map[string]string{
|
||||||
"image/jpeg": ".jpg",
|
"image/jpeg": ".jpg",
|
||||||
"image/png": ".png",
|
"image/png": ".png",
|
||||||
"image/gif": ".gif",
|
"image/gif": ".gif",
|
||||||
"image/webp": ".webp",
|
"image/webp": ".webp",
|
||||||
"video/mp4": ".mp4",
|
"video/mp4": ".mp4",
|
||||||
"video/mpeg": ".mpeg",
|
"video/mpeg": ".mpeg",
|
||||||
"video/webm": ".webm",
|
"video/webm": ".webm",
|
||||||
"video/3gpp": ".3gp",
|
"video/3gpp": ".3gp",
|
||||||
"application/pdf": ".pdf",
|
"application/pdf": ".pdf",
|
||||||
"application/msword": ".doc",
|
"application/msword": ".doc",
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||||||
"application/vnd.ms-excel": ".xls",
|
"application/vnd.ms-excel": ".xls",
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||||||
"text/plain": ".txt",
|
"text/plain": ".txt",
|
||||||
"application/json": ".json",
|
"application/json": ".json",
|
||||||
"audio/mpeg": ".mp3",
|
"audio/mpeg": ".mp3",
|
||||||
"audio/ogg": ".ogg",
|
"audio/ogg": ".ogg",
|
||||||
}
|
}
|
||||||
|
|
||||||
if ext, ok := extensions[mimeType]; ok {
|
if ext, ok := extensions[mimeType]; ok {
|
||||||
|
|||||||
@@ -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
|
// 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
|
// Step 1: Get the media URL
|
||||||
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
|
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
|
||||||
c.config.APIVersion,
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to read media data: %w", err)
|
return nil, "", fmt.Errorf("failed to read media data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, mediaResp.MimeType, nil
|
mimeType = mediaResp.MimeType
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package businessapi
|
|||||||
|
|
||||||
// SendMessageRequest represents a request to send a text message via Business API
|
// SendMessageRequest represents a request to send a text message via Business API
|
||||||
type SendMessageRequest struct {
|
type SendMessageRequest struct {
|
||||||
MessagingProduct string `json:"messaging_product"` // Always "whatsapp"
|
MessagingProduct string `json:"messaging_product"` // Always "whatsapp"
|
||||||
RecipientType string `json:"recipient_type,omitempty"` // "individual"
|
RecipientType string `json:"recipient_type,omitempty"` // "individual"
|
||||||
To string `json:"to"` // Phone number in E.164 format
|
To string `json:"to"` // Phone number in E.164 format
|
||||||
Type string `json:"type"` // "text", "image", "video", "document"
|
Type string `json:"type"` // "text", "image", "video", "document"
|
||||||
Text *TextObject `json:"text,omitempty"`
|
Text *TextObject `json:"text,omitempty"`
|
||||||
Image *MediaObject `json:"image,omitempty"`
|
Image *MediaObject `json:"image,omitempty"`
|
||||||
Video *MediaObject `json:"video,omitempty"`
|
Video *MediaObject `json:"video,omitempty"`
|
||||||
Document *DocumentObject `json:"document,omitempty"`
|
Document *DocumentObject `json:"document,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,14 +19,14 @@ type TextObject struct {
|
|||||||
|
|
||||||
// MediaObject represents media (image/video) message
|
// MediaObject represents media (image/video) message
|
||||||
type MediaObject struct {
|
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
|
Link string `json:"link,omitempty"` // Or direct URL
|
||||||
Caption string `json:"caption,omitempty"`
|
Caption string `json:"caption,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentObject represents a document message
|
// DocumentObject represents a document message
|
||||||
type DocumentObject struct {
|
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
|
Link string `json:"link,omitempty"` // Or direct URL
|
||||||
Caption string `json:"caption,omitempty"`
|
Caption string `json:"caption,omitempty"`
|
||||||
Filename string `json:"filename,omitempty"`
|
Filename string `json:"filename,omitempty"`
|
||||||
@@ -51,11 +51,11 @@ type MediaUploadResponse struct {
|
|||||||
|
|
||||||
// MediaURLResponse represents the response when getting media URL
|
// MediaURLResponse represents the response when getting media URL
|
||||||
type MediaURLResponse struct {
|
type MediaURLResponse struct {
|
||||||
URL string `json:"url"` // CDN URL to download media
|
URL string `json:"url"` // CDN URL to download media
|
||||||
MimeType string `json:"mime_type"`
|
MimeType string `json:"mime_type"`
|
||||||
SHA256 string `json:"sha256"`
|
SHA256 string `json:"sha256"`
|
||||||
FileSize int64 `json:"file_size"`
|
FileSize int64 `json:"file_size"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
MessagingProduct string `json:"messaging_product"`
|
MessagingProduct string `json:"messaging_product"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,11 +90,11 @@ type WebhookChange struct {
|
|||||||
|
|
||||||
// WebhookValue contains the actual webhook data
|
// WebhookValue contains the actual webhook data
|
||||||
type WebhookValue struct {
|
type WebhookValue struct {
|
||||||
MessagingProduct string `json:"messaging_product"`
|
MessagingProduct string `json:"messaging_product"`
|
||||||
Metadata WebhookMetadata `json:"metadata"`
|
Metadata WebhookMetadata `json:"metadata"`
|
||||||
Contacts []WebhookContact `json:"contacts,omitempty"`
|
Contacts []WebhookContact `json:"contacts,omitempty"`
|
||||||
Messages []WebhookMessage `json:"messages,omitempty"`
|
Messages []WebhookMessage `json:"messages,omitempty"`
|
||||||
Statuses []WebhookStatus `json:"statuses,omitempty"`
|
Statuses []WebhookStatus `json:"statuses,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebhookMetadata contains metadata about the phone number
|
// WebhookMetadata contains metadata about the phone number
|
||||||
@@ -116,15 +116,15 @@ type WebhookProfile struct {
|
|||||||
|
|
||||||
// WebhookMessage represents a message in the webhook
|
// WebhookMessage represents a message in the webhook
|
||||||
type WebhookMessage struct {
|
type WebhookMessage struct {
|
||||||
From string `json:"from"` // Sender phone number
|
From string `json:"from"` // Sender phone number
|
||||||
ID string `json:"id"` // Message ID
|
ID string `json:"id"` // Message ID
|
||||||
Timestamp string `json:"timestamp"` // Unix timestamp as string
|
Timestamp string `json:"timestamp"` // Unix timestamp as string
|
||||||
Type string `json:"type"` // "text", "image", "video", "document", etc.
|
Type string `json:"type"` // "text", "image", "video", "document", etc.
|
||||||
Text *WebhookText `json:"text,omitempty"`
|
Text *WebhookText `json:"text,omitempty"`
|
||||||
Image *WebhookMediaMessage `json:"image,omitempty"`
|
Image *WebhookMediaMessage `json:"image,omitempty"`
|
||||||
Video *WebhookMediaMessage `json:"video,omitempty"`
|
Video *WebhookMediaMessage `json:"video,omitempty"`
|
||||||
Document *WebhookDocumentMessage `json:"document,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
|
// WebhookText represents a text message
|
||||||
@@ -158,20 +158,20 @@ type WebhookContext struct {
|
|||||||
|
|
||||||
// WebhookStatus represents a message status update
|
// WebhookStatus represents a message status update
|
||||||
type WebhookStatus struct {
|
type WebhookStatus struct {
|
||||||
ID string `json:"id"` // Message ID
|
ID string `json:"id"` // Message ID
|
||||||
Status string `json:"status"` // "sent", "delivered", "read", "failed"
|
Status string `json:"status"` // "sent", "delivered", "read", "failed"
|
||||||
Timestamp string `json:"timestamp"` // Unix timestamp as string
|
Timestamp string `json:"timestamp"` // Unix timestamp as string
|
||||||
RecipientID string `json:"recipient_id"`
|
RecipientID string `json:"recipient_id"`
|
||||||
Conversation *WebhookConversation `json:"conversation,omitempty"`
|
Conversation *WebhookConversation `json:"conversation,omitempty"`
|
||||||
Pricing *WebhookPricing `json:"pricing,omitempty"`
|
Pricing *WebhookPricing `json:"pricing,omitempty"`
|
||||||
Errors []WebhookError `json:"errors,omitempty"`
|
Errors []WebhookError `json:"errors,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebhookConversation contains conversation details
|
// WebhookConversation contains conversation details
|
||||||
type WebhookConversation struct {
|
type WebhookConversation struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
ExpirationTimestamp string `json:"expiration_timestamp,omitempty"`
|
ExpirationTimestamp string `json:"expiration_timestamp,omitempty"`
|
||||||
Origin WebhookOrigin `json:"origin"`
|
Origin WebhookOrigin `json:"origin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebhookOrigin contains conversation origin
|
// WebhookOrigin contains conversation origin
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ func (c *Client) handleEvent(evt interface{}) {
|
|||||||
|
|
||||||
// Extract message content based on type
|
// Extract message content based on type
|
||||||
var text string
|
var text string
|
||||||
var messageType string = "text"
|
var messageType = "text"
|
||||||
var mimeType string
|
var mimeType string
|
||||||
var filename string
|
var filename string
|
||||||
var mediaBase64 string
|
var mediaBase64 string
|
||||||
@@ -516,12 +516,13 @@ func (c *Client) handleEvent(evt interface{}) {
|
|||||||
|
|
||||||
case *waEvents.Receipt:
|
case *waEvents.Receipt:
|
||||||
// Handle delivery and read receipts
|
// Handle delivery and read receipts
|
||||||
if v.Type == types.ReceiptTypeDelivered {
|
switch v.Type {
|
||||||
|
case types.ReceiptTypeDelivered:
|
||||||
for _, messageID := range v.MessageIDs {
|
for _, messageID := range v.MessageIDs {
|
||||||
logging.Debug("Message delivered", "account_id", c.id, "message_id", messageID, "from", v.Sender.String())
|
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))
|
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 {
|
for _, messageID := range v.MessageIDs {
|
||||||
logging.Debug("Message read", "account_id", c.id, "message_id", messageID, "from", v.Sender.String())
|
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))
|
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
|
// 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
|
mode := c.mediaConfig.Mode
|
||||||
var filename string
|
|
||||||
var mediaURL string
|
|
||||||
|
|
||||||
// Generate filename
|
// Generate filename
|
||||||
ext := getExtensionFromMimeType(mimeType)
|
ext := getExtensionFromMimeType(mimeType)
|
||||||
|
|||||||
Reference in New Issue
Block a user