Browse Source

Merge branch 'main' into fix/lid-normalize

pull/291/head
almogbaku 9 months ago
parent
commit
71660ee1c1
No known key found for this signature in database GPG Key ID: 66C92B1C5B475512
  1. 122
      .cursor/rules/project-structure.mdc
  2. 10
      .dockerignore
  3. 2
      docker/golang.Dockerfile
  4. 209
      docs/openapi.yaml
  5. 148
      readme.md
  6. 2
      src/.env.example
  7. 62
      src/cmd/mcp.go
  8. 115
      src/cmd/rest.go
  9. 168
      src/cmd/root.go
  10. 5
      src/config/settings.go
  11. 2
      src/domains/app/app.go
  12. 2
      src/domains/group/group.go
  13. 2
      src/domains/message/message.go
  14. 2
      src/domains/newsletter/newsletter.go
  15. 2
      src/domains/send/send.go
  16. 8
      src/domains/user/account.go
  17. 3
      src/domains/user/user.go
  18. 22
      src/go.mod
  19. 102
      src/go.sum
  20. 78
      src/infrastructure/whatsapp/init.go
  21. 5
      src/infrastructure/whatsapp/utils.go
  22. 16
      src/infrastructure/whatsapp/webhook.go
  23. 338
      src/ui/mcp/send.go
  24. 5
      src/ui/rest/app.go
  25. 6
      src/ui/rest/group.go
  26. 7
      src/ui/rest/helpers/common.go
  27. 0
      src/ui/rest/helpers/flushChatCsv.go
  28. 6
      src/ui/rest/message.go
  29. 0
      src/ui/rest/middleware/basicauth.go
  30. 0
      src/ui/rest/middleware/recovery.go
  31. 4
      src/ui/rest/newsletter.go
  32. 6
      src/ui/rest/send.go
  33. 23
      src/ui/rest/user.go
  34. 2
      src/ui/websocket/websocket.go
  35. 16
      src/usecase/app.go
  36. 24
      src/usecase/group.go
  37. 10
      src/usecase/message.go
  38. 13
      src/usecase/newsletter.go
  39. 10
      src/usecase/send.go
  40. 42
      src/usecase/user.go
  41. 95
      src/views/components/AccountUserCheck.js
  42. 4
      src/views/index.html

122
.cursor/rules/project-structure.mdc

@ -0,0 +1,122 @@
---
description:
globs:
alwaysApply: false
---
# Go WhatsApp Web Multidevice API
This is a Go implementation of a WhatsApp Web Multidevice API that allows you to interact with WhatsApp through HTTP APIs.
## Project Structure
### Root Directory
- [readme.md](mdc:readme.md) - Project documentation and usage instructions
- [docker-compose.yml](mdc:docker-compose.yml) - Docker configuration for running the application
- [LICENCE.txt](mdc:LICENCE.txt) - License information
### Source Code (`src/`)
The main source code is organized in the `src` directory with the following structure:
#### Command Line Interface
- [src/cmd/root.go](mdc:src/cmd/root.go) - Main entry point using Cobra for CLI commands, handles configuration loading and server initialization
#### Configuration
- [src/config/](mdc:src/config) - Application configuration settings and constants
#### Domain Models
The application is organized using domain-driven design principles:
- [src/domains/app/](mdc:src/domains/app) - Core application domain models
- [src/domains/group/](mdc:src/domains/group) - Group-related domain models
- [src/domains/message/](mdc:src/domains/message) - Message-related domain models
- [src/domains/newsletter/](mdc:src/domains/newsletter) - Newsletter-related domain models
- [src/domains/send/](mdc:src/domains/send) - Message sending domain models
- [src/domains/user/](mdc:src/domains/user) - User-related domain models
#### Infrastructure
- [src/infrastructure/whatsapp/](mdc:src/infrastructure/whatsapp) - WhatsApp client implementation and related infrastructure
#### User Interface
- [src/ui/rest/](mdc:src/ui/rest) - REST API implementation
- [src/ui/rest/helpers/](mdc:src/ui/rest/helpers) - Helper functions for REST handlers
- [src/ui/rest/middleware/](mdc:src/ui/rest/middleware) - Middleware components for request processing
- [src/ui/websocket/](mdc:src/ui/websocket) - WebSocket implementation for real-time communication
- [src/ui/mcp/](mdc:src/ui/mcp) - Model Context Protocol server to communication with AI Agent
#### Utilities and Shared Components
- [src/pkg/error/](mdc:src/pkg/error) - Error handling utilities
- [src/pkg/utils/](mdc:src/pkg/utils) - General utility functions
#### Use Cases
- [src/usecase/](mdc:src/usecase) - Application services that implement business logic
#### Static Resources
- [src/statics/](mdc:src/statics) - Static resources like media files
- [src/statics/media/](mdc:src/statics/media) - Media files
- [src/statics/qrcode/](mdc:src/statics/qrcode) - QR code images for WhatsApp authentication
- [src/statics/senditems/](mdc:src/statics/senditems) - Items to be sent via WhatsApp
#### Storage
- [src/storages/](mdc:src/storages) - Storage-related functionality and database connection
#### Temporary Files
- [src/tmp/](mdc:src/tmp) - Temporary files and directories
#### Validation
- [src/validations/](mdc:src/validations) - Request validation logic
#### Views
- [src/views/](mdc:src/views) - Templates and UI components
- [src/views/assets/](mdc:src/views/assets) - Frontend assets (CSS, JS, etc.)
- [src/views/components/](mdc:src/views/components) - Reusable UI components
- [src/views/components/generic/](mdc:src/views/components/generic) - Generic UI components
### Documentation
- [docs/](mdc:docs) - Project documentation
- [docs/sdk/](mdc:docs/sdk) - SDK documentation
### Docker
- [docker/](mdc:docker) - Docker-related files and configurations
### GitHub Configuration
- [.github/](mdc:.github) - GitHub-specific configuration
- [.github/ISSUE_TEMPLATE/](mdc:.github/ISSUE_TEMPLATE) - Templates for GitHub issues
- [.github/workflows/](mdc:.github/workflows) - GitHub Actions workflows
## Key Application Features
- WhatsApp login via QR code or pairing code
- Send/receive messages, media, contacts, locations
- Group management features
- Newsletter management
- WebSocket real-time updates
- Webhooks for message events
- Auto-reply functionality
## Application Flow
1. The application starts from [src/cmd/root.go](mdc:src/cmd/root.go)
2. Configuration is loaded from environment variables or command line flags
3. The REST server is initialized using Fiber framework
4. WhatsApp client is initialized and services are created
5. REST routes are registered for different domains
6. WebSocket hub is started for real-time communication
7. Background tasks are started (auto-reconnect, chat storage flushing)
8. The server listens for requests on the configured port

10
.dockerignore

@ -0,0 +1,10 @@
.idea
.vscode
.git
.gitignore
.env
.env.local
.env.development
.env.test
.env.production
docker

2
docker/golang.Dockerfile

@ -21,3 +21,5 @@ WORKDIR /app
COPY --from=builder /app/whatsapp /app/whatsapp
# Run the binary.
ENTRYPOINT ["/app/whatsapp"]
CMD [ "rest" ]

209
docs/openapi.yaml

@ -1,7 +1,7 @@
openapi: "3.0.0"
info:
title: WhatsApp API MultiDevice
version: 5.4.0
version: 6.1.0
description: This API is used for sending whatsapp via API
servers:
- url: http://localhost:3000
@ -175,6 +175,12 @@ paths:
type: boolean
example: true
description: Whether to fetch a preview of the avatar
- name: is_community
in: query
schema:
type: boolean
example: false
description: Whether to fetch a community avatar
responses:
'200':
description: OK
@ -342,6 +348,38 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/user/check:
get:
operationId: userCheck
tags:
- user
summary: Check if user is on WhatsApp
parameters:
- name: phone
in: query
schema:
type: string
example: '628912344551'
description: Phone number with country code
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/UserCheckResponse'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorBadRequest'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/send/message:
post:
@ -367,6 +405,10 @@ paths:
type: string
example: 3EB089B9D6ADD58153C561
description: Message ID that you want reply
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
responses:
'200':
description: OK
@ -422,6 +464,10 @@ paths:
type: boolean
example: false
description: Compress image
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
responses:
'200':
description: OK
@ -461,6 +507,10 @@ paths:
type: string
format: binary
description: Audio to send
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
responses:
'200':
description: OK
@ -504,6 +554,10 @@ paths:
type: string
format: binary
description: File to send
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
responses:
'200':
description: OK
@ -545,7 +599,7 @@ paths:
description: Caption to send
view_once:
type: boolean
example: 'false'
example: false
description: View once
video:
type: string
@ -553,8 +607,12 @@ paths:
description: Video to send
compress:
type: boolean
example: 'false'
example: false
description: Compress video
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
responses:
'200':
description: OK
@ -598,6 +656,10 @@ paths:
type: string
example: '6289685024992'
description: Contact phone number
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
responses:
'200':
description: OK
@ -641,6 +703,10 @@ paths:
type: string
example: 'Halo ini contoh caption'
description: Caption to send
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
responses:
'200':
description: OK
@ -684,6 +750,10 @@ paths:
type: string
example: '110.370529'
description: Longitude coordinate
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
responses:
'200':
description: OK
@ -771,13 +841,17 @@ paths:
schema:
type: object
properties:
presence:
type:
type: string
description: The presence status to send
description: The presence type to send
enum: [available, unavailable]
example: 'available'
is_forwarded:
type: boolean
example: false
description: Whether this is a forwarded message
required:
- presence
- type
responses:
'200':
description: OK
@ -1020,6 +1094,94 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/message/{message_id}/star:
post:
operationId: starMessage
tags:
- message
summary: Star message
parameters:
- in: path
name: message_id
schema:
type: string
required: true
description: Message ID
requestBody:
content:
application/json:
schema:
type: object
properties:
phone:
type: string
example: '62819273192397132@s.whatsapp.net'
description: Phone number with country code
required:
- phone
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GenericResponse'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorBadRequest'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/message/{message_id}/unstar:
post:
operationId: unstarMessage
tags:
- message
summary: Unstar message
parameters:
- in: path
name: message_id
schema:
type: string
required: true
description: Message ID
requestBody:
content:
application/json:
schema:
type: object
properties:
phone:
type: string
example: '62819273192397132@s.whatsapp.net'
description: Phone number with country code
required:
- phone
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/GenericResponse'
'400':
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorBadRequest'
'500':
description: Internal Server Error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorInternalServer'
/group:
post:
operationId: createGroup
@ -1269,13 +1431,15 @@ paths:
type: string
example: '120363024512399999@g.us'
description: The group ID
participant_id:
participants:
type: array
items:
type: string
example: '6281234567890'
description: The participant's WhatsApp ID to approve
example: ['6281234567890']
description: Array of participant WhatsApp IDs to approve
required:
- group_id
- participant_id
- participants
responses:
'200':
description: OK
@ -1311,13 +1475,15 @@ paths:
type: string
example: '120363024512399999@g.us'
description: The group ID
participant_id:
participants:
type: array
items:
type: string
example: '6281234567890'
description: The participant's WhatsApp ID to reject
example: ['6281234567890']
description: Array of participant WhatsApp IDs to reject
required:
- group_id
- participant_id
- participants
responses:
'200':
description: OK
@ -2004,3 +2170,18 @@ components:
type: string
format: date-time
example: "2024-10-11T21:27:29+07:00"
UserCheckResponse:
type: object
properties:
code:
type: string
example: SUCCESS
message:
type: string
example: Success check user
results:
type: object
properties:
is_on_whatsapp:
type: boolean
example: true

148
readme.md

@ -13,16 +13,25 @@ ___
![release linux](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/release-linux.yml/badge.svg)
![release macos](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/release-mac.yml/badge.svg)
## Support `ARM` Architecture
## Support for `ARM` & `AMD` Architecture along with `MCP` Support
Now that we support ARM64 for Linux:
Download:
- [Release](https://github.com/aldinokemal/go-whatsapp-web-multidevice/releases/latest) for ARM64
- [Docker Image](https://hub.docker.com/r/aldinokemal2104/go-whatsapp-web-multidevice/tags) for ARM64.
- [Release](https://github.com/aldinokemal/go-whatsapp-web-multidevice/releases/latest)
- [Docker Image](https://hub.docker.com/r/aldinokemal2104/go-whatsapp-web-multidevice/tags)
## Breaking Changes
- `v6`
- For REST mode, you need to run `<binary> rest` instead of `<binary>`
- for example: `./whatsapp rest` instead of ~~./whatsapp~~
- For MCP mode, you need to run `<binary> mcp`
- for example: `./whatsapp mcp`
## Feature
- Send WhatsApp message via http API, [docs/openapi.yml](./docs/openapi.yaml) for more details
- **MCP (Model Context Protocol) Server Support** - Integrate with AI agents and tools using standardized protocol
- Mention someone
- `@phoneNumber`
- example: `Hello @628974812XXXX, @628974812XXXX`
@ -59,19 +68,52 @@ can be set in three ways (in order of priority):
### Environment Variables
You can configure the application using environment variables. Configuration can be set in three ways (in order of priority):
1. Command-line flags (highest priority)
2. Environment variables
3. `.env` file (lowest priority)
To use environment variables:
1. Copy `.env.example` to `.env` in your project root
1. Copy `.env.example` to `.env` in your project root (`cp src/.env.example src/.env`)
2. Modify the values in `.env` according to your needs
3. Or set the same variables as system environment variables
See [.env.example](./src/.env.example) for all available configuration options.
#### Available Environment Variables
| Variable | Description | Default | Example |
|----------|-------------|---------|---------|
| `APP_PORT` | Application port | `3000` | `APP_PORT=8080` |
| `APP_DEBUG` | Enable debug logging | `false` | `APP_DEBUG=true` |
| `APP_OS` | OS name (device name in WhatsApp) | `Chrome` | `APP_OS=MyApp` |
| `APP_BASIC_AUTH` | Basic authentication credentials | - | `APP_BASIC_AUTH=user1:pass1,user2:pass2` |
| `APP_CHAT_FLUSH_INTERVAL` | Chat flush interval in days | `7` | `APP_CHAT_FLUSH_INTERVAL=30` |
| `DB_URI` | Database connection URI | `file:storages/whatsapp.db?_foreign_keys=on` | `DB_URI=postgres://user:pass@host/db` |
| `WHATSAPP_AUTO_REPLY` | Auto-reply message | - | `WHATSAPP_AUTO_REPLY="Auto reply message"` |
| `WHATSAPP_WEBHOOK` | Webhook URL(s) for events (comma-separated) | - | `WHATSAPP_WEBHOOK=https://webhook.site/xxx` |
| `WHATSAPP_WEBHOOK_SECRET` | Webhook secret for validation | `secret` | `WHATSAPP_WEBHOOK_SECRET=super-secret-key` |
| `WHATSAPP_ACCOUNT_VALIDATION` | Enable account validation | `true` | `WHATSAPP_ACCOUNT_VALIDATION=false` |
| `WHATSAPP_CHAT_STORAGE` | Enable chat storage | `true` | `WHATSAPP_CHAT_STORAGE=false` |
Note: Command-line flags will override any values set in environment variables or `.env` file.
- For more command `./main --help`
- For more command `./whatsapp --help`
## Requirements
### System Requirements
## Required (without docker)
- **Go 1.24.0 or higher** (for building from source)
- **FFmpeg** (for media processing)
### Platform Support
- Linux (x86_64, ARM64)
- macOS (Intel, Apple Silicon)
- Windows (x86_64) - WSL recommended
### Dependencies (without docker)
- Mac OS:
- `brew install ffmpeg`
@ -80,7 +122,7 @@ Note: Command-line flags will override any values set in environment variables o
- `sudo apt update`
- `sudo apt install ffmpeg`
- Windows (not recomended, prefer using [WSL](https://docs.microsoft.com/en-us/windows/wsl/install)):
- install ffmpeg, download [here](https://www.ffmpeg.org/download.html#build-windows)
- install ffmpeg, [download here](https://www.ffmpeg.org/download.html#build-windows)
- add to ffmpeg to [environment variable](https://www.google.com/search?q=windows+add+to+environment+path)
## How to use
@ -90,7 +132,7 @@ Note: Command-line flags will override any values set in environment variables o
1. Clone this repo: `git clone https://github.com/aldinokemal/go-whatsapp-web-multidevice`
2. Open the folder that was cloned via cmd/terminal.
3. run `cd src`
4. run `go run main.go`
4. run `go run . rest` (for REST API mode)
5. Open `http://localhost:3000`
### Docker (you don't need to install in required)
@ -109,19 +151,62 @@ Note: Command-line flags will override any values set in environment variables o
1. Linux & MacOS: `go build -o whatsapp`
2. Windows (CMD / PowerShell): `go build -o whatsapp.exe`
5. run
1. Linux & MacOS: `./whatsapp`
1. Linux & MacOS: `./whatsapp rest` (for REST API mode)
1. run `./whatsapp --help` for more detail flags
2. Windows: `.\whatsapp.exe` or you can double-click it
2. Windows: `.\whatsapp.exe rest` (for REST API mode)
1. run `.\whatsapp.exe --help` for more detail flags
6. open `http://localhost:3000` in browser
### Production Mode (docker)
### MCP Server (Model Context Protocol)
This application can also run as an MCP server, allowing AI agents and tools to interact with WhatsApp through a standardized protocol.
1. Clone this repo `git clone https://github.com/aldinokemal/go-whatsapp-web-multidevice`
2. Open the folder that was cloned via cmd/terminal.
3. run `cd src`
4. run `go run . mcp` or build the binary and run `./whatsapp mcp`
5. The MCP server will start on `http://localhost:8080` by default
#### MCP Server Options
- `--host localhost` - Set the host for MCP server (default: localhost)
- `--port 8080` - Set the port for MCP server (default: 8080)
#### Available MCP Tools
- `whatsapp_send_text` - Send text messages
- `whatsapp_send_contact` - Send contact cards
- `whatsapp_send_link` - Send links with captions
- `whatsapp_send_location` - Send location coordinates
#### MCP Endpoints
- SSE endpoint: `http://localhost:8080/sse`
- Message endpoint: `http://localhost:8080/message`
### MCP Configuration
Make sure you have the MCP server running: `./whatsapp mcp`
For AI tools that support MCP with SSE (like Cursor), add this configuration:
```json
{
"mcpServers": {
"whatsapp": {
"url": "http://localhost:8080/sse"
}
}
}
```
### Production Mode REST (docker)
```bash
docker run --detach --publish=3000:3000 --name=whatsapp --restart=always --volume=$(docker volume create --name=whatsapp):/app/storages aldinokemal2104/go-whatsapp-web-multidevice --autoreply="Dont't reply this message please"
docker run --detach --publish=3000:3000 --name=whatsapp --restart=always --volume=$(docker volume create --name=whatsapp):/app/storages aldinokemal2104/go-whatsapp-web-multidevice rest --autoreply="Dont't reply this message please"
```
### Production Mode (docker compose)
### Production Mode REST (docker compose)
create `docker-compose.yml` file with the following configuration:
@ -136,6 +221,7 @@ services:
volumes:
- whatsapp:/app/storages
command:
- rest
- --basic-auth=admin:admin
- --port=3000
- --debug=true
@ -177,6 +263,15 @@ You can fork or edit this source code !
## Current API
### MCP (Model Context Protocol) API
- MCP server provides standardized tools for AI agents to interact with WhatsApp
- Supports Server-Sent Events (SSE) transport
- Available tools: `whatsapp_send_text`, `whatsapp_send_contact`, `whatsapp_send_link`, `whatsapp_send_location`
- Compatible with MCP-enabled AI tools and agents
### HTTP REST API
- [API Specification Document](https://bump.sh/aldinokemal/doc/go-whatsapp-web-multidevice).
- Check [docs/openapi.yml](./docs/openapi.yaml) for detailed API specifications.
- Use [SwaggerEditor](https://editor.swagger.io) to visualize the API.
@ -213,6 +308,7 @@ You can fork or edit this source code !
| ✅ | Edit Message | POST | /message/:message_id/update |
| ✅ | Read Message (DM) | POST | /message/:message_id/read |
| ✅ | Star Message | POST | /message/:message_id/star |
| ✅ | Unstar Message | POST | /message/:message_id/unstar |
| ✅ | Join Group With Link | POST | /group/join-with-link |
| ✅ | Leave Group | POST | /group/leave |
| ✅ | Create Group | POST | /group |
@ -220,9 +316,9 @@ You can fork or edit this source code !
| ✅ | Remove Participant in Group | POST | /group/participants/remove |
| ✅ | Promote Participant in Group | POST | /group/participants/promote |
| ✅ | Demote Participant in Group | POST | /group/participants/demote |
| ✅ | List Requested Participants in Group | POST | /group/participants/requested |
| ✅ | Approve Requested Participant in Group | POST | /group/participants/requested/approve |
| ✅ | Reject Requested Participant in Group | POST | /group/participants/requested/reject |
| ✅ | List Requested Participants in Group | GET | /group/participant-requests |
| ✅ | Approve Requested Participant in Group | POST | /group/participant-requests/approve |
| ✅ | Reject Requested Participant in Group | POST | /group/participant-requests/reject |
| ✅ | Unfollow Newsletter | POST | /newsletter/unfollow |
```txt
@ -230,7 +326,18 @@ You can fork or edit this source code !
❌ = Not Available Yet
```
### User Interface
## User Interface
### MCP UI
- Setup MCP (tested in cursor)
![Setup MCP](https://i.ibb.co/vCg4zNWt/mcpsetup.png)
- Test MCP
![Test MCP](https://i.ibb.co/B2LX38DW/mcptest.png)
- Successfully setup MCP
![Success MCP](https://i.ibb.co/1fCx0Myc/mcpsuccess.png)
### HTTP REST API UI
| Description | Image |
|----------------------|------------------------------------------------------------------------------------------|
@ -244,7 +351,7 @@ You can fork or edit this source code !
| Send Contact | ![Send Contact](https://i.ibb.co.com/NsFfQBv/send-Contact.png) |
| Send Location | ![Send Location](https://i.ibb.co.com/vDGmFvk/send-Location.png) |
| Send Audio | ![Send Audio](https://i.ibb.co.com/XJdQLP8/send-Audio.png) |
| Send Poll | ![Send Poll](https://i.ibb.co.com/4TswfT3/sendPoll.png) |
| Send Poll | ![Send Poll](https://i.ibb.co.com/4TswfT3/sendPoll.png?v=1) |
| Send Presence | ![Send Presence](https://i.ibb.co.com/NSTC3QX/send-Presence.png) |
| Revoke Message | ![Revoke Message](https://i.ibb.co.com/r4nDc57/revoke-Message.png) |
| Delete Message | ![Delete Message](https://i.ibb.co.com/dtrTJ1M/delete-Message.png) |
@ -270,3 +377,4 @@ You can fork or edit this source code !
- This project is unofficial and not affiliated with WhatsApp.
- Please use official WhatsApp API to avoid any issues.
- We only able to run MCP or REST API, this is limitation from whatsmeow library. independent MCP will be available in the future.

2
src/.env.example

@ -6,7 +6,7 @@ APP_BASIC_AUTH=user1:pass1,user2:pass2
APP_CHAT_FLUSH_INTERVAL=7
# Database Settings
DB_URI="file:storages/whatsapp.db?_foreign_keys=off"
DB_URI="file:storages/whatsapp.db?_foreign_keys=on"
# WhatsApp Settings
WHATSAPP_AUTO_REPLY="Auto reply message"

62
src/cmd/mcp.go

@ -0,0 +1,62 @@
package cmd
import (
"fmt"
"log"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/aldinokemal/go-whatsapp-web-multidevice/ui/mcp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/ui/rest/helpers"
"github.com/mark3labs/mcp-go/server"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var mcpCmd = &cobra.Command{
Use: "mcp",
Short: "Start WhatsApp MCP server using SSE",
Long: `Start a WhatsApp MCP (Model Context Protocol) server using Server-Sent Events (SSE) transport. This allows AI agents to interact with WhatsApp through a standardized protocol.`,
Run: mcpServer,
}
func init() {
rootCmd.AddCommand(mcpCmd)
mcpCmd.Flags().StringVar(&config.McpPort, "port", "8080", "Port for the SSE MCP server")
mcpCmd.Flags().StringVar(&config.McpHost, "host", "localhost", "Host for the SSE MCP server")
}
func mcpServer(_ *cobra.Command, _ []string) {
// Set auto reconnect to whatsapp server after booting
go helpers.SetAutoConnectAfterBooting(appUsecase)
// Set auto reconnect checking
go helpers.SetAutoReconnectChecking(whatsappCli)
// Create MCP server with capabilities
mcpServer := server.NewMCPServer(
"WhatsApp Web Multidevice MCP Server",
config.AppVersion,
server.WithToolCapabilities(true),
server.WithResourceCapabilities(true, true),
)
// Add all WhatsApp tools
sendHandler := mcp.InitMcpSend(sendUsecase)
sendHandler.AddSendTools(mcpServer)
// Create SSE server
sseServer := server.NewSSEServer(
mcpServer,
server.WithBaseURL(fmt.Sprintf("http://%s:%s", config.McpHost, config.McpPort)),
server.WithKeepAlive(true),
)
// Start the SSE server
addr := fmt.Sprintf("%s:%s", config.McpHost, config.McpPort)
log.Printf("Starting WhatsApp MCP SSE server on %s", addr)
log.Printf("SSE endpoint: http://%s:%s/sse", config.McpHost, config.McpPort)
log.Printf("Message endpoint: http://%s:%s/message", config.McpHost, config.McpPort)
if err := sseServer.Start(addr); err != nil {
log.Fatalf("Failed to start SSE server: %v", err)
}
}

115
src/cmd/rest.go

@ -0,0 +1,115 @@
package cmd
import (
"fmt"
"log"
"net/http"
"strings"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/aldinokemal/go-whatsapp-web-multidevice/ui/rest"
"github.com/aldinokemal/go-whatsapp-web-multidevice/ui/rest/helpers"
"github.com/aldinokemal/go-whatsapp-web-multidevice/ui/rest/middleware"
"github.com/aldinokemal/go-whatsapp-web-multidevice/ui/websocket"
"github.com/dustin/go-humanize"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/template/html/v2"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var restCmd = &cobra.Command{
Use: "rest",
Short: "Send whatsapp API over http",
Long: `This application is from clone https://github.com/aldinokemal/go-whatsapp-web-multidevice`,
Run: restServer,
}
func init() {
rootCmd.AddCommand(restCmd)
}
func restServer(_ *cobra.Command, _ []string) {
engine := html.NewFileSystem(http.FS(EmbedIndex), ".html")
engine.AddFunc("isEnableBasicAuth", func(token any) bool {
return token != nil
})
app := fiber.New(fiber.Config{
Views: engine,
BodyLimit: int(config.WhatsappSettingMaxVideoSize),
})
app.Static("/statics", "./statics")
app.Use("/components", filesystem.New(filesystem.Config{
Root: http.FS(EmbedViews),
PathPrefix: "views/components",
Browse: true,
}))
app.Use("/assets", filesystem.New(filesystem.Config{
Root: http.FS(EmbedViews),
PathPrefix: "views/assets",
Browse: true,
}))
app.Use(middleware.Recovery())
app.Use(middleware.BasicAuth())
if config.AppDebug {
app.Use(logger.New())
}
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Origin, Content-Type, Accept",
}))
if len(config.AppBasicAuthCredential) > 0 {
account := make(map[string]string)
for _, basicAuth := range config.AppBasicAuthCredential {
ba := strings.Split(basicAuth, ":")
if len(ba) != 2 {
log.Fatalln("Basic auth is not valid, please this following format <user>:<secret>")
}
account[ba[0]] = ba[1]
}
app.Use(basicauth.New(basicauth.Config{
Users: account,
}))
}
// Rest
rest.InitRestApp(app, appUsecase)
rest.InitRestSend(app, sendUsecase)
rest.InitRestUser(app, userUsecase)
rest.InitRestMessage(app, messageUsecase)
rest.InitRestGroup(app, groupUsecase)
rest.InitRestNewsletter(app, newsletterUsecase)
app.Get("/", func(c *fiber.Ctx) error {
return c.Render("views/index", fiber.Map{
"AppHost": fmt.Sprintf("%s://%s", c.Protocol(), c.Hostname()),
"AppVersion": config.AppVersion,
"BasicAuthToken": c.UserContext().Value(middleware.AuthorizationValue("BASIC_AUTH")),
"MaxFileSize": humanize.Bytes(uint64(config.WhatsappSettingMaxFileSize)),
"MaxVideoSize": humanize.Bytes(uint64(config.WhatsappSettingMaxVideoSize)),
})
})
websocket.RegisterRoutes(app, appUsecase)
go websocket.RunHub()
// Set auto reconnect to whatsapp server after booting
go helpers.SetAutoConnectAfterBooting(appUsecase)
// Set auto reconnect checking
go helpers.SetAutoReconnectChecking(whatsappCli)
// Start auto flush chat csv
if config.WhatsappChatStorage {
go helpers.StartAutoFlushChatStorage()
}
if err := app.Listen(":" + config.AppPort); err != nil {
log.Fatalln("Failed to start: ", err.Error())
}
}

168
src/cmd/root.go

@ -1,37 +1,47 @@
package cmd
import (
"context"
"embed"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/rest"
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/rest/helpers"
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/rest/middleware"
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/websocket"
domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group"
domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message"
domainNewsletter "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/newsletter"
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send"
domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/services"
"github.com/dustin/go-humanize"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/basicauth"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/template/html/v2"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"github.com/aldinokemal/go-whatsapp-web-multidevice/usecase"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/store/sqlstore"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
var (
EmbedIndex embed.FS
EmbedViews embed.FS
// Whatsapp
whatsappCli *whatsmeow.Client
whatsappDB *sqlstore.Container
// Usecase
appUsecase domainApp.IAppUsecase
sendUsecase domainSend.ISendUsecase
userUsecase domainUser.IUserUsecase
messageUsecase domainMessage.IMessageUsecase
groupUsecase domainGroup.IGroupUsecase
newsletterUsecase domainNewsletter.INewsletterUsecase
)
// rootCmd represents the base command when called without any subcommands
@ -39,16 +49,21 @@ var rootCmd = &cobra.Command{
Short: "Send free whatsapp API",
Long: `This application is from clone https://github.com/aldinokemal/go-whatsapp-web-multidevice,
you can send whatsapp over http api but your whatsapp account have to be multi device version`,
Run: runRest,
}
func init() {
// Load environment variables first
utils.LoadConfig(".")
// Initialize configurations, flag is higher priority than env
initEnvConfig()
time.Local = time.UTC
rootCmd.CompletionOptions.DisableDefaultCmd = true
// Initialize flags first, before any subcommands are added
initFlags()
// Then initialize other components
cobra.OnInitialize(initEnvConfig, initApp)
}
// initEnvConfig loads configuration from environment variables
@ -95,10 +110,7 @@ func initEnvConfig() {
}
}
// initFlags sets up command line flags that override environment variables
func initFlags() {
rootCmd.CompletionOptions.DisableDefaultCmd = true
// Application flags
rootCmd.PersistentFlags().StringVarP(
&config.AppPort,
@ -106,6 +118,7 @@ func initFlags() {
config.AppPort,
"change port number with --port <number> | example: --port=8080",
)
rootCmd.PersistentFlags().BoolVarP(
&config.AppDebug,
"debug", "d",
@ -136,7 +149,7 @@ func initFlags() {
&config.DBURI,
"db-uri", "",
config.DBURI,
`the database uri to store the connection data database uri (by default, we'll use sqlite3 under storages/whatsapp.db). database uri --db-uri <string> | example: --db-uri="file:storages/whatsapp.db?_foreign_keys=off or postgres://user:password@localhost:5432/whatsapp"`,
`the database uri to store the connection data database uri (by default, we'll use sqlite3 under storages/whatsapp.db). database uri --db-uri <string> | example: --db-uri="file:storages/whatsapp.db?_foreign_keys=on or postgres://user:password@localhost:5432/whatsapp"`,
)
// WhatsApp flags
@ -172,108 +185,27 @@ func initFlags() {
)
}
func runRest(_ *cobra.Command, _ []string) {
func initApp() {
if config.AppDebug {
config.WhatsappLogLevel = "DEBUG"
}
// TODO: Init Rest App
//preparing folder if not exist
err := utils.CreateFolder(config.PathQrCode, config.PathSendItems, config.PathStorages, config.PathMedia)
if err != nil {
log.Fatalln(err)
logrus.Errorln(err)
}
engine := html.NewFileSystem(http.FS(EmbedIndex), ".html")
engine.AddFunc("isEnableBasicAuth", func(token any) bool {
return token != nil
})
app := fiber.New(fiber.Config{
Views: engine,
BodyLimit: int(config.WhatsappSettingMaxVideoSize),
})
ctx := context.Background()
whatsappDB = whatsapp.InitWaDB(ctx)
whatsappCli = whatsapp.InitWaCLI(ctx, whatsappDB)
app.Static("/statics", "./statics")
app.Use("/components", filesystem.New(filesystem.Config{
Root: http.FS(EmbedViews),
PathPrefix: "views/components",
Browse: true,
}))
app.Use("/assets", filesystem.New(filesystem.Config{
Root: http.FS(EmbedViews),
PathPrefix: "views/assets",
Browse: true,
}))
app.Use(middleware.Recovery())
app.Use(middleware.BasicAuth())
if config.AppDebug {
app.Use(logger.New())
}
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Origin, Content-Type, Accept",
}))
if len(config.AppBasicAuthCredential) > 0 {
account := make(map[string]string)
for _, basicAuth := range config.AppBasicAuthCredential {
ba := strings.Split(basicAuth, ":")
if len(ba) != 2 {
log.Fatalln("Basic auth is not valid, please this following format <user>:<secret>")
}
account[ba[0]] = ba[1]
}
app.Use(basicauth.New(basicauth.Config{
Users: account,
}))
}
db := whatsapp.InitWaDB()
cli := whatsapp.InitWaCLI(db)
// Service
appService := services.NewAppService(cli, db)
sendService := services.NewSendService(cli, appService)
userService := services.NewUserService(cli)
messageService := services.NewMessageService(cli)
groupService := services.NewGroupService(cli)
newsletterService := services.NewNewsletterService(cli)
// Rest
rest.InitRestApp(app, appService)
rest.InitRestSend(app, sendService)
rest.InitRestUser(app, userService)
rest.InitRestMessage(app, messageService)
rest.InitRestGroup(app, groupService)
rest.InitRestNewsletter(app, newsletterService)
app.Get("/", func(c *fiber.Ctx) error {
return c.Render("views/index", fiber.Map{
"AppHost": fmt.Sprintf("%s://%s", c.Protocol(), c.Hostname()),
"AppVersion": config.AppVersion,
"BasicAuthToken": c.UserContext().Value(middleware.AuthorizationValue("BASIC_AUTH")),
"MaxFileSize": humanize.Bytes(uint64(config.WhatsappSettingMaxFileSize)),
"MaxVideoSize": humanize.Bytes(uint64(config.WhatsappSettingMaxVideoSize)),
})
})
websocket.RegisterRoutes(app, appService)
go websocket.RunHub()
// Set auto reconnect to whatsapp server after booting
go helpers.SetAutoConnectAfterBooting(appService)
// Set auto reconnect checking
go helpers.SetAutoReconnectChecking(cli)
// Start auto flush chat csv
if config.WhatsappChatStorage {
go helpers.StartAutoFlushChatStorage()
}
if err = app.Listen(":" + config.AppPort); err != nil {
log.Fatalln("Failed to start: ", err.Error())
}
// Usecase
appUsecase = usecase.NewAppService(whatsappCli, whatsappDB)
sendUsecase = usecase.NewSendService(whatsappCli, appUsecase)
userUsecase = usecase.NewUserService(whatsappCli)
messageUsecase = usecase.NewMessageService(whatsappCli)
groupUsecase = usecase.NewGroupService(whatsappCli)
newsletterUsecase = usecase.NewNewsletterService(whatsappCli)
}
// Execute adds all child commands to the root command and sets flags appropriately.

5
src/config/settings.go

@ -5,7 +5,7 @@ import (
)
var (
AppVersion = "v5.6.1"
AppVersion = "v6.0.2"
AppPort = "3000"
AppDebug = false
AppOs = "AldinoKemal"
@ -13,6 +13,9 @@ var (
AppBasicAuthCredential []string
AppChatFlushIntervalDays = 7 // Number of days before flushing chat.csv
McpPort = "8080"
McpHost = "localhost"
PathQrCode = "statics/qrcode"
PathSendItems = "statics/senditems"
PathMedia = "statics/media"

2
src/domains/app/app.go

@ -5,7 +5,7 @@ import (
"time"
)
type IAppService interface {
type IAppUsecase interface {
Login(ctx context.Context) (response LoginResponse, err error)
LoginWithCode(ctx context.Context, phoneNumber string) (loginCode string, err error)
Logout(ctx context.Context) (err error)

2
src/domains/group/group.go

@ -7,7 +7,7 @@ import (
"go.mau.fi/whatsmeow"
)
type IGroupService interface {
type IGroupUsecase interface {
JoinGroupWithLink(ctx context.Context, request JoinGroupWithLinkRequest) (groupID string, err error)
LeaveGroup(ctx context.Context, request LeaveGroupRequest) (err error)
CreateGroup(ctx context.Context, request CreateGroupRequest) (groupID string, err error)

2
src/domains/message/message.go

@ -2,7 +2,7 @@ package message
import "context"
type IMessageService interface {
type IMessageUsecase interface {
MarkAsRead(ctx context.Context, request MarkAsReadRequest) (response GenericResponse, err error)
ReactMessage(ctx context.Context, request ReactionRequest) (response GenericResponse, err error)
RevokeMessage(ctx context.Context, request RevokeRequest) (response GenericResponse, err error)

2
src/domains/newsletter/newsletter.go

@ -2,7 +2,7 @@ package newsletter
import "context"
type INewsletterService interface {
type INewsletterUsecase interface {
Unfollow(ctx context.Context, request UnfollowRequest) (err error)
}

2
src/domains/send/send.go

@ -4,7 +4,7 @@ import (
"context"
)
type ISendService interface {
type ISendUsecase interface {
SendText(ctx context.Context, request MessageRequest) (response GenericResponse, err error)
SendImage(ctx context.Context, request ImageRequest) (response GenericResponse, err error)
SendFile(ctx context.Context, request FileRequest) (response GenericResponse, err error)

8
src/domains/user/account.go

@ -73,3 +73,11 @@ type MyListContactsResponseData struct {
type ChangePushNameRequest struct {
PushName string `json:"push_name" form:"push_name"`
}
type CheckRequest struct {
Phone string `json:"phone" query:"phone"`
}
type CheckResponse struct {
IsOnWhatsApp bool `json:"is_on_whatsapp"`
}

3
src/domains/user/user.go

@ -4,7 +4,7 @@ import (
"context"
)
type IUserService interface {
type IUserUsecase interface {
Info(ctx context.Context, request InfoRequest) (response InfoResponse, err error)
Avatar(ctx context.Context, request AvatarRequest) (response AvatarResponse, err error)
ChangeAvatar(ctx context.Context, request ChangeAvatarRequest) (err error)
@ -13,4 +13,5 @@ type IUserService interface {
MyListNewsletter(ctx context.Context) (response MyListNewsletterResponse, err error)
MyPrivacySetting(ctx context.Context) (response MyPrivacySettingResponse, err error)
MyListContacts(ctx context.Context) (response MyListContactsResponse, err error)
IsOnWhatsApp(ctx context.Context, request CheckRequest) (response CheckResponse, err error)
}

22
src/go.mod

@ -7,11 +7,12 @@ require (
github.com/disintegration/imaging v1.6.2
github.com/dustin/go-humanize v1.0.1
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/gofiber/fiber/v2 v2.52.6
github.com/gofiber/fiber/v2 v2.52.8
github.com/gofiber/template/html/v2 v2.1.3
github.com/gofiber/websocket/v2 v2.2.1
github.com/google/uuid v1.6.0
github.com/lib/pq v1.10.9
github.com/mark3labs/mcp-go v0.31.0
github.com/mattn/go-sqlite3 v1.14.28
github.com/sirupsen/logrus v1.9.3
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
@ -19,9 +20,9 @@ require (
github.com/spf13/viper v1.20.1
github.com/stretchr/testify v1.10.0
github.com/valyala/fasthttp v1.62.0
go.mau.fi/libsignal v0.1.2
go.mau.fi/whatsmeow v0.0.0-20250501130609-4c93ee4e6efa
golang.org/x/image v0.27.0
go.mau.fi/libsignal v0.2.0
go.mau.fi/whatsmeow v0.0.0-20250606170101-3afe34f8ab8f
golang.org/x/image v0.28.0
google.golang.org/protobuf v1.36.6
)
@ -51,16 +52,17 @@ require (
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.8.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.mau.fi/util v0.8.6 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.mau.fi/util v0.8.7 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

102
src/go.sum

@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
@ -25,8 +25,6 @@ github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOU
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
@ -34,8 +32,8 @@ github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRi
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
@ -60,6 +58,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4=
github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@ -69,18 +69,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a h1:S+AGcmAESQ0pXCUNnRH7V+bOUIgkSX5qVt2cNKCrm0Q=
github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -96,12 +88,8 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa24bR+2i53WBCz1nW/Pc47oVYauC4=
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@ -112,16 +100,12 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -133,27 +117,19 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw=
github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc=
github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0=
go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE=
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82 h1:AZlDkXHgoQNW4gd2hnTCvPH7hYznmwc3gPaYqGZ5w8A=
go.mau.fi/whatsmeow v0.0.0-20250318233852-06705625cf82/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs=
go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088 h1:ns6nk2NjqdaQnCKrp+Qqwpf+3OI7+nnH56D71+7XzOM=
go.mau.fi/whatsmeow v0.0.0-20250402091807-b0caa1b76088/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs=
go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526 h1:i9w16FdM3zmOWdF5nh1l2MlmE/wK7ulL6rbT02WBBJs=
go.mau.fi/whatsmeow v0.0.0-20250417131650-164ddf482526/go.mod h1:NlPtoLdpX3RnltqCTCZQ6kIUfprqLirtSK1gHvwoNx0=
go.mau.fi/whatsmeow v0.0.0-20250501130609-4c93ee4e6efa h1:+bQKfMtnhX2jVoCSaneH4Ctk51IVT1K2gvjyqfFjVW0=
go.mau.fi/whatsmeow v0.0.0-20250501130609-4c93ee4e6efa/go.mod h1:NlPtoLdpX3RnltqCTCZQ6kIUfprqLirtSK1gHvwoNx0=
go.mau.fi/libsignal v0.2.0 h1:oRXj3OHhEJq51BFEM8/50UZblmWiTYH93hsNTPcbk90=
go.mau.fi/libsignal v0.2.0/go.mod h1:tvjoDsMejgT38CXTXwqaYu8itBiY8O2Mb6biWvZBb9k=
go.mau.fi/util v0.8.7 h1:ywKarPxouJQEEijTs4mPlxC7F4AWEKokEpWc+2TYy6c=
go.mau.fi/util v0.8.7/go.mod h1:j6R3cENakc1f8HpQeFl0N15UiSTcNmIfDBNJUbL71RY=
go.mau.fi/whatsmeow v0.0.0-20250606170101-3afe34f8ab8f h1:8csRM0kOS9nGgT162JFwi3FZ93NPM6fWVf/d5AfTceA=
go.mau.fi/whatsmeow v0.0.0-20250606170101-3afe34f8ab8f/go.mod h1:Qy3L3BNBcnxfrAQ09lmFMa0ItZfg8zl9DzxKrptzfU4=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -162,23 +138,13 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4=
golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@ -193,14 +159,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -222,10 +182,6 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
@ -246,12 +202,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -259,8 +211,6 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

78
src/pkg/whatsapp/init.go → src/infrastructure/whatsapp/init.go

@ -10,9 +10,9 @@ import (
"time"
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/websocket"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/ui/websocket"
"github.com/sirupsen/logrus"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate"
@ -53,11 +53,11 @@ var (
)
// InitWaDB initializes the WhatsApp database connection
func InitWaDB() *sqlstore.Container {
func InitWaDB(ctx context.Context) *sqlstore.Container {
log = waLog.Stdout("Main", config.WhatsappLogLevel, true)
dbLog := waLog.Stdout("Database", config.WhatsappLogLevel, true)
storeContainer, err := initDatabase(dbLog)
storeContainer, err := initDatabase(ctx, dbLog)
if err != nil {
log.Errorf("Database initialization error: %v", err)
panic(pkgError.InternalServerError(fmt.Sprintf("Database initialization error: %v", err)))
@ -67,19 +67,19 @@ func InitWaDB() *sqlstore.Container {
}
// initDatabase creates and returns a database store container based on the configured URI
func initDatabase(dbLog waLog.Logger) (*sqlstore.Container, error) {
func initDatabase(ctx context.Context, dbLog waLog.Logger) (*sqlstore.Container, error) {
if strings.HasPrefix(config.DBURI, "file:") {
return sqlstore.New("sqlite3", config.DBURI, dbLog)
return sqlstore.New(ctx, "sqlite3", config.DBURI, dbLog)
} else if strings.HasPrefix(config.DBURI, "postgres:") {
return sqlstore.New("postgres", config.DBURI, dbLog)
return sqlstore.New(ctx, "postgres", config.DBURI, dbLog)
}
return nil, fmt.Errorf("unknown database type: %s. Currently only sqlite3(file:) and postgres are supported", config.DBURI)
}
// InitWaCLI initializes the WhatsApp client
func InitWaCLI(storeContainer *sqlstore.Container) *whatsmeow.Client {
device, err := storeContainer.GetFirstDevice()
func InitWaCLI(ctx context.Context, storeContainer *sqlstore.Container) *whatsmeow.Client {
device, err := storeContainer.GetFirstDevice(ctx)
if err != nil {
log.Errorf("Failed to get device: %v", err)
panic(err)
@ -99,46 +99,48 @@ func InitWaCLI(storeContainer *sqlstore.Container) *whatsmeow.Client {
cli = whatsmeow.NewClient(device, waLog.Stdout("Client", config.WhatsappLogLevel, true))
cli.EnableAutoReconnect = true
cli.AutoTrustIdentity = true
cli.AddEventHandler(handler)
cli.AddEventHandler(func(rawEvt interface{}) {
handler(ctx, rawEvt)
})
return cli
}
// handler is the main event handler for WhatsApp events
func handler(rawEvt interface{}) {
func handler(ctx context.Context, rawEvt interface{}) {
switch evt := rawEvt.(type) {
case *events.DeleteForMe:
handleDeleteForMe(evt)
handleDeleteForMe(ctx, evt)
case *events.AppStateSyncComplete:
handleAppStateSyncComplete(evt)
handleAppStateSyncComplete(ctx, evt)
case *events.PairSuccess:
handlePairSuccess(evt)
handlePairSuccess(ctx, evt)
case *events.LoggedOut:
handleLoggedOut()
handleLoggedOut(ctx)
case *events.Connected, *events.PushNameSetting:
handleConnectionEvents()
handleConnectionEvents(ctx)
case *events.StreamReplaced:
handleStreamReplaced()
handleStreamReplaced(ctx)
case *events.Message:
handleMessage(evt)
handleMessage(ctx, evt)
case *events.Receipt:
handleReceipt(evt)
handleReceipt(ctx, evt)
case *events.Presence:
handlePresence(evt)
handlePresence(ctx, evt)
case *events.HistorySync:
handleHistorySync(evt)
handleHistorySync(ctx, evt)
case *events.AppState:
handleAppState(evt)
handleAppState(ctx, evt)
}
}
// Event handler functions
func handleDeleteForMe(evt *events.DeleteForMe) {
func handleDeleteForMe(_ context.Context, evt *events.DeleteForMe) {
log.Infof("Deleted message %s for %s", evt.MessageID, evt.SenderJID.String())
}
func handleAppStateSyncComplete(evt *events.AppStateSyncComplete) {
func handleAppStateSyncComplete(_ context.Context, evt *events.AppStateSyncComplete) {
if len(cli.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
if err := cli.SendPresence(types.PresenceAvailable); err != nil {
log.Warnf("Failed to send available presence: %v", err)
@ -148,21 +150,21 @@ func handleAppStateSyncComplete(evt *events.AppStateSyncComplete) {
}
}
func handlePairSuccess(evt *events.PairSuccess) {
func handlePairSuccess(_ context.Context, evt *events.PairSuccess) {
websocket.Broadcast <- websocket.BroadcastMessage{
Code: "LOGIN_SUCCESS",
Message: fmt.Sprintf("Successfully pair with %s", evt.ID.String()),
}
}
func handleLoggedOut() {
func handleLoggedOut(_ context.Context) {
websocket.Broadcast <- websocket.BroadcastMessage{
Code: "LIST_DEVICES",
Result: nil,
}
}
func handleConnectionEvents() {
func handleConnectionEvents(_ context.Context) {
if len(cli.Store.PushName) == 0 {
return
}
@ -176,11 +178,11 @@ func handleConnectionEvents() {
}
}
func handleStreamReplaced() {
func handleStreamReplaced(_ context.Context) {
os.Exit(0)
}
func handleMessage(evt *events.Message) {
func handleMessage(ctx context.Context, evt *events.Message) {
// Log message metadata
metaParts := buildMessageMetaParts(evt)
log.Infof("Received message %s from %s (%s): %+v",
@ -195,13 +197,13 @@ func handleMessage(evt *events.Message) {
utils.RecordMessage(evt.Info.ID, evt.Info.Sender.String(), message)
// Handle image message if present
handleImageMessage(evt)
handleImageMessage(ctx, evt)
// Handle auto-reply if configured
handleAutoReply(evt)
// Forward to webhook if configured
handleWebhookForward(evt)
handleWebhookForward(ctx, evt)
}
func buildMessageMetaParts(evt *events.Message) []string {
@ -221,9 +223,9 @@ func buildMessageMetaParts(evt *events.Message) []string {
return metaParts
}
func handleImageMessage(evt *events.Message) {
func handleImageMessage(ctx context.Context, evt *events.Message) {
if img := evt.Message.GetImageMessage(); img != nil {
if path, err := ExtractMedia(config.PathStorages, img); err != nil {
if path, err := ExtractMedia(ctx, config.PathStorages, img); err != nil {
log.Errorf("Failed to download image: %v", err)
} else {
log.Infof("Image downloaded to %s", path)
@ -244,19 +246,19 @@ func handleAutoReply(evt *events.Message) {
}
}
func handleWebhookForward(evt *events.Message) {
func handleWebhookForward(ctx context.Context, evt *events.Message) {
if len(config.WhatsappWebhook) > 0 &&
!strings.Contains(evt.Info.SourceString(), "broadcast") &&
!isFromMySelf(evt.Info.SourceString()) {
go func(evt *events.Message) {
if err := forwardToWebhook(evt); err != nil {
if err := forwardToWebhook(ctx, evt); err != nil {
logrus.Error("Failed forward to webhook: ", err)
}
}(evt)
}
}
func handleReceipt(evt *events.Receipt) {
func handleReceipt(_ context.Context, evt *events.Receipt) {
if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf {
log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp)
} else if evt.Type == types.ReceiptTypeDelivered {
@ -264,7 +266,7 @@ func handleReceipt(evt *events.Receipt) {
}
}
func handlePresence(evt *events.Presence) {
func handlePresence(_ context.Context, evt *events.Presence) {
if evt.Unavailable {
if evt.LastSeen.IsZero() {
log.Infof("%s is now offline", evt.From)
@ -276,7 +278,7 @@ func handlePresence(evt *events.Presence) {
}
}
func handleHistorySync(evt *events.HistorySync) {
func handleHistorySync(_ context.Context, evt *events.HistorySync) {
id := atomic.AddInt32(&historySyncID, 1)
fileName := fmt.Sprintf("%s/history-%d-%s-%d-%s.json",
config.PathStorages,
@ -303,6 +305,6 @@ func handleHistorySync(evt *events.HistorySync) {
log.Infof("Wrote history sync to %s", fileName)
}
func handleAppState(evt *events.AppState) {
func handleAppState(ctx context.Context, evt *events.AppState) {
log.Debugf("App state event: %+v / %+v", evt.Index, evt.SyncActionValue)
}

5
src/pkg/whatsapp/utils.go → src/infrastructure/whatsapp/utils.go

@ -1,6 +1,7 @@
package whatsapp
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
@ -23,13 +24,13 @@ import (
)
// ExtractMedia is a helper function to extract media from whatsapp
func ExtractMedia(storageLocation string, mediaFile whatsmeow.DownloadableMessage) (extractedMedia ExtractedMedia, err error) {
func ExtractMedia(ctx context.Context, storageLocation string, mediaFile whatsmeow.DownloadableMessage) (extractedMedia ExtractedMedia, err error) {
if mediaFile == nil {
logrus.Info("Skip download because data is nil")
return extractedMedia, nil
}
data, err := cli.Download(mediaFile)
data, err := cli.Download(ctx, mediaFile)
if err != nil {
return extractedMedia, err
}

16
src/pkg/whatsapp/webhook.go → src/infrastructure/whatsapp/webhook.go

@ -17,9 +17,9 @@ import (
)
// forwardToWebhook is a helper function to forward event to webhook url
func forwardToWebhook(evt *events.Message) error {
func forwardToWebhook(ctx context.Context, evt *events.Message) error {
logrus.Info("Forwarding event to webhook:", config.WhatsappWebhook)
payload, err := createPayload(evt)
payload, err := createPayload(ctx, evt)
if err != nil {
return err
}
@ -34,7 +34,7 @@ func forwardToWebhook(evt *events.Message) error {
return nil
}
func createPayload(evt *events.Message) (map[string]interface{}, error) {
func createPayload(ctx context.Context, evt *events.Message) (map[string]interface{}, error) {
message := buildEventMessage(evt)
waReaction := buildEventReaction(evt)
forwarded := buildForwarded(evt)
@ -72,7 +72,7 @@ func createPayload(evt *events.Message) (map[string]interface{}, error) {
}
if audioMedia := evt.Message.GetAudioMessage(); audioMedia != nil {
path, err := ExtractMedia(config.PathMedia, audioMedia)
path, err := ExtractMedia(ctx, config.PathMedia, audioMedia)
if err != nil {
logrus.Errorf("Failed to download audio from %s: %v", evt.Info.SourceString(), err)
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download audio: %v", err))
@ -85,7 +85,7 @@ func createPayload(evt *events.Message) (map[string]interface{}, error) {
}
if documentMedia := evt.Message.GetDocumentMessage(); documentMedia != nil {
path, err := ExtractMedia(config.PathMedia, documentMedia)
path, err := ExtractMedia(ctx, config.PathMedia, documentMedia)
if err != nil {
logrus.Errorf("Failed to download document from %s: %v", evt.Info.SourceString(), err)
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download document: %v", err))
@ -94,7 +94,7 @@ func createPayload(evt *events.Message) (map[string]interface{}, error) {
}
if imageMedia := evt.Message.GetImageMessage(); imageMedia != nil {
path, err := ExtractMedia(config.PathMedia, imageMedia)
path, err := ExtractMedia(ctx, config.PathMedia, imageMedia)
if err != nil {
logrus.Errorf("Failed to download image from %s: %v", evt.Info.SourceString(), err)
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download image: %v", err))
@ -119,7 +119,7 @@ func createPayload(evt *events.Message) (map[string]interface{}, error) {
}
if stickerMedia := evt.Message.GetStickerMessage(); stickerMedia != nil {
path, err := ExtractMedia(config.PathMedia, stickerMedia)
path, err := ExtractMedia(ctx, config.PathMedia, stickerMedia)
if err != nil {
logrus.Errorf("Failed to download sticker from %s: %v", evt.Info.SourceString(), err)
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download sticker: %v", err))
@ -128,7 +128,7 @@ func createPayload(evt *events.Message) (map[string]interface{}, error) {
}
if videoMedia := evt.Message.GetVideoMessage(); videoMedia != nil {
path, err := ExtractMedia(config.PathMedia, videoMedia)
path, err := ExtractMedia(ctx, config.PathMedia, videoMedia)
if err != nil {
logrus.Errorf("Failed to download video from %s: %v", evt.Info.SourceString(), err)
return nil, pkgError.WebhookError(fmt.Sprintf("Failed to download video: %v", err))

338
src/ui/mcp/send.go

@ -0,0 +1,338 @@
package mcp
import (
"context"
"errors"
"fmt"
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type SendHandler struct {
sendService domainSend.ISendUsecase
}
func InitMcpSend(sendService domainSend.ISendUsecase) *SendHandler {
return &SendHandler{
sendService: sendService,
}
}
func (s *SendHandler) AddSendTools(mcpServer *server.MCPServer) {
mcpServer.AddTool(s.toolSendText(), s.handleSendText)
mcpServer.AddTool(s.toolSendContact(), s.handleSendContact)
mcpServer.AddTool(s.toolSendLink(), s.handleSendLink)
mcpServer.AddTool(s.toolSendLocation(), s.handleSendLocation)
mcpServer.AddTool(s.toolSendImage(), s.handleSendImage)
}
func (s *SendHandler) toolSendText() mcp.Tool {
sendTextTool := mcp.NewTool("whatsapp_send_text",
mcp.WithDescription("Send a text message to a WhatsApp contact or group."),
mcp.WithString("phone",
mcp.Required(),
mcp.Description("Phone number or group ID to send message to"),
),
mcp.WithString("message",
mcp.Required(),
mcp.Description("The text message to send"),
),
mcp.WithBoolean("is_forwarded",
mcp.Description("Whether this message is being forwarded (default: false)"),
),
mcp.WithString("reply_message_id",
mcp.Description("Message ID to reply to (optional)"),
),
)
return sendTextTool
}
func (s *SendHandler) handleSendText(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
phone, ok := request.GetArguments()["phone"].(string)
if !ok {
return nil, errors.New("phone must be a string")
}
message, ok := request.GetArguments()["message"].(string)
if !ok {
return nil, errors.New("message must be a string")
}
isForwarded, ok := request.GetArguments()["is_forwarded"].(bool)
if !ok {
isForwarded = false
}
replyMessageId, ok := request.GetArguments()["reply_message_id"].(string)
if !ok {
replyMessageId = ""
}
res, err := s.sendService.SendText(ctx, domainSend.MessageRequest{
Phone: phone,
Message: message,
IsForwarded: isForwarded,
ReplyMessageID: &replyMessageId,
})
if err != nil {
return nil, err
}
return mcp.NewToolResultText(fmt.Sprintf("Message sent successfully with ID %s", res.MessageID)), nil
}
func (s *SendHandler) toolSendContact() mcp.Tool {
sendContactTool := mcp.NewTool("whatsapp_send_contact",
mcp.WithDescription("Send a contact card to a WhatsApp contact or group."),
mcp.WithString("phone",
mcp.Required(),
mcp.Description("Phone number or group ID to send contact to"),
),
mcp.WithString("contact_name",
mcp.Required(),
mcp.Description("Name of the contact to send"),
),
mcp.WithString("contact_phone",
mcp.Required(),
mcp.Description("Phone number of the contact to send"),
),
mcp.WithBoolean("is_forwarded",
mcp.Description("Whether this message is being forwarded (default: false)"),
),
)
return sendContactTool
}
func (s *SendHandler) handleSendContact(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
phone, ok := request.GetArguments()["phone"].(string)
if !ok {
return nil, errors.New("phone must be a string")
}
contactName, ok := request.GetArguments()["contact_name"].(string)
if !ok {
return nil, errors.New("contact_name must be a string")
}
contactPhone, ok := request.GetArguments()["contact_phone"].(string)
if !ok {
return nil, errors.New("contact_phone must be a string")
}
isForwarded, ok := request.GetArguments()["is_forwarded"].(bool)
if !ok {
isForwarded = false
}
res, err := s.sendService.SendContact(ctx, domainSend.ContactRequest{
Phone: phone,
ContactName: contactName,
ContactPhone: contactPhone,
IsForwarded: isForwarded,
})
if err != nil {
return nil, err
}
return mcp.NewToolResultText(fmt.Sprintf("Contact sent successfully with ID %s", res.MessageID)), nil
}
func (s *SendHandler) toolSendLink() mcp.Tool {
sendLinkTool := mcp.NewTool("whatsapp_send_link",
mcp.WithDescription("Send a link with caption to a WhatsApp contact or group."),
mcp.WithString("phone",
mcp.Required(),
mcp.Description("Phone number or group ID to send link to"),
),
mcp.WithString("link",
mcp.Required(),
mcp.Description("URL link to send"),
),
mcp.WithString("caption",
mcp.Required(),
mcp.Description("Caption or description for the link"),
),
mcp.WithBoolean("is_forwarded",
mcp.Description("Whether this message is being forwarded (default: false)"),
),
)
return sendLinkTool
}
func (s *SendHandler) handleSendLink(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
phone, ok := request.GetArguments()["phone"].(string)
if !ok {
return nil, errors.New("phone must be a string")
}
link, ok := request.GetArguments()["link"].(string)
if !ok {
return nil, errors.New("link must be a string")
}
caption, ok := request.GetArguments()["caption"].(string)
if !ok {
caption = ""
}
isForwarded, ok := request.GetArguments()["is_forwarded"].(bool)
if !ok {
isForwarded = false
}
res, err := s.sendService.SendLink(ctx, domainSend.LinkRequest{
Phone: phone,
Link: link,
Caption: caption,
IsForwarded: isForwarded,
})
if err != nil {
return nil, err
}
return mcp.NewToolResultText(fmt.Sprintf("Link sent successfully with ID %s", res.MessageID)), nil
}
func (s *SendHandler) toolSendLocation() mcp.Tool {
sendLocationTool := mcp.NewTool("whatsapp_send_location",
mcp.WithDescription("Send a location coordinates to a WhatsApp contact or group."),
mcp.WithString("phone",
mcp.Required(),
mcp.Description("Phone number or group ID to send location to"),
),
mcp.WithString("latitude",
mcp.Required(),
mcp.Description("Latitude coordinate (as string)"),
),
mcp.WithString("longitude",
mcp.Required(),
mcp.Description("Longitude coordinate (as string)"),
),
mcp.WithBoolean("is_forwarded",
mcp.Description("Whether this message is being forwarded (default: false)"),
),
)
return sendLocationTool
}
func (s *SendHandler) handleSendLocation(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
phone, ok := request.GetArguments()["phone"].(string)
if !ok {
return nil, errors.New("phone must be a string")
}
latitude, ok := request.GetArguments()["latitude"].(string)
if !ok {
return nil, errors.New("latitude must be a string")
}
longitude, ok := request.GetArguments()["longitude"].(string)
if !ok {
return nil, errors.New("longitude must be a string")
}
isForwarded, ok := request.GetArguments()["is_forwarded"].(bool)
if !ok {
isForwarded = false
}
res, err := s.sendService.SendLocation(ctx, domainSend.LocationRequest{
Phone: phone,
Latitude: latitude,
Longitude: longitude,
IsForwarded: isForwarded,
})
if err != nil {
return nil, err
}
return mcp.NewToolResultText(fmt.Sprintf("Location sent successfully with ID %s", res.MessageID)), nil
}
func (s *SendHandler) toolSendImage() mcp.Tool {
sendImageTool := mcp.NewTool("whatsapp_send_image",
mcp.WithDescription("Send an image to a WhatsApp contact or group."),
mcp.WithString("phone",
mcp.Required(),
mcp.Description("Phone number or group ID to send image to"),
),
mcp.WithString("image_url",
mcp.Description("URL of the image to send"),
),
mcp.WithString("caption",
mcp.Description("Caption or description for the image"),
),
mcp.WithBoolean("view_once",
mcp.Description("Whether this image should be viewed only once (default: false)"),
),
mcp.WithBoolean("compress",
mcp.Description("Whether to compress the image (default: true)"),
),
mcp.WithBoolean("is_forwarded",
mcp.Description("Whether this message is being forwarded (default: false)"),
),
)
return sendImageTool
}
func (s *SendHandler) handleSendImage(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
phone, ok := request.GetArguments()["phone"].(string)
if !ok {
return nil, errors.New("phone must be a string")
}
imageURL, imageURLOk := request.GetArguments()["image_url"].(string)
if !imageURLOk {
return nil, errors.New("image_url must be a string")
}
caption, ok := request.GetArguments()["caption"].(string)
if !ok {
caption = ""
}
viewOnce, ok := request.GetArguments()["view_once"].(bool)
if !ok {
viewOnce = false
}
compress, ok := request.GetArguments()["compress"].(bool)
if !ok {
compress = true
}
isForwarded, ok := request.GetArguments()["is_forwarded"].(bool)
if !ok {
isForwarded = false
}
// Create image request
imageRequest := domainSend.ImageRequest{
Phone: phone,
Caption: caption,
ViewOnce: viewOnce,
Compress: compress,
IsForwarded: isForwarded,
}
if imageURLOk && imageURL != "" {
imageRequest.ImageURL = &imageURL
}
res, err := s.sendService.SendImage(ctx, imageRequest)
if err != nil {
return nil, err
}
return mcp.NewToolResultText(fmt.Sprintf("Image sent successfully with ID %s", res.MessageID)), nil
}

5
src/internal/rest/app.go → src/ui/rest/app.go

@ -2,16 +2,17 @@ package rest
import (
"fmt"
domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/gofiber/fiber/v2"
)
type App struct {
Service domainApp.IAppService
Service domainApp.IAppUsecase
}
func InitRestApp(app *fiber.App, service domainApp.IAppService) App {
func InitRestApp(app *fiber.App, service domainApp.IAppUsecase) App {
rest := App{Service: service}
app.Get("/app/login", rest.Login)
app.Get("/app/login-with-code", rest.LoginWithCode)

6
src/internal/rest/group.go → src/ui/rest/group.go

@ -4,17 +4,17 @@ import (
"fmt"
domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/gofiber/fiber/v2"
"go.mau.fi/whatsmeow"
)
type Group struct {
Service domainGroup.IGroupService
Service domainGroup.IGroupUsecase
}
func InitRestGroup(app *fiber.App, service domainGroup.IGroupService) Group {
func InitRestGroup(app *fiber.App, service domainGroup.IGroupUsecase) Group {
rest := Group{Service: service}
app.Post("/group", rest.CreateGroup)
app.Post("/group/join-with-link", rest.JoinGroupWithLink)

7
src/internal/rest/helpers/common.go → src/ui/rest/helpers/common.go

@ -2,13 +2,14 @@ package helpers
import (
"context"
domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
"go.mau.fi/whatsmeow"
"mime/multipart"
"time"
domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
"go.mau.fi/whatsmeow"
)
func SetAutoConnectAfterBooting(service domainApp.IAppService) {
func SetAutoConnectAfterBooting(service domainApp.IAppUsecase) {
time.Sleep(2 * time.Second)
_ = service.Reconnect(context.Background())
}

0
src/internal/rest/helpers/flushChatCsv.go → src/ui/rest/helpers/flushChatCsv.go

6
src/internal/rest/message.go → src/ui/rest/message.go

@ -2,16 +2,16 @@ package rest
import (
domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/gofiber/fiber/v2"
)
type Message struct {
Service domainMessage.IMessageService
Service domainMessage.IMessageUsecase
}
func InitRestMessage(app *fiber.App, service domainMessage.IMessageService) Message {
func InitRestMessage(app *fiber.App, service domainMessage.IMessageUsecase) Message {
rest := Message{Service: service}
app.Post("/message/:message_id/reaction", rest.ReactMessage)
app.Post("/message/:message_id/revoke", rest.RevokeMessage)

0
src/internal/rest/middleware/basicauth.go → src/ui/rest/middleware/basicauth.go

0
src/internal/rest/middleware/recovery.go → src/ui/rest/middleware/recovery.go

4
src/internal/rest/newsletter.go → src/ui/rest/newsletter.go

@ -7,10 +7,10 @@ import (
)
type Newsletter struct {
Service domainNewsletter.INewsletterService
Service domainNewsletter.INewsletterUsecase
}
func InitRestNewsletter(app *fiber.App, service domainNewsletter.INewsletterService) Newsletter {
func InitRestNewsletter(app *fiber.App, service domainNewsletter.INewsletterUsecase) Newsletter {
rest := Newsletter{Service: service}
app.Post("/newsletter/unfollow", rest.Unfollow)
return rest

6
src/internal/rest/send.go → src/ui/rest/send.go

@ -2,16 +2,16 @@ package rest
import (
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/gofiber/fiber/v2"
)
type Send struct {
Service domainSend.ISendService
Service domainSend.ISendUsecase
}
func InitRestSend(app *fiber.App, service domainSend.ISendService) Send {
func InitRestSend(app *fiber.App, service domainSend.ISendUsecase) Send {
rest := Send{Service: service}
app.Post("/send/message", rest.SendText)
app.Post("/send/image", rest.SendImage)

23
src/internal/rest/user.go → src/ui/rest/user.go

@ -2,16 +2,16 @@ package rest
import (
domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/gofiber/fiber/v2"
)
type User struct {
Service domainUser.IUserService
Service domainUser.IUserUsecase
}
func InitRestUser(app *fiber.App, service domainUser.IUserService) User {
func InitRestUser(app *fiber.App, service domainUser.IUserUsecase) User {
rest := User{Service: service}
app.Get("/user/info", rest.UserInfo)
app.Get("/user/avatar", rest.UserAvatar)
@ -21,6 +21,7 @@ func InitRestUser(app *fiber.App, service domainUser.IUserService) User {
app.Get("/user/my/groups", rest.UserMyListGroups)
app.Get("/user/my/newsletters", rest.UserMyListNewsletter)
app.Get("/user/my/contacts", rest.UserMyListContacts)
app.Get("/user/check", rest.UserCheck)
return rest
}
@ -141,3 +142,19 @@ func (controller *User) UserChangePushName(c *fiber.Ctx) error {
Message: "Success change push name",
})
}
func (controller *User) UserCheck(c *fiber.Ctx) error {
var request domainUser.CheckRequest
err := c.QueryParser(&request)
utils.PanicIfNeeded(err)
response, err := controller.Service.IsOnWhatsApp(c.UserContext(), request)
utils.PanicIfNeeded(err)
return c.JSON(utils.ResponseData{
Status: 200,
Code: "SUCCESS",
Message: "Success check user",
Results: response,
})
}

2
src/internal/websocket/websocket.go → src/ui/websocket/websocket.go

@ -76,7 +76,7 @@ func RunHub() {
}
}
func RegisterRoutes(app *fiber.App, service domainApp.IAppService) {
func RegisterRoutes(app *fiber.App, service domainApp.IAppUsecase) {
app.Use("/ws", func(c *fiber.Ctx) error {
if websocket.IsWebSocketUpgrade(c) {
return c.Next()

16
src/services/app.go → src/usecase/app.go

@ -1,4 +1,4 @@
package services
package usecase
import (
"context"
@ -26,7 +26,7 @@ type serviceApp struct {
db *sqlstore.Container
}
func NewAppService(waCli *whatsmeow.Client, db *sqlstore.Container) domainApp.IAppService {
func NewAppService(waCli *whatsmeow.Client, db *sqlstore.Container) domainApp.IAppUsecase {
return &serviceApp{
WaCli: waCli,
db: db,
@ -107,7 +107,7 @@ func (service serviceApp) LoginWithCode(ctx context.Context, phoneNumber string)
// reconnect first
_ = service.Reconnect(ctx)
loginCode, err = service.WaCli.PairPhone(phoneNumber, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
loginCode, err = service.WaCli.PairPhone(ctx, phoneNumber, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
if err != nil {
logrus.Errorf("Error when pairing phone: %s", err.Error())
return loginCode, err
@ -117,7 +117,7 @@ func (service serviceApp) LoginWithCode(ctx context.Context, phoneNumber string)
return loginCode, nil
}
func (service serviceApp) Logout(_ context.Context) (err error) {
func (service serviceApp) Logout(ctx context.Context) (err error) {
// delete history
files, err := filepath.Glob(fmt.Sprintf("./%s/history-*", config.PathStorages))
if err != nil {
@ -158,7 +158,7 @@ func (service serviceApp) Logout(_ context.Context) (err error) {
}
}
err = service.WaCli.Logout()
err = service.WaCli.Logout(ctx)
return
}
@ -172,7 +172,7 @@ func (service serviceApp) FirstDevice(ctx context.Context) (response domainApp.D
return response, pkgError.ErrWaCLI
}
devices, err := service.db.GetFirstDevice()
devices, err := service.db.GetFirstDevice(ctx)
if err != nil {
return response, err
}
@ -187,12 +187,12 @@ func (service serviceApp) FirstDevice(ctx context.Context) (response domainApp.D
return response, nil
}
func (service serviceApp) FetchDevices(_ context.Context) (response []domainApp.DevicesResponse, err error) {
func (service serviceApp) FetchDevices(ctx context.Context) (response []domainApp.DevicesResponse, err error) {
if service.WaCli == nil {
return response, pkgError.ErrWaCLI
}
devices, err := service.db.GetAllDevices()
devices, err := service.db.GetAllDevices(ctx)
if err != nil {
return nil, err
}

24
src/services/group.go → src/usecase/group.go

@ -1,4 +1,4 @@
package services
package usecase
import (
"context"
@ -6,24 +6,24 @@ import (
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
domainGroup "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/group"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
)
type groupService struct {
type serviceGroup struct {
WaCli *whatsmeow.Client
}
func NewGroupService(waCli *whatsmeow.Client) domainGroup.IGroupService {
return &groupService{
func NewGroupService(waCli *whatsmeow.Client) domainGroup.IGroupUsecase {
return &serviceGroup{
WaCli: waCli,
}
}
func (service groupService) JoinGroupWithLink(ctx context.Context, request domainGroup.JoinGroupWithLinkRequest) (groupID string, err error) {
func (service serviceGroup) JoinGroupWithLink(ctx context.Context, request domainGroup.JoinGroupWithLinkRequest) (groupID string, err error) {
if err = validations.ValidateJoinGroupWithLink(ctx, request); err != nil {
return groupID, err
}
@ -36,7 +36,7 @@ func (service groupService) JoinGroupWithLink(ctx context.Context, request domai
return jid.String(), nil
}
func (service groupService) LeaveGroup(ctx context.Context, request domainGroup.LeaveGroupRequest) (err error) {
func (service serviceGroup) LeaveGroup(ctx context.Context, request domainGroup.LeaveGroupRequest) (err error) {
if err = validations.ValidateLeaveGroup(ctx, request); err != nil {
return err
}
@ -49,7 +49,7 @@ func (service groupService) LeaveGroup(ctx context.Context, request domainGroup.
return service.WaCli.LeaveGroup(JID)
}
func (service groupService) CreateGroup(ctx context.Context, request domainGroup.CreateGroupRequest) (groupID string, err error) {
func (service serviceGroup) CreateGroup(ctx context.Context, request domainGroup.CreateGroupRequest) (groupID string, err error) {
if err = validations.ValidateCreateGroup(ctx, request); err != nil {
return groupID, err
}
@ -75,7 +75,7 @@ func (service groupService) CreateGroup(ctx context.Context, request domainGroup
return groupInfo.JID.String(), nil
}
func (service groupService) ManageParticipant(ctx context.Context, request domainGroup.ParticipantRequest) (result []domainGroup.ParticipantStatus, err error) {
func (service serviceGroup) ManageParticipant(ctx context.Context, request domainGroup.ParticipantRequest) (result []domainGroup.ParticipantStatus, err error) {
if err = validations.ValidateParticipant(ctx, request); err != nil {
return result, err
}
@ -115,7 +115,7 @@ func (service groupService) ManageParticipant(ctx context.Context, request domai
return result, nil
}
func (service groupService) GetGroupRequestParticipants(ctx context.Context, request domainGroup.GetGroupRequestParticipantsRequest) (result []domainGroup.GetGroupRequestParticipantsResponse, err error) {
func (service serviceGroup) GetGroupRequestParticipants(ctx context.Context, request domainGroup.GetGroupRequestParticipantsRequest) (result []domainGroup.GetGroupRequestParticipantsResponse, err error) {
if err = validations.ValidateGetGroupRequestParticipants(ctx, request); err != nil {
return result, err
}
@ -140,7 +140,7 @@ func (service groupService) GetGroupRequestParticipants(ctx context.Context, req
return result, nil
}
func (service groupService) ManageGroupRequestParticipants(ctx context.Context, request domainGroup.GroupRequestParticipantsRequest) (result []domainGroup.ParticipantStatus, err error) {
func (service serviceGroup) ManageGroupRequestParticipants(ctx context.Context, request domainGroup.GroupRequestParticipantsRequest) (result []domainGroup.ParticipantStatus, err error) {
if err = validations.ValidateManageGroupRequestParticipants(ctx, request); err != nil {
return result, err
}
@ -179,7 +179,7 @@ func (service groupService) ManageGroupRequestParticipants(ctx context.Context,
return result, nil
}
func (service groupService) participantToJID(participants []string) ([]types.JID, error) {
func (service serviceGroup) participantToJID(participants []string) ([]types.JID, error) {
var participantsJID []types.JID
for _, participant := range participants {
formattedParticipant := participant + config.WhatsappTypeUser

10
src/services/message.go → src/usecase/message.go

@ -1,4 +1,4 @@
package services
package usecase
import (
"context"
@ -6,7 +6,7 @@ import (
"time"
domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
"github.com/sirupsen/logrus"
"go.mau.fi/whatsmeow"
@ -22,7 +22,7 @@ type serviceMessage struct {
WaCli *whatsmeow.Client
}
func NewMessageService(waCli *whatsmeow.Client) domainMessage.IMessageService {
func NewMessageService(waCli *whatsmeow.Client) domainMessage.IMessageUsecase {
return &serviceMessage{
WaCli: waCli,
}
@ -131,7 +131,7 @@ func (service serviceMessage) DeleteMessage(ctx context.Context, request domainM
}},
}
if err = service.WaCli.SendAppState(patchInfo); err != nil {
if err = service.WaCli.SendAppState(ctx, patchInfo); err != nil {
return err
}
return nil
@ -176,7 +176,7 @@ func (service serviceMessage) StarMessage(ctx context.Context, request domainMes
patchInfo := appstate.BuildStar(dataWaRecipient.ToNonAD(), *service.WaCli.Store.ID, request.MessageID, isFromMe, request.IsStarred)
if err = service.WaCli.SendAppState(patchInfo); err != nil {
if err = service.WaCli.SendAppState(ctx, patchInfo); err != nil {
return err
}
return nil

13
src/services/newsletter.go → src/usecase/newsletter.go

@ -1,24 +1,25 @@
package services
package usecase
import (
"context"
domainNewsletter "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/newsletter"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
"go.mau.fi/whatsmeow"
)
type newsletterService struct {
type serviceNewsletter struct {
WaCli *whatsmeow.Client
}
func NewNewsletterService(waCli *whatsmeow.Client) domainNewsletter.INewsletterService {
return &newsletterService{
func NewNewsletterService(waCli *whatsmeow.Client) domainNewsletter.INewsletterUsecase {
return &serviceNewsletter{
WaCli: waCli,
}
}
func (service newsletterService) Unfollow(ctx context.Context, request domainNewsletter.UnfollowRequest) (err error) {
func (service serviceNewsletter) Unfollow(ctx context.Context, request domainNewsletter.UnfollowRequest) (err error) {
if err = validations.ValidateUnfollowNewsletter(ctx, request); err != nil {
return err
}

10
src/services/send.go → src/usecase/send.go

@ -1,4 +1,4 @@
package services
package usecase
import (
"context"
@ -10,10 +10,10 @@ import (
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
"github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send"
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/rest/helpers"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/ui/rest/helpers"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
"github.com/disintegration/imaging"
fiberUtils "github.com/gofiber/fiber/v2/utils"
@ -27,10 +27,10 @@ import (
type serviceSend struct {
WaCli *whatsmeow.Client
appService app.IAppService
appService app.IAppUsecase
}
func NewSendService(waCli *whatsmeow.Client, appService app.IAppService) domainSend.ISendService {
func NewSendService(waCli *whatsmeow.Client, appService app.IAppUsecase) domainSend.ISendUsecase {
return &serviceSend{
WaCli: waCli,
appService: appService,

42
src/services/user.go → src/usecase/user.go

@ -1,4 +1,4 @@
package services
package usecase
import (
"bytes"
@ -9,8 +9,8 @@ import (
"time"
domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user"
"github.com/aldinokemal/go-whatsapp-web-multidevice/infrastructure/whatsapp"
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp"
"github.com/aldinokemal/go-whatsapp-web-multidevice/validations"
"github.com/disintegration/imaging"
"go.mau.fi/whatsmeow"
@ -18,17 +18,17 @@ import (
"go.mau.fi/whatsmeow/types"
)
type userService struct {
type serviceUser struct {
WaCli *whatsmeow.Client
}
func NewUserService(waCli *whatsmeow.Client) domainUser.IUserService {
return &userService{
func NewUserService(waCli *whatsmeow.Client) domainUser.IUserUsecase {
return &serviceUser{
WaCli: waCli,
}
}
func (service userService) Info(ctx context.Context, request domainUser.InfoRequest) (response domainUser.InfoResponse, err error) {
func (service serviceUser) Info(ctx context.Context, request domainUser.InfoRequest) (response domainUser.InfoResponse, err error) {
err = validations.ValidateUserInfo(ctx, request)
if err != nil {
return response, err
@ -71,7 +71,7 @@ func (service userService) Info(ctx context.Context, request domainUser.InfoRequ
return response, nil
}
func (service userService) Avatar(ctx context.Context, request domainUser.AvatarRequest) (response domainUser.AvatarResponse, err error) {
func (service serviceUser) Avatar(ctx context.Context, request domainUser.AvatarRequest) (response domainUser.AvatarResponse, err error) {
chanResp := make(chan domainUser.AvatarResponse)
chanErr := make(chan error)
@ -118,7 +118,7 @@ func (service userService) Avatar(ctx context.Context, request domainUser.Avatar
}
func (service userService) MyListGroups(_ context.Context) (response domainUser.MyListGroupsResponse, err error) {
func (service serviceUser) MyListGroups(_ context.Context) (response domainUser.MyListGroupsResponse, err error) {
whatsapp.MustLogin(service.WaCli)
groups, err := service.WaCli.GetJoinedGroups()
@ -132,7 +132,7 @@ func (service userService) MyListGroups(_ context.Context) (response domainUser.
return response, nil
}
func (service userService) MyListNewsletter(_ context.Context) (response domainUser.MyListNewsletterResponse, err error) {
func (service serviceUser) MyListNewsletter(_ context.Context) (response domainUser.MyListNewsletterResponse, err error) {
whatsapp.MustLogin(service.WaCli)
datas, err := service.WaCli.GetSubscribedNewsletters()
@ -146,10 +146,10 @@ func (service userService) MyListNewsletter(_ context.Context) (response domainU
return response, nil
}
func (service userService) MyPrivacySetting(_ context.Context) (response domainUser.MyPrivacySettingResponse, err error) {
func (service serviceUser) MyPrivacySetting(ctx context.Context) (response domainUser.MyPrivacySettingResponse, err error) {
whatsapp.MustLogin(service.WaCli)
resp, err := service.WaCli.TryFetchPrivacySettings(true)
resp, err := service.WaCli.TryFetchPrivacySettings(ctx, true)
if err != nil {
return
}
@ -161,10 +161,10 @@ func (service userService) MyPrivacySetting(_ context.Context) (response domainU
return response, nil
}
func (service userService) MyListContacts(ctx context.Context) (response domainUser.MyListContactsResponse, err error) {
func (service serviceUser) MyListContacts(ctx context.Context) (response domainUser.MyListContactsResponse, err error) {
whatsapp.MustLogin(service.WaCli)
contacts, err := service.WaCli.Store.Contacts.GetAllContacts()
contacts, err := service.WaCli.Store.Contacts.GetAllContacts(ctx)
if err != nil {
return
}
@ -179,7 +179,7 @@ func (service userService) MyListContacts(ctx context.Context) (response domainU
return response, nil
}
func (service userService) ChangeAvatar(ctx context.Context, request domainUser.ChangeAvatarRequest) (err error) {
func (service serviceUser) ChangeAvatar(ctx context.Context, request domainUser.ChangeAvatarRequest) (err error) {
whatsapp.MustLogin(service.WaCli)
file, err := request.Avatar.Open()
@ -233,12 +233,22 @@ func (service userService) ChangeAvatar(ctx context.Context, request domainUser.
return nil
}
func (service userService) ChangePushName(ctx context.Context, request domainUser.ChangePushNameRequest) (err error) {
func (service serviceUser) ChangePushName(ctx context.Context, request domainUser.ChangePushNameRequest) (err error) {
whatsapp.MustLogin(service.WaCli)
err = service.WaCli.SendAppState(appstate.BuildSettingPushName(request.PushName))
err = service.WaCli.SendAppState(ctx, appstate.BuildSettingPushName(request.PushName))
if err != nil {
return err
}
return nil
}
func (service serviceUser) IsOnWhatsApp(ctx context.Context, request domainUser.CheckRequest) (response domainUser.CheckResponse, err error) {
whatsapp.MustLogin(service.WaCli)
whatsapp.SanitizePhone(&request.Phone)
response.IsOnWhatsApp = whatsapp.IsOnWhatsapp(service.WaCli, request.Phone)
return response, nil
}

95
src/views/components/AccountUserCheck.js

@ -0,0 +1,95 @@
import FormRecipient from "./generic/FormRecipient.js";
export default {
name: 'AccountUserCheck',
components: {
FormRecipient
},
data() {
return {
type: window.TYPEUSER,
phone: '',
isOnWhatsApp: null,
loading: false,
}
},
computed: {
phone_id() {
return this.phone + this.type;
}
},
methods: {
async openModal() {
this.handleReset();
$('#modalUserCheck').modal('show');
},
isValidForm() {
return this.phone.trim() !== '';
},
async handleSubmit() {
if (!this.isValidForm() || this.loading) {
return;
}
try {
await this.submitApi();
showSuccessInfo("Check completed")
} catch (err) {
showErrorInfo(err)
}
},
async submitApi() {
this.loading = true;
try {
let response = await window.http.get(`/user/check?phone=${this.phone_id}`)
this.isOnWhatsApp = response.data.results.is_on_whatsapp;
} catch (error) {
if (error.response) {
throw new Error(error.response.data.message);
}
throw new Error(error.message);
} finally {
this.loading = false;
}
},
handleReset() {
this.phone = '';
this.isOnWhatsApp = null;
this.type = window.TYPEUSER;
}
},
template: `
<div class="olive card" @click="openModal" style="cursor: pointer;">
<div class="content">
<a class="ui olive right ribbon label">Account</a>
<div class="header">User Check</div>
<div class="description">
Check if a user is on WhatsApp
</div>
</div>
</div>
<div class="ui small modal" id="modalUserCheck">
<i class="close icon"></i>
<div class="header">
Check if User is on WhatsApp
</div>
<div class="content">
<form class="ui form">
<FormRecipient v-model:type="type" v-model:phone="phone"/>
<button type="button" class="ui primary button" :class="{'loading': loading, 'disabled': !this.isValidForm() || this.loading}"
@click.prevent="handleSubmit">
Check
</button>
</form>
<div v-if="isOnWhatsApp !== null" class="ui message" :class="isOnWhatsApp ? 'positive' : 'negative'">
<div class="header">
<i :class="isOnWhatsApp ? 'check circle icon' : 'times circle icon'"></i>
{{ isOnWhatsApp ? 'User is on WhatsApp' : 'User is not on WhatsApp' }}
</div>
<p>Phone: {{ phone_id }}</p>
</div>
</div>
</div>
`
}

4
src/views/index.html

@ -161,6 +161,7 @@
<account-user-info></account-user-info>
<account-privacy></account-privacy>
<account-contact></account-contact>
<account-user-check></account-user-check>
</div>
</div>
@ -225,6 +226,7 @@
import AccountUserInfo from "./components/AccountUserInfo.js";
import AccountPrivacy from "./components/AccountPrivacy.js";
import AccountContact from "./components/AccountContact.js";
import AccountUserCheck from "./components/AccountUserCheck.js";
const showErrorInfo = (message) => {
$('body').toast({
@ -263,7 +265,7 @@
MessageDelete, MessageUpdate, MessageReact, MessageRevoke,
GroupList, GroupCreate, GroupJoinWithLink, GroupAddParticipants,
NewsletterList,
AccountAvatar, AccountUserInfo, AccountPrivacy, AccountChangeAvatar, AccountContact, AccountChangePushName
AccountAvatar, AccountUserInfo, AccountPrivacy, AccountChangeAvatar, AccountContact, AccountChangePushName, AccountUserCheck
},
delimiters: ['[[', ']]'],
data() {

Loading…
Cancel
Save