diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc new file mode 100644 index 0000000..5af0483 --- /dev/null +++ b/.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 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..66a439b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.idea +.vscode +.git +.gitignore +.env +.env.local +.env.development +.env.test +.env.production +docker diff --git a/docker/golang.Dockerfile b/docker/golang.Dockerfile index a8bd640..7fea96d 100644 --- a/docker/golang.Dockerfile +++ b/docker/golang.Dockerfile @@ -20,4 +20,6 @@ WORKDIR /app # Copy compiled from builder. COPY --from=builder /app/whatsapp /app/whatsapp # Run the binary. -ENTRYPOINT ["/app/whatsapp"] \ No newline at end of file +ENTRYPOINT ["/app/whatsapp"] + +CMD [ "rest" ] \ No newline at end of file diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 3b34482..53d8317 100644 --- a/docs/openapi.yaml +++ b/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: - type: string - example: '6281234567890' - description: The participant's WhatsApp ID to approve + participants: + type: array + items: + type: string + 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: - type: string - example: '6281234567890' - description: The participant's WhatsApp ID to reject + participants: + type: array + items: + type: string + example: ['6281234567890'] + description: Array of participant WhatsApp IDs to reject required: - group_id - - participant_id + - participants responses: '200': description: OK @@ -2003,4 +2169,19 @@ components: requested_at: type: string format: date-time - example: "2024-10-11T21:27:29+07:00" \ No newline at end of file + 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 \ No newline at end of file diff --git a/readme.md b/readme.md index 7c25343..2136097 100644 --- a/readme.md +++ b/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 ` rest` instead of `` + - for example: `./whatsapp rest` instead of ~~./whatsapp~~ + - For MCP mode, you need to run ` 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. diff --git a/src/.env.example b/src/.env.example index 4c5e126..80a226a 100644 --- a/src/.env.example +++ b/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" diff --git a/src/cmd/mcp.go b/src/cmd/mcp.go new file mode 100644 index 0000000..7f0d846 --- /dev/null +++ b/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) + } +} diff --git a/src/cmd/rest.go b/src/cmd/rest.go new file mode 100644 index 0000000..9189d30 --- /dev/null +++ b/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 :") + } + 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()) + } +} diff --git a/src/cmd/root.go b/src/cmd/root.go index 42d42a2..1f76819 100644 --- a/src/cmd/root.go +++ b/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 | 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 | 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 | 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), - }) - - 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, - })) + ctx := context.Background() + whatsappDB = whatsapp.InitWaDB(ctx) + whatsappCli = whatsapp.InitWaCLI(ctx, whatsappDB) - 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 :") - } - 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. diff --git a/src/config/settings.go b/src/config/settings.go index e5aaa6f..053f550 100644 --- a/src/config/settings.go +++ b/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" diff --git a/src/domains/app/app.go b/src/domains/app/app.go index dbe3bb3..6325ed7 100644 --- a/src/domains/app/app.go +++ b/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) diff --git a/src/domains/group/group.go b/src/domains/group/group.go index 36e8db4..b6fd437 100644 --- a/src/domains/group/group.go +++ b/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) diff --git a/src/domains/message/message.go b/src/domains/message/message.go index a2db0ad..6a0f445 100644 --- a/src/domains/message/message.go +++ b/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) diff --git a/src/domains/newsletter/newsletter.go b/src/domains/newsletter/newsletter.go index 16b1452..05a1f7c 100644 --- a/src/domains/newsletter/newsletter.go +++ b/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) } diff --git a/src/domains/send/send.go b/src/domains/send/send.go index 9e0be5e..643f933 100644 --- a/src/domains/send/send.go +++ b/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) diff --git a/src/domains/user/account.go b/src/domains/user/account.go index c0fb6b5..a60090e 100644 --- a/src/domains/user/account.go +++ b/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"` +} diff --git a/src/domains/user/user.go b/src/domains/user/user.go index ef78619..fc984eb 100644 --- a/src/domains/user/user.go +++ b/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) } diff --git a/src/go.mod b/src/go.mod index 9cd323f..e2aded4 100644 --- a/src/go.mod +++ b/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 ) diff --git a/src/go.sum b/src/go.sum index 7693be3..9a954eb 100644 --- a/src/go.sum +++ b/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= diff --git a/src/pkg/whatsapp/init.go b/src/infrastructure/whatsapp/init.go similarity index 78% rename from src/pkg/whatsapp/init.go rename to src/infrastructure/whatsapp/init.go index f5ecc4f..1190798 100644 --- a/src/pkg/whatsapp/init.go +++ b/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) } diff --git a/src/pkg/whatsapp/utils.go b/src/infrastructure/whatsapp/utils.go similarity index 98% rename from src/pkg/whatsapp/utils.go rename to src/infrastructure/whatsapp/utils.go index a4e84ca..2205126 100644 --- a/src/pkg/whatsapp/utils.go +++ b/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 } diff --git a/src/pkg/whatsapp/webhook.go b/src/infrastructure/whatsapp/webhook.go similarity index 90% rename from src/pkg/whatsapp/webhook.go rename to src/infrastructure/whatsapp/webhook.go index 4529a34..953fb25 100644 --- a/src/pkg/whatsapp/webhook.go +++ b/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)) diff --git a/src/ui/mcp/send.go b/src/ui/mcp/send.go new file mode 100644 index 0000000..6f7edfc --- /dev/null +++ b/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 +} diff --git a/src/internal/rest/app.go b/src/ui/rest/app.go similarity index 95% rename from src/internal/rest/app.go rename to src/ui/rest/app.go index c72cde0..73f3f61 100644 --- a/src/internal/rest/app.go +++ b/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) diff --git a/src/internal/rest/group.go b/src/ui/rest/group.go similarity index 96% rename from src/internal/rest/group.go rename to src/ui/rest/group.go index b365862..c56d5ca 100644 --- a/src/internal/rest/group.go +++ b/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) diff --git a/src/internal/rest/helpers/common.go b/src/ui/rest/helpers/common.go similarity index 91% rename from src/internal/rest/helpers/common.go rename to src/ui/rest/helpers/common.go index 0b89a67..ae81d81 100644 --- a/src/internal/rest/helpers/common.go +++ b/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()) } diff --git a/src/internal/rest/helpers/flushChatCsv.go b/src/ui/rest/helpers/flushChatCsv.go similarity index 100% rename from src/internal/rest/helpers/flushChatCsv.go rename to src/ui/rest/helpers/flushChatCsv.go diff --git a/src/internal/rest/message.go b/src/ui/rest/message.go similarity index 96% rename from src/internal/rest/message.go rename to src/ui/rest/message.go index 8d6d9fc..dfa1463 100644 --- a/src/internal/rest/message.go +++ b/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) diff --git a/src/internal/rest/middleware/basicauth.go b/src/ui/rest/middleware/basicauth.go similarity index 100% rename from src/internal/rest/middleware/basicauth.go rename to src/ui/rest/middleware/basicauth.go diff --git a/src/internal/rest/middleware/recovery.go b/src/ui/rest/middleware/recovery.go similarity index 100% rename from src/internal/rest/middleware/recovery.go rename to src/ui/rest/middleware/recovery.go diff --git a/src/internal/rest/newsletter.go b/src/ui/rest/newsletter.go similarity index 90% rename from src/internal/rest/newsletter.go rename to src/ui/rest/newsletter.go index 0386e9f..7b0d146 100644 --- a/src/internal/rest/newsletter.go +++ b/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 diff --git a/src/internal/rest/send.go b/src/ui/rest/send.go similarity index 96% rename from src/internal/rest/send.go rename to src/ui/rest/send.go index e30e9e4..0e84e17 100644 --- a/src/internal/rest/send.go +++ b/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) diff --git a/src/internal/rest/user.go b/src/ui/rest/user.go similarity index 85% rename from src/internal/rest/user.go rename to src/ui/rest/user.go index 1dc244b..4281f17 100644 --- a/src/internal/rest/user.go +++ b/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, + }) +} diff --git a/src/internal/websocket/websocket.go b/src/ui/websocket/websocket.go similarity index 97% rename from src/internal/websocket/websocket.go rename to src/ui/websocket/websocket.go index 4c3fe3a..fab698e 100644 --- a/src/internal/websocket/websocket.go +++ b/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() diff --git a/src/services/app.go b/src/usecase/app.go similarity index 91% rename from src/services/app.go rename to src/usecase/app.go index a52b66a..d16093d 100644 --- a/src/services/app.go +++ b/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 } diff --git a/src/services/group.go b/src/usecase/group.go similarity index 89% rename from src/services/group.go rename to src/usecase/group.go index 2c31bb6..109aab2 100644 --- a/src/services/group.go +++ b/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 diff --git a/src/services/message.go b/src/usecase/message.go similarity index 95% rename from src/services/message.go rename to src/usecase/message.go index 9d16da1..debb819 100644 --- a/src/services/message.go +++ b/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 diff --git a/src/services/newsletter.go b/src/usecase/newsletter.go similarity index 72% rename from src/services/newsletter.go rename to src/usecase/newsletter.go index 0afcf42..476088d 100644 --- a/src/services/newsletter.go +++ b/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 } diff --git a/src/services/send.go b/src/usecase/send.go similarity index 99% rename from src/services/send.go rename to src/usecase/send.go index 48a0383..35eaa23 100644 --- a/src/services/send.go +++ b/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, diff --git a/src/services/user.go b/src/usecase/user.go similarity index 79% rename from src/services/user.go rename to src/usecase/user.go index 12e7f1f..13e890b 100644 --- a/src/services/user.go +++ b/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 +} diff --git a/src/views/components/AccountUserCheck.js b/src/views/components/AccountUserCheck.js new file mode 100644 index 0000000..c417c68 --- /dev/null +++ b/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: ` +
+
+ Account +
User Check
+
+ Check if a user is on WhatsApp +
+
+
+ + + ` +} \ No newline at end of file diff --git a/src/views/index.html b/src/views/index.html index 7635eea..58e266f 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -161,6 +161,7 @@ + @@ -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() {