Compare commits
2 Commits
75cff3699f
...
16aaf1919d
| Author | SHA1 | Date | |
|---|---|---|---|
| 16aaf1919d | |||
| d54b0eaddf |
46
.dockerignore
Normal file
46
.dockerignore
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Git files
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
PLAN.md
|
||||||
|
TODO.md
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
*_test.go
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Data directories
|
||||||
|
sessions/
|
||||||
|
data/
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
|
||||||
|
# Config files (will be mounted as volumes)
|
||||||
|
config.json
|
||||||
|
*.example.json
|
||||||
|
.whatshooked-cli.example.json
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
assets/
|
||||||
|
|
||||||
|
# Claude
|
||||||
|
.claude/
|
||||||
|
CLAUDE.md
|
||||||
|
AI_USE.md
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"server_url": "http://localhost:8080"
|
"server_url": "http://localhost:8080",
|
||||||
|
"auth_key": "",
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
}
|
}
|
||||||
|
|||||||
466
CLI.md
Normal file
466
CLI.md
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
# WhatsHooked CLI Documentation
|
||||||
|
|
||||||
|
The WhatsHooked CLI provides a command-line interface for managing the WhatsHooked server, webhooks, and WhatsApp accounts.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Authentication](#authentication)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Commands](#commands)
|
||||||
|
- [Examples](#examples)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Build the CLI using the provided Makefile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary will be available at `./bin/whatshook-cli`.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
The CLI supports multiple authentication methods to secure communication with the WhatsHooked server.
|
||||||
|
|
||||||
|
### Authentication Methods
|
||||||
|
|
||||||
|
The CLI supports two authentication methods (in priority order):
|
||||||
|
|
||||||
|
1. **API Key Authentication** (Recommended)
|
||||||
|
- Uses Bearer token or x-api-key header
|
||||||
|
- Most secure and simple to use
|
||||||
|
|
||||||
|
2. **Basic Authentication**
|
||||||
|
- Uses username and password
|
||||||
|
- HTTP Basic Auth
|
||||||
|
|
||||||
|
### Configuration Priority
|
||||||
|
|
||||||
|
Authentication credentials are loaded in the following priority order (highest to lowest):
|
||||||
|
|
||||||
|
1. **Command-line flags** (highest priority)
|
||||||
|
2. **Environment variables**
|
||||||
|
3. **Configuration file** (lowest priority)
|
||||||
|
|
||||||
|
### Setting Up Authentication
|
||||||
|
|
||||||
|
#### Option 1: Configuration File (Recommended)
|
||||||
|
|
||||||
|
Create a configuration file at `~/.whatshooked/cli.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_url": "http://localhost:8080",
|
||||||
|
"auth_key": "your-api-key-here",
|
||||||
|
"username": "",
|
||||||
|
"password": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use API key authentication:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_url": "http://localhost:8080",
|
||||||
|
"auth_key": "your-secure-api-key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use username/password authentication:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_url": "http://localhost:8080",
|
||||||
|
"username": "admin",
|
||||||
|
"password": "your-secure-password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also create a local configuration file in your project directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .whatshooked-cli.example.json .whatshooked-cli.json
|
||||||
|
# Edit the file with your credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 2: Environment Variables
|
||||||
|
|
||||||
|
Set environment variables (useful for CI/CD):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export WHATSHOOKED_SERVER_URL="http://localhost:8080"
|
||||||
|
export WHATSHOOKED_AUTH_KEY="your-api-key-here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with username/password:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export WHATSHOOKED_SERVER_URL="http://localhost:8080"
|
||||||
|
export WHATSHOOKED_USERNAME="admin"
|
||||||
|
export WHATSHOOKED_PASSWORD="your-password"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 3: Command-Line Flags
|
||||||
|
|
||||||
|
Pass credentials via command-line flags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli --server http://localhost:8080 health
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Currently, authentication credentials can only be set via config file or environment variables. Command-line flags for auth credentials may be added in future versions.
|
||||||
|
|
||||||
|
### No Authentication
|
||||||
|
|
||||||
|
If your server doesn't require authentication, simply omit the authentication fields:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server_url": "http://localhost:8080"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Configuration File Locations
|
||||||
|
|
||||||
|
The CLI looks for configuration files in the following locations (in order):
|
||||||
|
|
||||||
|
1. File specified by `--config` flag
|
||||||
|
2. `$HOME/.whatshooked/cli.json`
|
||||||
|
3. `./.whatshooked-cli.json` (current directory)
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `server_url` | string | `http://localhost:8080` | WhatsHooked server URL |
|
||||||
|
| `auth_key` | string | `""` | API key for authentication |
|
||||||
|
| `username` | string | `""` | Username for Basic Auth |
|
||||||
|
| `password` | string | `""` | Password for Basic Auth |
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Global Flags
|
||||||
|
|
||||||
|
All commands support the following global flags:
|
||||||
|
|
||||||
|
- `--config <path>`: Path to configuration file
|
||||||
|
- `--server <url>`: Server URL (overrides config file)
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
Check if the server is running and healthy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hooks Management
|
||||||
|
|
||||||
|
#### List Hooks
|
||||||
|
|
||||||
|
List all configured webhooks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli hooks
|
||||||
|
./bin/whatshook-cli hooks list
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add Hook
|
||||||
|
|
||||||
|
Add a new webhook interactively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli hooks add
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll be prompted for:
|
||||||
|
- Hook ID
|
||||||
|
- Hook Name
|
||||||
|
- Webhook URL
|
||||||
|
- HTTP Method (default: POST)
|
||||||
|
- Events to subscribe to (optional, comma-separated)
|
||||||
|
- Description (optional)
|
||||||
|
|
||||||
|
**Available Event Types:**
|
||||||
|
|
||||||
|
WhatsApp Connection Events:
|
||||||
|
- `whatsapp.connected` - WhatsApp client connected
|
||||||
|
- `whatsapp.disconnected` - WhatsApp client disconnected
|
||||||
|
- `whatsapp.qr.code` - QR code generated for pairing
|
||||||
|
- `whatsapp.qr.timeout` - QR code expired
|
||||||
|
- `whatsapp.qr.error` - QR code generation error
|
||||||
|
- `whatsapp.pair.success` - Device paired successfully
|
||||||
|
- `whatsapp.pair.failed` - Device pairing failed
|
||||||
|
- `whatsapp.pair.event` - Generic pairing event
|
||||||
|
|
||||||
|
Message Events:
|
||||||
|
- `message.received` - Message received from WhatsApp
|
||||||
|
- `message.sent` - Message sent successfully
|
||||||
|
- `message.failed` - Message sending failed
|
||||||
|
- `message.delivered` - Message delivered to recipient
|
||||||
|
- `message.read` - Message read by recipient
|
||||||
|
|
||||||
|
Hook Events:
|
||||||
|
- `hook.triggered` - Hook was triggered
|
||||||
|
- `hook.success` - Hook executed successfully
|
||||||
|
- `hook.failed` - Hook execution failed
|
||||||
|
|
||||||
|
If no events are specified, the hook will receive all events.
|
||||||
|
|
||||||
|
#### Remove Hook
|
||||||
|
|
||||||
|
Remove a webhook by ID:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli hooks remove <hook_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accounts Management
|
||||||
|
|
||||||
|
#### List Accounts
|
||||||
|
|
||||||
|
List all configured WhatsApp accounts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli accounts
|
||||||
|
./bin/whatshook-cli accounts list
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add Account
|
||||||
|
|
||||||
|
Add a new WhatsApp account interactively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli accounts add
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll be prompted for:
|
||||||
|
- Account ID
|
||||||
|
- Phone Number (with country code, e.g., +1234567890)
|
||||||
|
- Session Path (where to store session data)
|
||||||
|
|
||||||
|
After adding, check the server logs for a QR code to scan with WhatsApp.
|
||||||
|
|
||||||
|
### Send Messages
|
||||||
|
|
||||||
|
#### Send Text Message
|
||||||
|
|
||||||
|
Send a text message interactively:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli send text
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll be prompted for:
|
||||||
|
- Account ID
|
||||||
|
- Recipient (phone number or JID)
|
||||||
|
- Message text
|
||||||
|
|
||||||
|
#### Send Image
|
||||||
|
|
||||||
|
Send an image with optional caption:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli send image <file_path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported formats: JPG, PNG, GIF, WebP
|
||||||
|
|
||||||
|
#### Send Video
|
||||||
|
|
||||||
|
Send a video with optional caption:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli send video <file_path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported formats: MP4, MOV, AVI, WebM, 3GP
|
||||||
|
|
||||||
|
#### Send Document
|
||||||
|
|
||||||
|
Send a document with optional caption:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli send document <file_path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported formats: PDF, DOC, DOCX, XLS, XLSX, TXT, ZIP, and more
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Basic Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create config file
|
||||||
|
mkdir -p ~/.whatshooked
|
||||||
|
cat > ~/.whatshooked/cli.json <<EOF
|
||||||
|
{
|
||||||
|
"server_url": "http://localhost:8080",
|
||||||
|
"auth_key": "my-secure-api-key"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Check server health
|
||||||
|
./bin/whatshook-cli health
|
||||||
|
|
||||||
|
# List hooks
|
||||||
|
./bin/whatshook-cli hooks list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Using Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set environment variables
|
||||||
|
export WHATSHOOKED_SERVER_URL="https://my-server.com:8080"
|
||||||
|
export WHATSHOOKED_AUTH_KEY="production-api-key"
|
||||||
|
|
||||||
|
# Use CLI without config file
|
||||||
|
./bin/whatshook-cli health
|
||||||
|
./bin/whatshook-cli accounts list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Managing Hooks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add a new hook for all events
|
||||||
|
./bin/whatshook-cli hooks add
|
||||||
|
# Hook ID: my_hook
|
||||||
|
# Hook Name: My Webhook
|
||||||
|
# Webhook URL: https://example.com/webhook
|
||||||
|
# HTTP Method (POST): POST
|
||||||
|
# Events (comma-separated, or press Enter for all): [press Enter]
|
||||||
|
# Description (optional): My webhook handler
|
||||||
|
|
||||||
|
# Add a hook for specific events only
|
||||||
|
./bin/whatshook-cli hooks add
|
||||||
|
# Hook ID: message_hook
|
||||||
|
# Hook Name: Message Handler
|
||||||
|
# Webhook URL: https://example.com/messages
|
||||||
|
# HTTP Method (POST): POST
|
||||||
|
# Events (comma-separated, or press Enter for all): message.received, message.sent
|
||||||
|
# Description (optional): Handle incoming and outgoing messages
|
||||||
|
|
||||||
|
# List all hooks
|
||||||
|
./bin/whatshook-cli hooks
|
||||||
|
|
||||||
|
# Remove a hook
|
||||||
|
./bin/whatshook-cli hooks remove message_hook
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Sending Messages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send text message
|
||||||
|
./bin/whatshook-cli send text
|
||||||
|
# Enter account ID, recipient, and message when prompted
|
||||||
|
|
||||||
|
# Send image
|
||||||
|
./bin/whatshook-cli send image /path/to/photo.jpg
|
||||||
|
|
||||||
|
# Send video
|
||||||
|
./bin/whatshook-cli send video /path/to/video.mp4
|
||||||
|
|
||||||
|
# Send document
|
||||||
|
./bin/whatshook-cli send document /path/to/report.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 5: Production Setup with Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server config.json (enable authentication)
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8080,
|
||||||
|
"auth_key": "super-secret-production-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# CLI config (~/.whatshooked/cli.json)
|
||||||
|
{
|
||||||
|
"server_url": "https://whatshooked.mycompany.com",
|
||||||
|
"auth_key": "super-secret-production-key"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Now all CLI commands will be authenticated
|
||||||
|
./bin/whatshook-cli hooks list
|
||||||
|
./bin/whatshook-cli accounts add
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The CLI provides clear error messages for common issues:
|
||||||
|
|
||||||
|
### Authentication Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: HTTP 401: Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Check your authentication credentials in the config file or environment variables.
|
||||||
|
|
||||||
|
### Connection Errors
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: dial tcp: connect: connection refused
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Ensure the server is running and the URL is correct.
|
||||||
|
|
||||||
|
### Invalid Credentials
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: HTTP 403: Forbidden
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Verify your API key or username/password are correct and match the server configuration.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use API Key Authentication**: More secure than username/password, easier to rotate
|
||||||
|
2. **Store Config Securely**: Don't commit config files with credentials to version control
|
||||||
|
3. **Use Environment Variables in CI/CD**: Safer than storing credentials in files
|
||||||
|
4. **Enable Authentication in Production**: Always use authentication for production servers
|
||||||
|
5. **Use HTTPS**: In production, always use HTTPS for the server URL
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Never commit configuration files containing credentials to version control
|
||||||
|
- Use restrictive file permissions for config files: `chmod 600 ~/.whatshooked/cli.json`
|
||||||
|
- Rotate API keys regularly
|
||||||
|
- Use different credentials for development and production
|
||||||
|
- In production, always use HTTPS to encrypt traffic
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Config File Not Found
|
||||||
|
|
||||||
|
If you see warnings about config file not being found, create one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.whatshooked
|
||||||
|
cp .whatshooked-cli.example.json ~/.whatshooked/cli.json
|
||||||
|
# Edit the file with your settings
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Unreachable
|
||||||
|
|
||||||
|
Verify the server is running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Required
|
||||||
|
|
||||||
|
If the server requires authentication but you haven't configured it:
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: HTTP 401: Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Add authentication to your config file as shown in the [Authentication](#authentication) section.
|
||||||
326
DOCKER.md
Normal file
326
DOCKER.md
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
# Docker Deployment Guide
|
||||||
|
|
||||||
|
This guide explains how to run WhatsHooked using Docker and Docker Compose.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed (version 20.10 or later)
|
||||||
|
- Docker Compose installed (version 1.29 or later)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. **Copy the example configuration file:**
|
||||||
|
```bash
|
||||||
|
cp config.example.json config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Edit the configuration file:**
|
||||||
|
Open `config.json` and update:
|
||||||
|
- WhatsApp phone numbers
|
||||||
|
- Webhook URLs and authentication
|
||||||
|
- Server settings (port, authentication)
|
||||||
|
|
||||||
|
3. **Create required directories:**
|
||||||
|
```bash
|
||||||
|
mkdir -p sessions data/media
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Build and start the server:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **View logs to scan QR code (first run):**
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f whatshooked
|
||||||
|
```
|
||||||
|
Scan the QR code with WhatsApp on your phone to authenticate.
|
||||||
|
|
||||||
|
6. **Check server health:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Commands
|
||||||
|
|
||||||
|
### Build the image
|
||||||
|
```bash
|
||||||
|
docker-compose build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start the service
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop the service
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart the service
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access the container shell
|
||||||
|
```bash
|
||||||
|
docker-compose exec whatshooked sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Volume Mounts
|
||||||
|
|
||||||
|
The docker-compose.yml file mounts three important volumes:
|
||||||
|
|
||||||
|
1. **config.json** - Server configuration (read-only)
|
||||||
|
2. **sessions/** - WhatsApp session data (persistent authentication)
|
||||||
|
3. **data/media/** - Downloaded media files
|
||||||
|
|
||||||
|
These volumes ensure your data persists across container restarts.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Port Mapping
|
||||||
|
|
||||||
|
By default, the server runs on port 8080. To change the port:
|
||||||
|
|
||||||
|
**Option 1: Update config.json**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"port": 9090
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Then update docker-compose.yml:
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Map to different host port**
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "3000:8080" # Access via localhost:3000, server still runs on 8080 internally
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
Set authentication in config.json:
|
||||||
|
|
||||||
|
**Option 1: API Key**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"auth_key": "your-secure-api-key-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Username/Password**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"username": "admin",
|
||||||
|
"password": "secure-password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resource Limits
|
||||||
|
|
||||||
|
Uncomment the deploy section in docker-compose.yml to set resource limits:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1.0'
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 128M
|
||||||
|
```
|
||||||
|
|
||||||
|
## QR Code Scanning
|
||||||
|
|
||||||
|
When you first start the server, you'll need to scan a QR code to authenticate with WhatsApp.
|
||||||
|
|
||||||
|
### View QR Code in Logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f whatshooked
|
||||||
|
```
|
||||||
|
|
||||||
|
The QR code will be displayed in ASCII art in the terminal. Scan it with WhatsApp on your phone:
|
||||||
|
1. Open WhatsApp
|
||||||
|
2. Go to Settings > Linked Devices
|
||||||
|
3. Tap "Link a Device"
|
||||||
|
4. Scan the QR code from the terminal
|
||||||
|
|
||||||
|
### Alternative: Use CLI Tool
|
||||||
|
|
||||||
|
You can also use the CLI tool outside Docker to link accounts, then mount the session:
|
||||||
|
```bash
|
||||||
|
./bin/whatshook-cli accounts add
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container won't start
|
||||||
|
Check logs for errors:
|
||||||
|
```bash
|
||||||
|
docker-compose logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config file not found
|
||||||
|
Ensure config.json exists in the project root:
|
||||||
|
```bash
|
||||||
|
ls -la config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission issues with volumes
|
||||||
|
Fix ownership of mounted directories:
|
||||||
|
```bash
|
||||||
|
sudo chown -R $(id -u):$(id -g) sessions data
|
||||||
|
```
|
||||||
|
|
||||||
|
### QR code not displaying
|
||||||
|
Ensure `show_qr: true` is set in config.json:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"whatsapp": [
|
||||||
|
{
|
||||||
|
"show_qr": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cannot connect to server
|
||||||
|
Check if the container is running:
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Check health status:
|
||||||
|
```bash
|
||||||
|
docker inspect whatshooked-server | grep -A 10 Health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
|
||||||
|
1. **Use secrets for sensitive data:**
|
||||||
|
- Don't commit config.json with real credentials
|
||||||
|
- Use Docker secrets or environment variables
|
||||||
|
- Enable authentication (auth_key or username/password)
|
||||||
|
|
||||||
|
2. **Use HTTPS:**
|
||||||
|
- Put the server behind a reverse proxy (nginx, Traefik, Caddy)
|
||||||
|
- Enable SSL/TLS certificates
|
||||||
|
- Update webhook base_url to use https://
|
||||||
|
|
||||||
|
3. **Network isolation:**
|
||||||
|
- Use Docker networks to isolate the service
|
||||||
|
- Only expose necessary ports
|
||||||
|
- Consider using a VPN for webhook endpoints
|
||||||
|
|
||||||
|
4. **Regular backups:**
|
||||||
|
- Backup the sessions/ directory regularly
|
||||||
|
- Backup config.json (securely)
|
||||||
|
- Backup media files if needed
|
||||||
|
|
||||||
|
### Example with Reverse Proxy (nginx)
|
||||||
|
|
||||||
|
Create a network:
|
||||||
|
```bash
|
||||||
|
docker network create whatshooked-net
|
||||||
|
```
|
||||||
|
|
||||||
|
Update docker-compose.yml:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
whatshooked:
|
||||||
|
networks:
|
||||||
|
- whatshooked-net
|
||||||
|
# Don't expose ports to host, only to network
|
||||||
|
# ports:
|
||||||
|
# - "8080:8080"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
whatshooked-net:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure nginx to proxy to the container:
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name whatshooked.yourdomain.com;
|
||||||
|
|
||||||
|
ssl_certificate /path/to/cert.pem;
|
||||||
|
ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://whatshooked:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
The compose file includes a health check that runs every 30 seconds:
|
||||||
|
```bash
|
||||||
|
docker inspect --format='{{.State.Health.Status}}' whatshooked-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Management
|
||||||
|
|
||||||
|
Limit log size to prevent disk space issues:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
whatshooked:
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
To update to the latest version:
|
||||||
|
|
||||||
|
1. Pull latest code:
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Rebuild the image:
|
||||||
|
```bash
|
||||||
|
docker-compose build --no-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Restart the service:
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Platform Builds
|
||||||
|
|
||||||
|
To build for different architectures (e.g., ARM for Raspberry Pi):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t whatshooked:latest .
|
||||||
|
```
|
||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
|
# Install build dependencies (SQLite requires CGO)
|
||||||
|
RUN apk add --no-cache gcc musl-dev sqlite-dev
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy go mod files
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the server binary
|
||||||
|
# CGO is required for mattn/go-sqlite3
|
||||||
|
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o whatshooked-server ./cmd/server
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk add --no-cache ca-certificates sqlite-libs tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy binary from builder
|
||||||
|
COPY --from=builder /build/whatshooked-server .
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
RUN mkdir -p /app/sessions /app/data/media
|
||||||
|
|
||||||
|
# Expose the default server port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
ENTRYPOINT ["/app/whatshooked-server"]
|
||||||
|
CMD ["-config", "/app/config.json"]
|
||||||
111
cmd/cli/client.go
Normal file
111
cmd/cli/client.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client wraps the HTTP client with authentication support
|
||||||
|
type Client struct {
|
||||||
|
baseURL string
|
||||||
|
authKey string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new authenticated HTTP client
|
||||||
|
func NewClient(cfg *CLIConfig) *Client {
|
||||||
|
return &Client{
|
||||||
|
baseURL: cfg.ServerURL,
|
||||||
|
authKey: cfg.AuthKey,
|
||||||
|
username: cfg.Username,
|
||||||
|
password: cfg.Password,
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addAuth adds authentication headers to the request
|
||||||
|
func (c *Client) addAuth(req *http.Request) {
|
||||||
|
// Priority 1: API Key (as Bearer token or x-api-key header)
|
||||||
|
if c.authKey != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.authKey)
|
||||||
|
req.Header.Set("x-api-key", c.authKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Basic Auth (username/password)
|
||||||
|
if c.username != "" && c.password != "" {
|
||||||
|
req.SetBasicAuth(c.username, c.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get performs an authenticated GET request
|
||||||
|
func (c *Client) Get(path string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("GET", c.baseURL+path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.addAuth(req)
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for HTTP error status codes
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post performs an authenticated POST request with JSON data
|
||||||
|
func (c *Client) Post(path string, data interface{}) (*http.Response, error) {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", c.baseURL+path, bytes.NewReader(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
c.addAuth(req)
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for HTTP error status codes
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeJSON decodes JSON response into target
|
||||||
|
func decodeJSON(resp *http.Response, target interface{}) error {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return json.NewDecoder(resp.Body).Decode(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkError prints error and exits if error is not nil
|
||||||
|
func checkError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
83
cmd/cli/commands_accounts.go
Normal file
83
cmd/cli/commands_accounts.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// accountsCmd is the parent command for account management
|
||||||
|
var accountsCmd = &cobra.Command{
|
||||||
|
Use: "accounts",
|
||||||
|
Short: "Manage WhatsApp accounts",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
listAccounts(client)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountsListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List all accounts",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
listAccounts(client)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var accountsAddCmd = &cobra.Command{
|
||||||
|
Use: "add",
|
||||||
|
Short: "Add a new WhatsApp account",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
addAccount(client)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
accountsCmd.AddCommand(accountsListCmd)
|
||||||
|
accountsCmd.AddCommand(accountsAddCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listAccounts(client *Client) {
|
||||||
|
resp, err := client.Get("/api/accounts")
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var accounts []config.WhatsAppConfig
|
||||||
|
checkError(decodeJSON(resp, &accounts))
|
||||||
|
|
||||||
|
if len(accounts) == 0 {
|
||||||
|
fmt.Println("No accounts configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Configured accounts (%d):\n\n", len(accounts))
|
||||||
|
for _, acc := range accounts {
|
||||||
|
fmt.Printf("ID: %s\n", acc.ID)
|
||||||
|
fmt.Printf("Phone Number: %s\n", acc.PhoneNumber)
|
||||||
|
fmt.Printf("Session Path: %s\n", acc.SessionPath)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAccount(client *Client) {
|
||||||
|
var account config.WhatsAppConfig
|
||||||
|
|
||||||
|
fmt.Print("Account ID: ")
|
||||||
|
fmt.Scanln(&account.ID)
|
||||||
|
|
||||||
|
fmt.Print("Phone Number (with country code): ")
|
||||||
|
fmt.Scanln(&account.PhoneNumber)
|
||||||
|
|
||||||
|
fmt.Print("Session Path: ")
|
||||||
|
fmt.Scanln(&account.SessionPath)
|
||||||
|
|
||||||
|
resp, err := client.Post("/api/accounts/add", account)
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("Account added successfully")
|
||||||
|
fmt.Println("Check server logs for QR code to pair the device")
|
||||||
|
}
|
||||||
28
cmd/cli/commands_health.go
Normal file
28
cmd/cli/commands_health.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// healthCmd checks server health
|
||||||
|
var healthCmd = &cobra.Command{
|
||||||
|
Use: "health",
|
||||||
|
Short: "Check server health",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
checkHealth(client)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHealth(client *Client) {
|
||||||
|
resp, err := client.Get("/health")
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result map[string]string
|
||||||
|
checkError(decodeJSON(resp, &result))
|
||||||
|
|
||||||
|
fmt.Printf("Server status: %s\n", result["status"])
|
||||||
|
}
|
||||||
155
cmd/cli/commands_hooks.go
Normal file
155
cmd/cli/commands_hooks.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/whatshooked/internal/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// hooksCmd is the parent command for hook management
|
||||||
|
var hooksCmd = &cobra.Command{
|
||||||
|
Use: "hooks",
|
||||||
|
Short: "Manage webhooks",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
listHooks(client)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var hooksListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List all hooks",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
listHooks(client)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var hooksAddCmd = &cobra.Command{
|
||||||
|
Use: "add",
|
||||||
|
Short: "Add a new hook",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
addHook(client)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var hooksRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <hook_id>",
|
||||||
|
Short: "Remove a hook",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
removeHook(client, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
hooksCmd.AddCommand(hooksListCmd)
|
||||||
|
hooksCmd.AddCommand(hooksAddCmd)
|
||||||
|
hooksCmd.AddCommand(hooksRemoveCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listHooks(client *Client) {
|
||||||
|
resp, err := client.Get("/api/hooks")
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var hooks []config.Hook
|
||||||
|
checkError(decodeJSON(resp, &hooks))
|
||||||
|
|
||||||
|
if len(hooks) == 0 {
|
||||||
|
fmt.Println("No hooks configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Configured hooks (%d):\n\n", len(hooks))
|
||||||
|
for _, hook := range hooks {
|
||||||
|
status := "inactive"
|
||||||
|
if hook.Active {
|
||||||
|
status = "active"
|
||||||
|
}
|
||||||
|
fmt.Printf("ID: %s\n", hook.ID)
|
||||||
|
fmt.Printf("Name: %s\n", hook.Name)
|
||||||
|
fmt.Printf("URL: %s\n", hook.URL)
|
||||||
|
fmt.Printf("Method: %s\n", hook.Method)
|
||||||
|
fmt.Printf("Status: %s\n", status)
|
||||||
|
if len(hook.Events) > 0 {
|
||||||
|
fmt.Printf("Events: %v\n", hook.Events)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Events: all (no filter)\n")
|
||||||
|
}
|
||||||
|
if hook.Description != "" {
|
||||||
|
fmt.Printf("Description: %s\n", hook.Description)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addHook(client *Client) {
|
||||||
|
var hook config.Hook
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
|
fmt.Print("Hook ID: ")
|
||||||
|
fmt.Scanln(&hook.ID)
|
||||||
|
|
||||||
|
fmt.Print("Hook Name: ")
|
||||||
|
fmt.Scanln(&hook.Name)
|
||||||
|
|
||||||
|
fmt.Print("Webhook URL: ")
|
||||||
|
fmt.Scanln(&hook.URL)
|
||||||
|
|
||||||
|
fmt.Print("HTTP Method (POST): ")
|
||||||
|
fmt.Scanln(&hook.Method)
|
||||||
|
if hook.Method == "" {
|
||||||
|
hook.Method = "POST"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for events with helpful examples
|
||||||
|
fmt.Println("\nAvailable events:")
|
||||||
|
fmt.Println(" WhatsApp: whatsapp.connected, whatsapp.disconnected, whatsapp.qr.code")
|
||||||
|
fmt.Println(" Messages: message.received, message.sent, message.delivered, message.read")
|
||||||
|
fmt.Println(" Hooks: hook.triggered, hook.success, hook.failed")
|
||||||
|
fmt.Print("\nEvents (comma-separated, or press Enter for all): ")
|
||||||
|
|
||||||
|
scanner.Scan()
|
||||||
|
eventsInput := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
if eventsInput != "" {
|
||||||
|
// Split by comma and trim whitespace
|
||||||
|
eventsList := strings.Split(eventsInput, ",")
|
||||||
|
hook.Events = make([]string, 0, len(eventsList))
|
||||||
|
for _, event := range eventsList {
|
||||||
|
trimmed := strings.TrimSpace(event)
|
||||||
|
if trimmed != "" {
|
||||||
|
hook.Events = append(hook.Events, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("\nDescription (optional): ")
|
||||||
|
scanner.Scan()
|
||||||
|
hook.Description = strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
hook.Active = true
|
||||||
|
|
||||||
|
resp, err := client.Post("/api/hooks/add", hook)
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("Hook added successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeHook(client *Client, id string) {
|
||||||
|
req := map[string]string{"id": id}
|
||||||
|
|
||||||
|
resp, err := client.Post("/api/hooks/remove", req)
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("Hook removed successfully")
|
||||||
|
}
|
||||||
254
cmd/cli/commands_send.go
Normal file
254
cmd/cli/commands_send.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sendCmd is the parent command for sending messages
|
||||||
|
var sendCmd = &cobra.Command{
|
||||||
|
Use: "send",
|
||||||
|
Short: "Send messages",
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendTextCmd = &cobra.Command{
|
||||||
|
Use: "text",
|
||||||
|
Short: "Send a text message",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
sendMessage(client)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendImageCmd = &cobra.Command{
|
||||||
|
Use: "image <file_path>",
|
||||||
|
Short: "Send an image",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
sendImage(client, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendVideoCmd = &cobra.Command{
|
||||||
|
Use: "video <file_path>",
|
||||||
|
Short: "Send a video",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
sendVideo(client, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sendDocumentCmd = &cobra.Command{
|
||||||
|
Use: "document <file_path>",
|
||||||
|
Short: "Send a document",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
client := NewClient(cliConfig)
|
||||||
|
sendDocument(client, args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sendCmd.AddCommand(sendTextCmd)
|
||||||
|
sendCmd.AddCommand(sendImageCmd)
|
||||||
|
sendCmd.AddCommand(sendVideoCmd)
|
||||||
|
sendCmd.AddCommand(sendDocumentCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMessage(client *Client) {
|
||||||
|
var req struct {
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Account ID: ")
|
||||||
|
fmt.Scanln(&req.AccountID)
|
||||||
|
|
||||||
|
fmt.Print("Recipient (phone number or JID, e.g., 0834606792 or 1234567890@s.whatsapp.net): ")
|
||||||
|
fmt.Scanln(&req.To)
|
||||||
|
|
||||||
|
fmt.Print("Message text: ")
|
||||||
|
reader := os.Stdin
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := reader.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
checkError(fmt.Errorf("error reading input: %v", err))
|
||||||
|
}
|
||||||
|
req.Text = string(buf[:n])
|
||||||
|
|
||||||
|
resp, err := client.Post("/api/send", req)
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("Message sent successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendImage(client *Client, filePath string) {
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Account ID: ")
|
||||||
|
fmt.Scanln(&req.AccountID)
|
||||||
|
|
||||||
|
fmt.Print("Recipient (phone number): ")
|
||||||
|
fmt.Scanln(&req.To)
|
||||||
|
|
||||||
|
fmt.Print("Caption (optional): ")
|
||||||
|
reader := os.Stdin
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, _ := reader.Read(buf)
|
||||||
|
req.Caption = strings.TrimSpace(string(buf[:n]))
|
||||||
|
|
||||||
|
// Read image file
|
||||||
|
imageData, err := os.ReadFile(filePath)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Encode to base64
|
||||||
|
req.ImageData = base64.StdEncoding.EncodeToString(imageData)
|
||||||
|
|
||||||
|
// Detect mime type from extension
|
||||||
|
ext := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg":
|
||||||
|
req.MimeType = "image/jpeg"
|
||||||
|
case ".png":
|
||||||
|
req.MimeType = "image/png"
|
||||||
|
case ".gif":
|
||||||
|
req.MimeType = "image/gif"
|
||||||
|
case ".webp":
|
||||||
|
req.MimeType = "image/webp"
|
||||||
|
default:
|
||||||
|
req.MimeType = "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Post("/api/send/image", req)
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("Image sent successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendVideo(client *Client, filePath string) {
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Account ID: ")
|
||||||
|
fmt.Scanln(&req.AccountID)
|
||||||
|
|
||||||
|
fmt.Print("Recipient (phone number): ")
|
||||||
|
fmt.Scanln(&req.To)
|
||||||
|
|
||||||
|
fmt.Print("Caption (optional): ")
|
||||||
|
reader := os.Stdin
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, _ := reader.Read(buf)
|
||||||
|
req.Caption = strings.TrimSpace(string(buf[:n]))
|
||||||
|
|
||||||
|
// Read video file
|
||||||
|
videoData, err := os.ReadFile(filePath)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Encode to base64
|
||||||
|
req.VideoData = base64.StdEncoding.EncodeToString(videoData)
|
||||||
|
|
||||||
|
// Detect mime type from extension
|
||||||
|
ext := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
switch ext {
|
||||||
|
case ".mp4":
|
||||||
|
req.MimeType = "video/mp4"
|
||||||
|
case ".mov":
|
||||||
|
req.MimeType = "video/quicktime"
|
||||||
|
case ".avi":
|
||||||
|
req.MimeType = "video/x-msvideo"
|
||||||
|
case ".webm":
|
||||||
|
req.MimeType = "video/webm"
|
||||||
|
case ".3gp":
|
||||||
|
req.MimeType = "video/3gpp"
|
||||||
|
default:
|
||||||
|
req.MimeType = "video/mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Post("/api/send/video", req)
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("Video sent successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendDocument(client *Client, filePath string) {
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Account ID: ")
|
||||||
|
fmt.Scanln(&req.AccountID)
|
||||||
|
|
||||||
|
fmt.Print("Recipient (phone number): ")
|
||||||
|
fmt.Scanln(&req.To)
|
||||||
|
|
||||||
|
fmt.Print("Caption (optional): ")
|
||||||
|
reader := os.Stdin
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, _ := reader.Read(buf)
|
||||||
|
req.Caption = strings.TrimSpace(string(buf[:n]))
|
||||||
|
|
||||||
|
// Read document file
|
||||||
|
documentData, err := os.ReadFile(filePath)
|
||||||
|
checkError(err)
|
||||||
|
|
||||||
|
// Encode to base64
|
||||||
|
req.DocumentData = base64.StdEncoding.EncodeToString(documentData)
|
||||||
|
|
||||||
|
// Use the original filename
|
||||||
|
req.Filename = filepath.Base(filePath)
|
||||||
|
|
||||||
|
// Detect mime type from extension
|
||||||
|
ext := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
switch ext {
|
||||||
|
case ".pdf":
|
||||||
|
req.MimeType = "application/pdf"
|
||||||
|
case ".doc":
|
||||||
|
req.MimeType = "application/msword"
|
||||||
|
case ".docx":
|
||||||
|
req.MimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
|
case ".xls":
|
||||||
|
req.MimeType = "application/vnd.ms-excel"
|
||||||
|
case ".xlsx":
|
||||||
|
req.MimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
case ".txt":
|
||||||
|
req.MimeType = "text/plain"
|
||||||
|
case ".zip":
|
||||||
|
req.MimeType = "application/zip"
|
||||||
|
default:
|
||||||
|
req.MimeType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Post("/api/send/document", req)
|
||||||
|
checkError(err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Println("Document sent successfully")
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import (
|
|||||||
// CLIConfig holds the CLI configuration
|
// CLIConfig holds the CLI configuration
|
||||||
type CLIConfig struct {
|
type CLIConfig struct {
|
||||||
ServerURL string
|
ServerURL string
|
||||||
|
AuthKey string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadCLIConfig loads configuration with priority: config file → ENV → flag
|
// LoadCLIConfig loads configuration with priority: config file → ENV → flag
|
||||||
@@ -18,6 +21,9 @@ func LoadCLIConfig(configFile string, serverFlag string) (*CLIConfig, error) {
|
|||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
v.SetDefault("server_url", "http://localhost:8080")
|
v.SetDefault("server_url", "http://localhost:8080")
|
||||||
|
v.SetDefault("auth_key", "")
|
||||||
|
v.SetDefault("username", "")
|
||||||
|
v.SetDefault("password", "")
|
||||||
|
|
||||||
// 1. Load from config file (lowest priority)
|
// 1. Load from config file (lowest priority)
|
||||||
if configFile != "" {
|
if configFile != "" {
|
||||||
@@ -50,6 +56,9 @@ func LoadCLIConfig(configFile string, serverFlag string) (*CLIConfig, error) {
|
|||||||
|
|
||||||
cfg := &CLIConfig{
|
cfg := &CLIConfig{
|
||||||
ServerURL: v.GetString("server_url"),
|
ServerURL: v.GetString("server_url"),
|
||||||
|
AuthKey: v.GetString("auth_key"),
|
||||||
|
Username: v.GetString("username"),
|
||||||
|
Password: v.GetString("password"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
|
|||||||
585
cmd/cli/main.go
585
cmd/cli/main.go
@@ -1,17 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.warky.dev/wdevs/whatshooked/internal/config"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,584 +38,9 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: $HOME/.whatshooked/cli.json)")
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: $HOME/.whatshooked/cli.json)")
|
||||||
rootCmd.PersistentFlags().StringVar(&serverURL, "server", "", "server URL (default: http://localhost:8080)")
|
rootCmd.PersistentFlags().StringVar(&serverURL, "server", "", "server URL (default: http://localhost:8080)")
|
||||||
|
|
||||||
|
// Add all command groups
|
||||||
rootCmd.AddCommand(healthCmd)
|
rootCmd.AddCommand(healthCmd)
|
||||||
rootCmd.AddCommand(hooksCmd)
|
rootCmd.AddCommand(hooksCmd)
|
||||||
rootCmd.AddCommand(accountsCmd)
|
rootCmd.AddCommand(accountsCmd)
|
||||||
rootCmd.AddCommand(sendCmd)
|
rootCmd.AddCommand(sendCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health command
|
|
||||||
var healthCmd = &cobra.Command{
|
|
||||||
Use: "health",
|
|
||||||
Short: "Check server health",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
checkHealth(cliConfig.ServerURL)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hooks command group
|
|
||||||
var hooksCmd = &cobra.Command{
|
|
||||||
Use: "hooks",
|
|
||||||
Short: "Manage webhooks",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
listHooks(cliConfig.ServerURL)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var hooksListCmd = &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Short: "List all hooks",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
listHooks(cliConfig.ServerURL)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var hooksAddCmd = &cobra.Command{
|
|
||||||
Use: "add",
|
|
||||||
Short: "Add a new hook",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
addHook(cliConfig.ServerURL)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var hooksRemoveCmd = &cobra.Command{
|
|
||||||
Use: "remove <hook_id>",
|
|
||||||
Short: "Remove a hook",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
removeHook(cliConfig.ServerURL, args[0])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
hooksCmd.AddCommand(hooksListCmd)
|
|
||||||
hooksCmd.AddCommand(hooksAddCmd)
|
|
||||||
hooksCmd.AddCommand(hooksRemoveCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accounts command group
|
|
||||||
var accountsCmd = &cobra.Command{
|
|
||||||
Use: "accounts",
|
|
||||||
Short: "Manage WhatsApp accounts",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
listAccounts(cliConfig.ServerURL)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountsListCmd = &cobra.Command{
|
|
||||||
Use: "list",
|
|
||||||
Short: "List all accounts",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
listAccounts(cliConfig.ServerURL)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountsAddCmd = &cobra.Command{
|
|
||||||
Use: "add",
|
|
||||||
Short: "Add a new WhatsApp account",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
addAccount(cliConfig.ServerURL)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
accountsCmd.AddCommand(accountsListCmd)
|
|
||||||
accountsCmd.AddCommand(accountsAddCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send command group
|
|
||||||
var sendCmd = &cobra.Command{
|
|
||||||
Use: "send",
|
|
||||||
Short: "Send messages",
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendTextCmd = &cobra.Command{
|
|
||||||
Use: "text",
|
|
||||||
Short: "Send a text message",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
sendMessage(cliConfig.ServerURL)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendImageCmd = &cobra.Command{
|
|
||||||
Use: "image <file_path>",
|
|
||||||
Short: "Send an image",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
sendImage(cliConfig.ServerURL, args[0])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendVideoCmd = &cobra.Command{
|
|
||||||
Use: "video <file_path>",
|
|
||||||
Short: "Send a video",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
sendVideo(cliConfig.ServerURL, args[0])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendDocumentCmd = &cobra.Command{
|
|
||||||
Use: "document <file_path>",
|
|
||||||
Short: "Send a document",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
sendDocument(cliConfig.ServerURL, args[0])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
sendCmd.AddCommand(sendTextCmd)
|
|
||||||
sendCmd.AddCommand(sendImageCmd)
|
|
||||||
sendCmd.AddCommand(sendVideoCmd)
|
|
||||||
sendCmd.AddCommand(sendDocumentCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
|
|
||||||
func checkHealth(serverURL string) {
|
|
||||||
resp, err := http.Get(serverURL + "/health")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var result map[string]string
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
fmt.Printf("Error decoding response: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Server status: %s\n", result["status"])
|
|
||||||
}
|
|
||||||
|
|
||||||
func listHooks(serverURL string) {
|
|
||||||
resp, err := http.Get(serverURL + "/api/hooks")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var hooks []config.Hook
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&hooks); err != nil {
|
|
||||||
fmt.Printf("Error decoding response: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(hooks) == 0 {
|
|
||||||
fmt.Println("No hooks configured")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Configured hooks (%d):\n\n", len(hooks))
|
|
||||||
for _, hook := range hooks {
|
|
||||||
status := "inactive"
|
|
||||||
if hook.Active {
|
|
||||||
status = "active"
|
|
||||||
}
|
|
||||||
fmt.Printf("ID: %s\n", hook.ID)
|
|
||||||
fmt.Printf("Name: %s\n", hook.Name)
|
|
||||||
fmt.Printf("URL: %s\n", hook.URL)
|
|
||||||
fmt.Printf("Method: %s\n", hook.Method)
|
|
||||||
fmt.Printf("Status: %s\n", status)
|
|
||||||
if hook.Description != "" {
|
|
||||||
fmt.Printf("Description: %s\n", hook.Description)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addHook(serverURL string) {
|
|
||||||
var hook config.Hook
|
|
||||||
|
|
||||||
fmt.Print("Hook ID: ")
|
|
||||||
fmt.Scanln(&hook.ID)
|
|
||||||
|
|
||||||
fmt.Print("Hook Name: ")
|
|
||||||
fmt.Scanln(&hook.Name)
|
|
||||||
|
|
||||||
fmt.Print("Webhook URL: ")
|
|
||||||
fmt.Scanln(&hook.URL)
|
|
||||||
|
|
||||||
fmt.Print("HTTP Method (POST): ")
|
|
||||||
fmt.Scanln(&hook.Method)
|
|
||||||
if hook.Method == "" {
|
|
||||||
hook.Method = "POST"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print("Description (optional): ")
|
|
||||||
fmt.Scanln(&hook.Description)
|
|
||||||
|
|
||||||
hook.Active = true
|
|
||||||
|
|
||||||
data, err := json.Marshal(hook)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Post(serverURL+"/api/hooks/add", "application/json", bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
fmt.Printf("Error: %s\n", string(body))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Hook added successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeHook(serverURL string, id string) {
|
|
||||||
req := map[string]string{"id": id}
|
|
||||||
data, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Post(serverURL+"/api/hooks/remove", "application/json", bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
fmt.Printf("Error: %s\n", string(body))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Hook removed successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func listAccounts(serverURL string) {
|
|
||||||
resp, err := http.Get(serverURL + "/api/accounts")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var accounts []config.WhatsAppConfig
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&accounts); err != nil {
|
|
||||||
fmt.Printf("Error decoding response: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(accounts) == 0 {
|
|
||||||
fmt.Println("No accounts configured")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Configured accounts (%d):\n\n", len(accounts))
|
|
||||||
for _, acc := range accounts {
|
|
||||||
fmt.Printf("ID: %s\n", acc.ID)
|
|
||||||
fmt.Printf("Phone Number: %s\n", acc.PhoneNumber)
|
|
||||||
fmt.Printf("Session Path: %s\n", acc.SessionPath)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addAccount(serverURL string) {
|
|
||||||
var account config.WhatsAppConfig
|
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
|
||||||
fmt.Scanln(&account.ID)
|
|
||||||
|
|
||||||
fmt.Print("Phone Number (with country code): ")
|
|
||||||
fmt.Scanln(&account.PhoneNumber)
|
|
||||||
|
|
||||||
fmt.Print("Session Path: ")
|
|
||||||
fmt.Scanln(&account.SessionPath)
|
|
||||||
|
|
||||||
data, err := json.Marshal(account)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Post(serverURL+"/api/accounts/add", "application/json", bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
fmt.Printf("Error: %s\n", string(body))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Account added successfully")
|
|
||||||
fmt.Println("Check server logs for QR code to pair the device")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendMessage(serverURL string) {
|
|
||||||
var req struct {
|
|
||||||
AccountID string `json:"account_id"`
|
|
||||||
To string `json:"to"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
|
||||||
fmt.Scanln(&req.AccountID)
|
|
||||||
|
|
||||||
fmt.Print("Recipient (phone number or JID, e.g., 0834606792 or 1234567890@s.whatsapp.net): ")
|
|
||||||
fmt.Scanln(&req.To)
|
|
||||||
|
|
||||||
fmt.Print("Message text: ")
|
|
||||||
reader := os.Stdin
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, err := reader.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading input: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
req.Text = string(buf[:n])
|
|
||||||
|
|
||||||
data, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Post(serverURL+"/api/send", "application/json", bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
fmt.Printf("Error: %s\n", string(body))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Message sent successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendImage(serverURL string, filePath string) {
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
|
||||||
fmt.Scanln(&req.AccountID)
|
|
||||||
|
|
||||||
fmt.Print("Recipient (phone number): ")
|
|
||||||
fmt.Scanln(&req.To)
|
|
||||||
|
|
||||||
fmt.Print("Caption (optional): ")
|
|
||||||
reader := os.Stdin
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, _ := reader.Read(buf)
|
|
||||||
req.Caption = strings.TrimSpace(string(buf[:n]))
|
|
||||||
|
|
||||||
// Read image file
|
|
||||||
imageData, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading image file: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode to base64
|
|
||||||
req.ImageData = base64.StdEncoding.EncodeToString(imageData)
|
|
||||||
|
|
||||||
// Detect mime type from extension
|
|
||||||
ext := strings.ToLower(filepath.Ext(filePath))
|
|
||||||
switch ext {
|
|
||||||
case ".jpg", ".jpeg":
|
|
||||||
req.MimeType = "image/jpeg"
|
|
||||||
case ".png":
|
|
||||||
req.MimeType = "image/png"
|
|
||||||
case ".gif":
|
|
||||||
req.MimeType = "image/gif"
|
|
||||||
case ".webp":
|
|
||||||
req.MimeType = "image/webp"
|
|
||||||
default:
|
|
||||||
req.MimeType = "image/jpeg"
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Post(serverURL+"/api/send/image", "application/json", bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
fmt.Printf("Error: %s\n", string(body))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Image sent successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendVideo(serverURL string, filePath string) {
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
|
||||||
fmt.Scanln(&req.AccountID)
|
|
||||||
|
|
||||||
fmt.Print("Recipient (phone number): ")
|
|
||||||
fmt.Scanln(&req.To)
|
|
||||||
|
|
||||||
fmt.Print("Caption (optional): ")
|
|
||||||
reader := os.Stdin
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, _ := reader.Read(buf)
|
|
||||||
req.Caption = strings.TrimSpace(string(buf[:n]))
|
|
||||||
|
|
||||||
// Read video file
|
|
||||||
videoData, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading video file: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode to base64
|
|
||||||
req.VideoData = base64.StdEncoding.EncodeToString(videoData)
|
|
||||||
|
|
||||||
// Detect mime type from extension
|
|
||||||
ext := strings.ToLower(filepath.Ext(filePath))
|
|
||||||
switch ext {
|
|
||||||
case ".mp4":
|
|
||||||
req.MimeType = "video/mp4"
|
|
||||||
case ".mov":
|
|
||||||
req.MimeType = "video/quicktime"
|
|
||||||
case ".avi":
|
|
||||||
req.MimeType = "video/x-msvideo"
|
|
||||||
case ".webm":
|
|
||||||
req.MimeType = "video/webm"
|
|
||||||
case ".3gp":
|
|
||||||
req.MimeType = "video/3gpp"
|
|
||||||
default:
|
|
||||||
req.MimeType = "video/mp4"
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Post(serverURL+"/api/send/video", "application/json", bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
fmt.Printf("Error: %s\n", string(body))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Video sent successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendDocument(serverURL string, filePath string) {
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Print("Account ID: ")
|
|
||||||
fmt.Scanln(&req.AccountID)
|
|
||||||
|
|
||||||
fmt.Print("Recipient (phone number): ")
|
|
||||||
fmt.Scanln(&req.To)
|
|
||||||
|
|
||||||
fmt.Print("Caption (optional): ")
|
|
||||||
reader := os.Stdin
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, _ := reader.Read(buf)
|
|
||||||
req.Caption = strings.TrimSpace(string(buf[:n]))
|
|
||||||
|
|
||||||
// Read document file
|
|
||||||
documentData, err := os.ReadFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading document file: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode to base64
|
|
||||||
req.DocumentData = base64.StdEncoding.EncodeToString(documentData)
|
|
||||||
|
|
||||||
// Use the original filename
|
|
||||||
req.Filename = filepath.Base(filePath)
|
|
||||||
|
|
||||||
// Detect mime type from extension
|
|
||||||
ext := strings.ToLower(filepath.Ext(filePath))
|
|
||||||
switch ext {
|
|
||||||
case ".pdf":
|
|
||||||
req.MimeType = "application/pdf"
|
|
||||||
case ".doc":
|
|
||||||
req.MimeType = "application/msword"
|
|
||||||
case ".docx":
|
|
||||||
req.MimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
||||||
case ".xls":
|
|
||||||
req.MimeType = "application/vnd.ms-excel"
|
|
||||||
case ".xlsx":
|
|
||||||
req.MimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
||||||
case ".txt":
|
|
||||||
req.MimeType = "text/plain"
|
|
||||||
case ".zip":
|
|
||||||
req.MimeType = "application/zip"
|
|
||||||
default:
|
|
||||||
req.MimeType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.Post(serverURL+"/api/send/document", "application/json", bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
fmt.Printf("Error: %s\n", string(body))
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Document sent successfully")
|
|
||||||
}
|
|
||||||
|
|||||||
51
docker-compose.yml
Normal file
51
docker-compose.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
whatshooked:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: whatshooked-server
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
# Mount config file
|
||||||
|
- ./config.json:/app/config.json:ro
|
||||||
|
|
||||||
|
# Mount sessions directory for WhatsApp authentication persistence
|
||||||
|
- ./sessions:/app/sessions
|
||||||
|
|
||||||
|
# Mount media directory for storing downloaded media files
|
||||||
|
- ./data/media:/app/data/media
|
||||||
|
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Environment variables (optional - can override config.json settings)
|
||||||
|
# environment:
|
||||||
|
# - LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Use host network mode if you need QR code scanning via terminal
|
||||||
|
# network_mode: "host"
|
||||||
|
|
||||||
|
# Resource limits (optional)
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# limits:
|
||||||
|
# cpus: '1.0'
|
||||||
|
# memory: 512M
|
||||||
|
# reservations:
|
||||||
|
# cpus: '0.25'
|
||||||
|
# memory: 128M
|
||||||
|
|
||||||
|
# Optional: Add networks for more complex setups
|
||||||
|
# networks:
|
||||||
|
# whatshooked-network:
|
||||||
|
# driver: bridge
|
||||||
Reference in New Issue
Block a user