diff --git a/docs/openapi.yaml b/docs/openapi.yaml index c278281..cdcd0ed 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1,7 +1,7 @@ -openapi: 3.0.0 +openapi: "3.0.0" info: title: WhatsApp API MultiDevice - version: 4.4.0 + version: 5.1.0 description: This API is used for sending whatsapp via API servers: - url: http://localhost:3000 @@ -131,8 +131,9 @@ paths: - name: phone in: query schema: - type: integer + type: string example: '6289685028129@s.whatsapp.net' + description: Phone number with country code responses: '200': description: OK @@ -162,13 +163,15 @@ paths: - name: phone in: query schema: - type: integer + type: string example: '6289685028129@s.whatsapp.net' + description: Phone number with country code - name: is_preview in: query schema: type: boolean example: true + description: Whether to fetch a preview of the avatar responses: '200': description: OK @@ -188,6 +191,40 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorInternalServer' + post: + operationId: userChangeAvatar + tags: + - user + summary: User Change Avatar + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + avatar: + type: string + format: binary + description: Avatar to send + 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' /user/my/privacy: get: operationId: userMyPrivacy @@ -316,6 +353,10 @@ paths: type: string format: binary description: Image to send + image_url: + type: string + example: https://example.com/image.jpg + description: Image URL to send compress: type: boolean example: false @@ -656,6 +697,45 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorInternalServer' + /send/presence: + post: + operationId: sendPresence + tags: + - send + summary: Send presence status + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + presence: + type: string + description: The presence status to send + enum: [available, unavailable] + example: 'available' + required: + - presence + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SendResponse' + '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}/revoke: post: operationId: revokeMessage @@ -1247,13 +1327,17 @@ components: type: number UserInfoResponse: type: object + required: + - code + - message + - results properties: code: type: string example: SUCCESS message: type: string - example: + example: Success results: type: object properties: @@ -1501,7 +1585,7 @@ components: properties: text: type: string - example: "WhatsApp’s official channel. Follow for our latest feature launches, updates, exclusive drops and more." + example: "WhatsApp's official channel. Follow for our latest feature launches, updates, exclusive drops and more." id: type: string example: "1689653839450668" diff --git a/readme.md b/readme.md index 4c40c1d..7ead8a4 100644 --- a/readme.md +++ b/readme.md @@ -1,60 +1,64 @@ -## WhatsApp API Multi Device Version +# WhatsApp API Multi Device Version ![release version](https://img.shields.io/github/v/release/aldinokemal/go-whatsapp-web-multidevice) -
+ ![Build Image](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/build-docker-image.yaml/badge.svg) -
+ ![release windows](https://github.com/aldinokemal/go-whatsapp-web-multidevice/actions/workflows/release-windows.yml/badge.svg) ![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 `ARM` Architecture Now that we support ARM64 for Linux: - [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. -### Feature +## Feature - Send WhatsApp message via http API, [docs/openapi.yml](./docs/openapi.yaml) for more details +- Mention someone + - `@phoneNumber` + - example: `Hello @628974812XXXX, @628974812XXXX` +- Post Whatsapp Status - Compress image before send - Compress video before send - Change OS name become your app (it's the device name when connect via mobile) - - `--os=Chrome` or `--os=MyApplication` + - `--os=Chrome` or `--os=MyApplication` - Basic Auth (able to add multi credentials) - - `--basic-auth=kemal:secret,toni:password,userName:secretPassword`, or you can simplify - - `-b=kemal:secret,toni:password,userName:secretPassword` + - `--basic-auth=kemal:secret,toni:password,userName:secretPassword`, or you can simplify + - `-b=kemal:secret,toni:password,userName:secretPassword` - Customizable port and debug mode - - `--port 8000` - - `--debug true` + - `--port 8000` + - `--debug true` - Auto reply message - - `--autoreply="Don't reply this message"` + - `--autoreply="Don't reply this message"` - Webhook for received message - - `--webhook="http://yourwebhook.site/handler"`, or you can simplify - - `-w="http://yourwebhook.site/handler"` + - `--webhook="http://yourwebhook.site/handler"`, or you can simplify + - `-w="http://yourwebhook.site/handler"` - Webhook Secret - Our webhook will be sent to you with an HMAC header and a sha256 default key `secret`.
You may modify this by using the option below: - - `--webhook-secret="secret"` + - `--webhook-secret="secret"` + - For more command `./main --help` -### Required (without docker) +## Required (without docker) - Mac OS: - - `brew install ffmpeg` - - `export CGO_CFLAGS_ALLOW="-Xpreprocessor"` + - `brew install ffmpeg` + - `export CGO_CFLAGS_ALLOW="-Xpreprocessor"` - Linux: - - `sudo apt update` - - `sudo apt install ffmpeg` + - `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) - - add to ffmpeg to [environment variable](https://www.google.com/search?q=windows+add+to+environment+path) + - 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 +## How to use -#### Basic +### Basic 1. Clone this repo: `git clone https://github.com/aldinokemal/go-whatsapp-web-multidevice` 2. Open the folder that was cloned via cmd/terminal. @@ -62,14 +66,14 @@ Now that we support ARM64 for Linux: 4. run `go run main.go` 5. Open `http://localhost:3000` -#### Docker (you don't need to install in required) +### Docker (you don't need to install in required) 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 `docker-compose up -d --build` 4. open `http://localhost:3000` -#### Build your own binary +### Build your own binary 1. Clone this repo `git clone https://github.com/aldinokemal/go-whatsapp-web-multidevice` 2. Open the folder that was cloned via cmd/terminal. @@ -86,22 +90,48 @@ Now that we support ARM64 for Linux: ### Production Mode (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" ``` +### Production Mode (docker compose) + +create `docker-compose.yml` file with the following configuration: + +```yml +services: + whatsapp: + image: aldinokemal2104/go-whatsapp-web-multidevice + container_name: whatsapp + restart: always + ports: + - "3000:3000" + volumes: + - whatsapp:/app/storages + command: + - --basic-auth=admin:admin + - --port=3000 + - --debug=true + - --os=Chrome + - --account-validation=false + +volumes: + whatsapp: +``` + + ### Production Mode (binary) - download binary from [release](https://github.com/aldinokemal/go-whatsapp-web-multidevice/releases) You can fork or edit this source code ! -### Current API +## Current API -- [Api Specification Document](https://bump.sh/aldinokemal/doc/go-whatsapp-web-multidevice) -- You can check [docs/openapi.yml](./docs/openapi.yaml) for detail API or paste - to [SwaggerEditor](https://editor.swagger.io). -- Furthermore you can generate HTTP Client from this API using [openapi-generator](https://openapi-generator.tech/#try) +- [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. +- Generate HTTP clients using [openapi-generator](https://openapi-generator.tech/#try). ## CURL for the api @@ -109,36 +139,37 @@ You can fork or edit this source code ! - curl -X 'GET' 'http://127.0.0.1:3000/user/check?phone=YOUR_PHONE' -H 'accept: application/json' \ -H 'Authorization: Basic qwertyASDFzxc=' -- curl -X 'GET' 'http://127.0.0.1:3000/user/check?phone=YOUR_PHONE' -H 'accept: application/json' -| Feature | Menu | Method | URL | +| Feature | Menu | Method | URL | |---------|------------------------------|--------|-------------------------------| | ✅ | Login with Scan QR | GET | /app/login | | ✅ | Login With Pair Code | GET | /app/login-with-code | | ✅ | Logout | GET | /app/logout | -| ✅ | Reconnect | GET | /app/reconnect | -| ✅ | Devices | GET | /app/devices | +| ✅ | Reconnect | GET | /app/reconnect | +| ✅ | Devices | GET | /app/devices | | ✅ | User Info | GET | /user/info | | ✅ | Check User is on whatsapp | GET | /user/check | | ✅ | User Avatar | GET | /user/avatar | +| ✅ | User Change Avatar | POST | /user/avatar | | ✅ | User My Groups | GET | /user/my/groups | | ✅ | User My Newsletter | GET | /user/my/newsletters | | ✅ | User My Privacy Setting | GET | /user/my/privacy | | ✅ | Send Message | POST | /send/message | -| ✅ | Send Image | POST | /send/image | -| ✅ | Send Audio | POST | /send/audio | -| ✅ | Send File | POST | /send/file | -| ✅ | Send Video | POST | /send/video | +| ✅ | Send Image | POST | /send/image | +| ✅ | Send Audio | POST | /send/audio | +| ✅ | Send File | POST | /send/file | +| ✅ | Send Video | POST | /send/video | | ✅ | Send Contact | POST | /send/contact | | ✅ | Send Link | POST | /send/link | | ✅ | Send Location | POST | /send/location | | ✅ | Send Poll / Vote | POST | /send/poll | +| ✅ | Send Presence | POST | /send/presence | | ✅ | Revoke Message | POST | /message/:message_id/revoke | | ✅ | React Message | POST | /message/:message_id/reaction | | ✅ | Delete Message | POST | /message/:message_id/delete | | ✅ | Edit Message | POST | /message/:message_id/update | | ✅ | Read Message (DM) | POST | /message/:message_id/read | -| ❌ | Star message | POST | /message/:message_id/star | +| ❌ | Star Message | POST | /message/:message_id/star | | ✅ | Join Group With Link | POST | /group/join-with-link | | ✅ | Leave Group | POST | /group/leave | | ✅ | Create Group | POST | /group | @@ -148,39 +179,42 @@ You can fork or edit this source code ! | ✅ | Demote Participant in Group | POST | /group/participants/demote | | ✅ | Unfollow Newsletter | POST | /newsletter/unfollow | -``` +```txt ✅ = Available ❌ = Not Available Yet ``` ### User Interface -| Description | Image | -|--------------------|------------------------------------------------------------------------------------------| -| Homepage | ![Homepage](https://i.ibb.co.com/Sy0dHZp/homepage-v4-20.png) | -| Login | ![Login](https://i.ibb.co.com/jkcB15R/login.png?v=1) | -| Login With Code | ![Login With Code](https://i.ibb.co.com/rdJGvGw/paircode.png) | -| Send Message | ![Send Message](https://i.ibb.co.com/rc3NXMX/send-message.png?v1) | -| Send Image | ![Send Image](https://i.ibb.co.com/BcFL3SD/send-image.png?v1) | -| Send File | ![Send File](https://i.ibb.co.com/f4yxjpp/send-file.png) | -| Send Video | ![Send Video](https://i.ibb.co.com/PrD3P51/send-video.png) | -| Send Contact | ![Send Contact](https://i.ibb.co.com/4810H7N/send-contact.png) | -| Send Location | ![Send Location](https://i.ibb.co.com/TWsy09G/send-location.png) | -| Send Audio | ![Send Audio](https://i.ibb.co.com/p1wL4wh/Send-Audio.png) | -| Send Poll | ![Send Poll](https://i.ibb.co.com/mq2fGHz/send-poll.png) | -| Revoke Message | ![Revoke Message](https://i.ibb.co.com/yswhvQY/revoke.png?v1) | -| Delete Message | ![Delete Message](https://i.ibb.co.com/F70SZ84/image.png) | -| Reaction Message | ![Reaction Message](https://i.ibb.co.com/BfHgSHG/react-message.png) | -| Edit Message | ![Edit Message](https://i.ibb.co.com/kXfpqJw/update-message.png) | -| User Info | ![User Info](https://i.ibb.co.com/3zjX6Cz/user-info.png?v=1) | -| Check User | ![Check User ](https://i.ibb.co/92gVZrx/Check-User.png?v=1) | -| User Avatar | ![User Avatar](https://i.ibb.co.com/ZmJZ4ZW/search-avatar.png?v=1) | -| My Privacy | ![My Privacy](https://i.ibb.co.com/Cw1sMQz/my-privacy.png) | -| My Group | ![My Group](https://i.ibb.co.com/WB268Xy/list-group.png) | -| Auto Reply | ![Auto Reply](https://i.ibb.co.com/D4rTytX/IMG-20220517-162500.jpg) | -| Basic Auth Prompt | ![Basic Auth Prompt](https://i.ibb.co.com/PDjQ92W/Screenshot-2022-11-06-at-14-06-29.png) | -| Manage Participant | ![Manage Participant](https://i.ibb.co.com/ynrN7cr/manage-participant.png) | -| My Newsletter | ![List Newsletter](https://i.ibb.co.com/WDg50jJ/image.png) | +| Description | Image | +|----------------------|------------------------------------------------------------------------------------------| +| Homepage | ![Homepage](https://i.ibb.co/251sHyF/Homepage.png) | +| Login | ![Login](https://i.ibb.co.com/xJyCWv8/login.png) | +| Login With Code | ![Login With Code](https://i.ibb.co.com/YDjyXby/login-With-Code.png) | +| Send Message | ![Send Message](https://i.ibb.co.com/7Y0wJ5R/send-Message.png) | +| Send Image | ![Send Image](https://i.ibb.co.com/NtyLLdS/send-Image.png) | +| Send File | ![Send File](https://i.ibb.co.com/D94yvnX/sendFile.png) | +| Send Video | ![Send Video](https://i.ibb.co.com/r0LdRFH/send-Video.png) | +| 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 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) | +| Reaction Message | ![Reaction Message](https://i.ibb.co.com/fNqJXF0/react-Message.png) | +| Edit Message | ![Edit Message](https://i.ibb.co.com/Vx5cQMg/update-Message.png) | +| User Info | ![User Info](https://i.ibb.co.com/qd7J7Nd/Search-User-Info.png) | +| Check User | ![Check User ](https://i.ibb.co/92gVZrx/Check-User.png?v=1) | +| User Avatar | ![User Avatar](https://i.ibb.co.com/zbSzpP3/Search-Avatar.png) | +| My Privacy | ![My Privacy](https://i.ibb.co.com/HCLxGHr/My-Privacy.png) | +| My Group | ![My Group](https://i.ibb.co.com/p19dFQ5/list-Group.png) | +| Create Group | ![My Group](https://i.ibb.co.com/YLT5Hyh/create-Group.png) | +| Join Group with LInk | ![My Group](https://i.ibb.co.com/x5p95J7/join-Group-With-Link.png) | +| Auto Reply | ![Auto Reply](https://i.ibb.co.com/D4rTytX/IMG-20220517-162500.jpg) | +| Basic Auth Prompt | ![Basic Auth Prompt](https://i.ibb.co.com/PDjQ92W/Screenshot-2022-11-06-at-14-06-29.png) | +| Manage Participant | ![Manage Participant](https://i.ibb.co.com/x7yn9nY/Manage-Participant.png) | +| My Newsletter | ![List Newsletter](https://i.ibb.co.com/BLvyS9j/My-Newsletter.png) | ### Mac OS NOTE diff --git a/src/cmd/root.go b/src/cmd/root.go index 7e362f0..858a85c 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -46,12 +46,13 @@ func init() { rootCmd.PersistentFlags().StringVarP(&config.AppPort, "port", "p", config.AppPort, "change port number with --port | example: --port=8080") rootCmd.PersistentFlags().BoolVarP(&config.AppDebug, "debug", "d", config.AppDebug, "hide or displaying log with --debug | example: --debug=true") rootCmd.PersistentFlags().StringVarP(&config.AppOs, "os", "", config.AppOs, `os name --os | example: --os="Chrome"`) - rootCmd.PersistentFlags().StringVarP(&config.AppBasicAuthCredential, "basic-auth", "b", config.AppBasicAuthCredential, "basic auth credential | -b=yourUsername:yourPassword") + rootCmd.PersistentFlags().StringSliceVarP(&config.AppBasicAuthCredential, "basic-auth", "b", config.AppBasicAuthCredential, "basic auth credential | -b=yourUsername:yourPassword") rootCmd.PersistentFlags().StringVarP(&config.WhatsappAutoReplyMessage, "autoreply", "", config.WhatsappAutoReplyMessage, `auto reply when received message --autoreply | example: --autoreply="Don't reply this message"`) - rootCmd.PersistentFlags().StringVarP(&config.WhatsappWebhook, "webhook", "w", config.WhatsappWebhook, `forward event to webhook --webhook | example: --webhook="https://yourcallback.com/callback"`) + rootCmd.PersistentFlags().StringSliceVarP(&config.WhatsappWebhook, "webhook", "w", config.WhatsappWebhook, `forward event to webhook --webhook | example: --webhook="https://yourcallback.com/callback"`) rootCmd.PersistentFlags().StringVarP(&config.WhatsappWebhookSecret, "webhook-secret", "", config.WhatsappWebhookSecret, `secure webhook request --webhook-secret | example: --webhook-secret="super-secret-key"`) rootCmd.PersistentFlags().BoolVarP(&config.WhatsappAccountValidation, "account-validation", "", config.WhatsappAccountValidation, `enable or disable account validation --account-validation | example: --account-validation=true`) rootCmd.PersistentFlags().StringVarP(&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"`) + rootCmd.PersistentFlags().IntVarP(&config.AppChatFlushIntervalDays, "chat-flush-interval", "", config.AppChatFlushIntervalDays, `the interval to flush the chat storage --chat-flush-interval | example: --chat-flush-interval=7`) } func runRest(_ *cobra.Command, _ []string) { @@ -74,12 +75,19 @@ func runRest(_ *cobra.Command, _ []string) { 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 { @@ -90,10 +98,9 @@ func runRest(_ *cobra.Command, _ []string) { AllowHeaders: "Origin, Content-Type, Accept", })) - if config.AppBasicAuthCredential != "" { + if len(config.AppBasicAuthCredential) > 0 { account := make(map[string]string) - multipleBA := strings.Split(config.AppBasicAuthCredential, ",") - for _, basicAuth := range multipleBA { + for _, basicAuth := range config.AppBasicAuthCredential { ba := strings.Split(basicAuth, ":") if len(ba) != 2 { log.Fatalln("Basic auth is not valid, please this following format :") @@ -142,6 +149,9 @@ func runRest(_ *cobra.Command, _ []string) { go helpers.SetAutoConnectAfterBooting(appService) // Set auto reconnect checking go helpers.SetAutoReconnectChecking(cli) + // Start auto flush chat csv + go helpers.StartAutoFlushChatStorage() + if err = app.Listen(":" + config.AppPort); err != nil { log.Fatalln("Failed to start: ", err.Error()) } diff --git a/src/config/settings.go b/src/config/settings.go index ed1c350..4de0fd3 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -5,24 +5,27 @@ import ( ) var ( - AppVersion = "v4.22.1" - AppPort = "3000" - AppDebug = false - AppOs = "AldinoKemal" - AppPlatform = waCompanionReg.DeviceProps_PlatformType(1) - AppBasicAuthCredential string + AppVersion = "v5.1.0" + AppPort = "3000" + AppDebug = false + AppOs = "AldinoKemal" + AppPlatform = waCompanionReg.DeviceProps_PlatformType(1) + AppBasicAuthCredential []string + AppChatFlushIntervalDays = 7 // Number of days before flushing chat.csv - PathQrCode = "statics/qrcode" - PathSendItems = "statics/senditems" - PathMedia = "statics/media" - PathStorages = "storages" + PathQrCode = "statics/qrcode" + PathSendItems = "statics/senditems" + PathMedia = "statics/media" + PathStorages = "storages" + PathChatStorage = "storages/chat.csv" DBURI = "file:storages/whatsapp.db?_foreign_keys=off" WhatsappAutoReplyMessage string - WhatsappWebhook string + WhatsappWebhook []string WhatsappWebhookSecret = "secret" WhatsappLogLevel = "ERROR" + WhatsappSettingMaxImageSize int64 = 20000000 // 20MB WhatsappSettingMaxFileSize int64 = 50000000 // 50MB WhatsappSettingMaxVideoSize int64 = 100000000 // 100MB WhatsappSettingMaxDownloadSize int64 = 500000000 // 500MB diff --git a/src/domains/message/message.go b/src/domains/message/message.go index 0de494b..a2db0ad 100644 --- a/src/domains/message/message.go +++ b/src/domains/message/message.go @@ -8,6 +8,7 @@ type IMessageService interface { RevokeMessage(ctx context.Context, request RevokeRequest) (response GenericResponse, err error) UpdateMessage(ctx context.Context, request UpdateMessageRequest) (response GenericResponse, err error) DeleteMessage(ctx context.Context, request DeleteRequest) (err error) + StarMessage(ctx context.Context, request StarRequest) (err error) } type GenericResponse struct { @@ -41,3 +42,9 @@ type MarkAsReadRequest struct { MessageID string `json:"message_id" uri:"message_id"` Phone string `json:"phone" form:"phone"` } + +type StarRequest struct { + MessageID string `json:"message_id" uri:"message_id"` + Phone string `json:"phone" form:"phone"` + IsStarred bool `json:"is_starred"` +} diff --git a/src/domains/send/audio.go b/src/domains/send/audio.go index f2e5063..16da723 100644 --- a/src/domains/send/audio.go +++ b/src/domains/send/audio.go @@ -4,5 +4,5 @@ import "mime/multipart" type AudioRequest struct { Phone string `json:"phone" form:"phone"` - Audio *multipart.FileHeader `json:"Audio" form:"Audio"` + Audio *multipart.FileHeader `json:"audio" form:"audio"` } diff --git a/src/domains/send/image.go b/src/domains/send/image.go index 783b030..b6baf95 100644 --- a/src/domains/send/image.go +++ b/src/domains/send/image.go @@ -6,6 +6,7 @@ type ImageRequest struct { Phone string `json:"phone" form:"phone"` Caption string `json:"caption" form:"caption"` Image *multipart.FileHeader `json:"image" form:"image"` + ImageURL *string `json:"image_url" form:"image_url"` ViewOnce bool `json:"view_once" form:"view_once"` Compress bool `json:"compress"` } diff --git a/src/domains/send/presence.go b/src/domains/send/presence.go new file mode 100644 index 0000000..301e3e3 --- /dev/null +++ b/src/domains/send/presence.go @@ -0,0 +1,5 @@ +package send + +type PresenceRequest struct { + Type string `json:"type" form:"type"` +} diff --git a/src/domains/send/send.go b/src/domains/send/send.go index 0e270c2..9e0be5e 100644 --- a/src/domains/send/send.go +++ b/src/domains/send/send.go @@ -14,6 +14,7 @@ type ISendService interface { SendLocation(ctx context.Context, request LocationRequest) (response GenericResponse, err error) SendAudio(ctx context.Context, request AudioRequest) (response GenericResponse, err error) SendPoll(ctx context.Context, request PollRequest) (response GenericResponse, err error) + SendPresence(ctx context.Context, request PresenceRequest) (response GenericResponse, err error) } type GenericResponse struct { diff --git a/src/domains/user/account.go b/src/domains/user/account.go index 45e9959..bbf7efc 100644 --- a/src/domains/user/account.go +++ b/src/domains/user/account.go @@ -1,6 +1,9 @@ package user -import "go.mau.fi/whatsmeow/types" +import ( + "go.mau.fi/whatsmeow/types" + "mime/multipart" +) type InfoRequest struct { Phone string `json:"phone" query:"phone"` @@ -71,3 +74,7 @@ type MyListGroupsResponse struct { type MyListNewsletterResponse struct { Data []types.NewsletterMetadata `json:"data"` } + +type ChangeAvatarRequest struct { + Avatar *multipart.FileHeader `json:"avatar" form:"avatar"` +} diff --git a/src/domains/user/user.go b/src/domains/user/user.go index 453f6da..4ffe1a2 100644 --- a/src/domains/user/user.go +++ b/src/domains/user/user.go @@ -8,6 +8,7 @@ type IUserService interface { Info(ctx context.Context, request InfoRequest) (response InfoResponse, err error) Check(ctx context.Context, request CheckRequest) (response CheckResponse, err error) Avatar(ctx context.Context, request AvatarRequest) (response AvatarResponse, err error) + ChangeAvatar(ctx context.Context, request ChangeAvatarRequest) (err error) MyListGroups(ctx context.Context) (response MyListGroupsResponse, err error) MyListNewsletter(ctx context.Context) (response MyListNewsletterResponse, err error) MyPrivacySetting(ctx context.Context) (response MyPrivacySettingResponse, err error) diff --git a/src/go.mod b/src/go.mod index 4a8ecf2..023cd50 100644 --- a/src/go.mod +++ b/src/go.mod @@ -10,7 +10,7 @@ require ( 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/template/html/v2 v2.1.2 + 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 @@ -21,8 +21,8 @@ require ( github.com/stretchr/testify v1.10.0 github.com/valyala/fasthttp v1.58.0 go.mau.fi/libsignal v0.1.1 - go.mau.fi/whatsmeow v0.0.0-20250104105216-918c879fcd19 - google.golang.org/protobuf v1.36.1 + go.mau.fi/whatsmeow v0.0.0-20250204095649-a75587ab11d7 + google.golang.org/protobuf v1.36.4 ) require ( @@ -38,22 +38,21 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.33.0 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - go.mau.fi/util v0.8.3 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/image v0.23.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect + go.mau.fi/util v0.8.4 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index 682e7d0..99b6be9 100644 --- a/src/go.sum +++ b/src/go.sum @@ -27,8 +27,8 @@ github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27X github.com/gofiber/fiber/v2 v2.52.6/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.2 h1:wkK/mYJ3nIhongTkG3t0QgV4ADdgOYJYVSAF2AHnh8Y= -github.com/gofiber/template/html/v2 v2.1.2/go.mod h1:E98Z/FzvpaSib06aWEgYk6GXNf3ctoyaJH8yW5ay5ak= +github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o= +github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE= github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w= @@ -50,8 +50,9 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -80,6 +81,8 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -96,20 +99,27 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mau.fi/libsignal v0.1.1 h1:m/0PGBh4QKP/I1MQ44ti4C0fMbLMuHb95cmDw01FIpI= go.mau.fi/libsignal v0.1.1/go.mod h1:QLs89F/OA3ThdSL2Wz2p+o+fi8uuQUz0e1BRa6ExdBw= -go.mau.fi/util v0.8.3 h1:sulhXtfquMrQjsOP67x9CzWVBYUwhYeoo8hNQIpCWZ4= -go.mau.fi/util v0.8.3/go.mod h1:c00Db8xog70JeIsEvhdHooylTkTkakgnAOsZ04hplQY= +go.mau.fi/util v0.8.4 h1:mVKlJcXWfVo8ZW3f4vqtjGpqtZqJvX4ETekxawt2vnQ= +go.mau.fi/util v0.8.4/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY= go.mau.fi/whatsmeow v0.0.0-20250104105216-918c879fcd19 h1:uVS+Zct5fF8rSXV9lfs87zoXdge0JXTzVGNkjmZ61UU= go.mau.fi/whatsmeow v0.0.0-20250104105216-918c879fcd19/go.mod h1:TLzm2XkwgufONEmiVAsFny+9uBqyEZnUoPrQAfMyuSU= +go.mau.fi/whatsmeow v0.0.0-20250130221717-faf72d668860 h1:jQhAJJGC42rwZ562nz6V9SXBCVz+QhORusd0r9cxiww= +go.mau.fi/whatsmeow v0.0.0-20250130221717-faf72d668860/go.mod h1:PG1x7fBW66I9q/e8a9mU2qF9M94+kK32MceMWgxBoiw= +go.mau.fi/whatsmeow v0.0.0-20250204095649-a75587ab11d7 h1:eLT0TKTpSeNcszoyasSylaxFCNU82XCP72DjbInFpzg= +go.mau.fi/whatsmeow v0.0.0-20250204095649-a75587ab11d7/go.mod h1:PG1x7fBW66I9q/e8a9mU2qF9M94+kK32MceMWgxBoiw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 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 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= 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= @@ -123,8 +133,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 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= @@ -148,6 +159,8 @@ 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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -173,8 +186,10 @@ 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.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/src/internal/rest/helpers/flushChatCsv.go b/src/internal/rest/helpers/flushChatCsv.go new file mode 100644 index 0000000..5f1c9a3 --- /dev/null +++ b/src/internal/rest/helpers/flushChatCsv.go @@ -0,0 +1,46 @@ +package helpers + +import ( + "os" + "sync" + "time" + + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" + "github.com/sirupsen/logrus" +) + +var flushMutex sync.Mutex + +func FlushChatCsv() error { + flushMutex.Lock() + defer flushMutex.Unlock() + + // Create an empty file (truncating any existing content) + file, err := os.OpenFile(config.PathChatStorage, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer file.Close() + + return nil +} + +// StartAutoFlushChatStorage starts a goroutine that periodically flushes the chat storage +func StartAutoFlushChatStorage() { + interval := time.Duration(config.AppChatFlushIntervalDays) * 24 * time.Hour + + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for range ticker.C { + if err := FlushChatCsv(); err != nil { + logrus.Errorf("Error flushing chat storage: %v", err) + } else { + logrus.Info("Successfully flushed chat storage") + } + } + }() + + logrus.Infof("Auto flush for chat storage started (your account chat still safe). Will flush every %d days", config.AppChatFlushIntervalDays) +} diff --git a/src/internal/rest/message.go b/src/internal/rest/message.go index 22086b5..8ec7ad1 100644 --- a/src/internal/rest/message.go +++ b/src/internal/rest/message.go @@ -19,6 +19,8 @@ func InitRestMessage(app *fiber.App, service domainMessage.IMessageService) Mess app.Post("/message/:message_id/delete", rest.DeleteMessage) app.Post("/message/:message_id/update", rest.UpdateMessage) app.Post("/message/:message_id/read", rest.MarkAsRead) + app.Post("/message/:message_id/star", rest.StarMessage) + app.Post("/message/:message_id/unstar", rest.UnstarMessage) return rest } @@ -116,3 +118,42 @@ func (controller *Message) MarkAsRead(c *fiber.Ctx) error { Results: response, }) } + +func (controller *Message) StarMessage(c *fiber.Ctx) error { + var request domainMessage.StarRequest + err := c.BodyParser(&request) + utils.PanicIfNeeded(err) + + request.MessageID = c.Params("message_id") + whatsapp.SanitizePhone(&request.Phone) + request.IsStarred = true + + err = controller.Service.StarMessage(c.UserContext(), request) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Status: 200, + Code: "SUCCESS", + Message: "Starred message successfully", + Results: nil, + }) +} + +func (controller *Message) UnstarMessage(c *fiber.Ctx) error { + var request domainMessage.StarRequest + err := c.BodyParser(&request) + utils.PanicIfNeeded(err) + + request.MessageID = c.Params("message_id") + whatsapp.SanitizePhone(&request.Phone) + request.IsStarred = false + err = controller.Service.StarMessage(c.UserContext(), request) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Status: 200, + Code: "SUCCESS", + Message: "Unstarred message successfully", + Results: nil, + }) +} diff --git a/src/internal/rest/send.go b/src/internal/rest/send.go index e0eb827..e30e9e4 100644 --- a/src/internal/rest/send.go +++ b/src/internal/rest/send.go @@ -22,6 +22,7 @@ func InitRestSend(app *fiber.App, service domainSend.ISendService) Send { app.Post("/send/location", rest.SendLocation) app.Post("/send/audio", rest.SendAudio) app.Post("/send/poll", rest.SendPoll) + app.Post("/send/presence", rest.SendPresence) return rest } @@ -51,9 +52,10 @@ func (controller *Send) SendImage(c *fiber.Ctx) error { utils.PanicIfNeeded(err) file, err := c.FormFile("image") - utils.PanicIfNeeded(err) + if err == nil { + request.Image = file + } - request.Image = file whatsapp.SanitizePhone(&request.Phone) response, err := controller.Service.SendImage(c.UserContext(), request) @@ -204,3 +206,19 @@ func (controller *Send) SendPoll(c *fiber.Ctx) error { Results: response, }) } + +func (controller *Send) SendPresence(c *fiber.Ctx) error { + var request domainSend.PresenceRequest + err := c.BodyParser(&request) + utils.PanicIfNeeded(err) + + response, err := controller.Service.SendPresence(c.UserContext(), request) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Status: 200, + Code: "SUCCESS", + Message: response.Status, + Results: response, + }) +} diff --git a/src/internal/rest/user.go b/src/internal/rest/user.go index 93d40b4..ab48fb2 100644 --- a/src/internal/rest/user.go +++ b/src/internal/rest/user.go @@ -16,6 +16,7 @@ func InitRestUser(app *fiber.App, service domainUser.IUserService) User { app.Get("/user/info", rest.UserInfo) app.Get("/user/check", rest.UserCheck) app.Get("/user/avatar", rest.UserAvatar) + app.Post("/user/avatar", rest.UserChangeAvatar) app.Get("/user/my/privacy", rest.UserMyPrivacySetting) app.Get("/user/my/groups", rest.UserMyListGroups) app.Get("/user/my/newsletters", rest.UserMyListNewsletter) @@ -78,6 +79,24 @@ func (controller *User) UserAvatar(c *fiber.Ctx) error { }) } +func (controller *User) UserChangeAvatar(c *fiber.Ctx) error { + var request domainUser.ChangeAvatarRequest + err := c.BodyParser(&request) + utils.PanicIfNeeded(err) + + request.Avatar, err = c.FormFile("avatar") + utils.PanicIfNeeded(err) + + err = controller.Service.ChangeAvatar(c.UserContext(), request) + utils.PanicIfNeeded(err) + + return c.JSON(utils.ResponseData{ + Status: 200, + Code: "SUCCESS", + Message: "Success change avatar", + }) +} + func (controller *User) UserMyPrivacySetting(c *fiber.Ctx) error { response, err := controller.Service.MyPrivacySetting(c.UserContext()) utils.PanicIfNeeded(err) diff --git a/src/pkg/utils/chat_storage.go b/src/pkg/utils/chat_storage.go new file mode 100644 index 0000000..e717e29 --- /dev/null +++ b/src/pkg/utils/chat_storage.go @@ -0,0 +1,96 @@ +package utils + +import ( + "encoding/csv" + "fmt" + "os" + "sync" + + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" +) + +type RecordedMessage struct { + MessageID string `json:"message_id,omitempty"` + JID string `json:"jid,omitempty"` + MessageContent string `json:"message_content,omitempty"` +} + +// mutex to prevent concurrent file access +var fileMutex sync.Mutex + +func FindRecordFromStorage(messageID string) (RecordedMessage, error) { + fileMutex.Lock() + defer fileMutex.Unlock() + + file, err := os.OpenFile(config.PathChatStorage, os.O_RDONLY|os.O_CREATE, 0644) + if err != nil { + return RecordedMessage{}, fmt.Errorf("failed to open storage file: %w", err) + } + defer file.Close() + + reader := csv.NewReader(file) + records, err := reader.ReadAll() + if err != nil { + return RecordedMessage{}, fmt.Errorf("failed to read CSV records: %w", err) + } + + for _, record := range records { + if len(record) == 3 && record[0] == messageID { + return RecordedMessage{ + MessageID: record[0], + JID: record[1], + MessageContent: record[2], + }, nil + } + } + return RecordedMessage{}, fmt.Errorf("message ID %s not found in storage", messageID) +} + +func RecordMessage(messageID string, senderJID string, messageContent string) error { + fileMutex.Lock() + defer fileMutex.Unlock() + + message := RecordedMessage{ + MessageID: messageID, + JID: senderJID, + MessageContent: messageContent, + } + + // Read existing messages + var records [][]string + if file, err := os.OpenFile(config.PathChatStorage, os.O_RDONLY|os.O_CREATE, 0644); err == nil { + defer file.Close() + reader := csv.NewReader(file) + records, err = reader.ReadAll() + if err != nil { + return fmt.Errorf("failed to read existing records: %w", err) + } + + // Check for duplicates + for _, record := range records { + if len(record) == 3 && record[0] == messageID { + return nil // Skip if duplicate found + } + } + } + + // Prepare the new record + newRecord := []string{message.MessageID, message.JID, message.MessageContent} + records = append([][]string{newRecord}, records...) // Prepend new message + + // Write all records back to file + file, err := os.OpenFile(config.PathChatStorage, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to open file for writing: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + defer writer.Flush() + + if err := writer.WriteAll(records); err != nil { + return fmt.Errorf("failed to write CSV records: %w", err) + } + + return nil +} diff --git a/src/pkg/utils/general.go b/src/pkg/utils/general.go index af43d31..90c8b80 100644 --- a/src/pkg/utils/general.go +++ b/src/pkg/utils/general.go @@ -2,7 +2,7 @@ package utils import ( "fmt" - "github.com/PuerkitoBio/goquery" + "io" "log" "net/http" "os" @@ -11,6 +11,9 @@ import ( "strconv" "strings" "time" + + "github.com/PuerkitoBio/goquery" + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" ) // RemoveFile is removing file with delay @@ -64,6 +67,8 @@ func StrToFloat64(text string) float64 { type Metadata struct { Title string Description string + Image string + ImageThumb []byte } func GetMetaDataFromURL(url string) (meta Metadata) { @@ -89,8 +94,26 @@ func GetMetaDataFromURL(url string) (meta Metadata) { meta.Title = element.Text() }) - // Print the meta description - fmt.Println("Meta data:", meta) + document.Find("meta[property='og:image']").Each(func(index int, element *goquery.Selection) { + meta.Image, _ = element.Attr("content") + }) + + // If an og:image is found, download it and store its content in ImageThumb + if meta.Image != "" { + imageResponse, err := http.Get(meta.Image) + if err != nil { + log.Printf("Failed to download image: %v", err) + } else { + defer imageResponse.Body.Close() + imageData, err := io.ReadAll(imageResponse.Body) + if err != nil { + log.Printf("Failed to read image data: %v", err) + } else { + meta.ImageThumb = imageData + } + } + } + return meta } @@ -109,3 +132,50 @@ func ContainsMention(message string) []string { } return phoneNumbers } + +func DownloadImageFromURL(url string) ([]byte, string, error) { + client := &http.Client{ + Timeout: 30 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return fmt.Errorf("too many redirects") + } + return nil + }, + } + response, err := client.Get(url) + if err != nil { + return nil, "", err + } + defer response.Body.Close() + contentType := response.Header.Get("Content-Type") + if !strings.HasPrefix(contentType, "image/") { + return nil, "", fmt.Errorf("invalid content type: %s", contentType) + } + // Check content length if available + if contentLength := response.ContentLength; contentLength > int64(config.WhatsappSettingMaxImageSize) { + return nil, "", fmt.Errorf("image size %d exceeds maximum allowed size %d", contentLength, config.WhatsappSettingMaxImageSize) + } + // Limit the size from config + reader := io.LimitReader(response.Body, int64(config.WhatsappSettingMaxImageSize)) + // Extract the file name from the URL and remove query parameters if present + segments := strings.Split(url, "/") + fileName := segments[len(segments)-1] + fileName = strings.Split(fileName, "?")[0] + // Check if the file extension is supported + allowedExtensions := map[string]bool{ + ".jpg": true, + ".jpeg": true, + ".png": true, + ".webp": true, + } + extension := strings.ToLower(filepath.Ext(fileName)) + if !allowedExtensions[extension] { + return nil, "", fmt.Errorf("unsupported file type: %s", extension) + } + imageData, err := io.ReadAll(reader) + if err != nil { + return nil, "", err + } + return imageData, fileName, nil +} diff --git a/src/pkg/utils/general_test.go b/src/pkg/utils/general_test.go index 6c80a29..b16c2bf 100644 --- a/src/pkg/utils/general_test.go +++ b/src/pkg/utils/general_test.go @@ -1,12 +1,21 @@ package utils_test import ( + "net/http" + "net/http/httptest" + "os" + "testing" + "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" "github.com/stretchr/testify/assert" - "testing" + "github.com/stretchr/testify/suite" ) -func TestContainsMention(t *testing.T) { +type UtilsTestSuite struct { + suite.Suite +} + +func (suite *UtilsTestSuite) TestContainsMention() { type args struct { message string } @@ -32,9 +41,82 @@ func TestContainsMention(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + suite.T().Run(tt.name, func(t *testing.T) { got := utils.ContainsMention(tt.args.message) assert.Equal(t, tt.want, got) }) } } + +func (suite *UtilsTestSuite) TestRemoveFile() { + tempFile, err := os.CreateTemp("", "testfile") + assert.NoError(suite.T(), err) + tempFilePath := tempFile.Name() + tempFile.Close() + + err = utils.RemoveFile(0, tempFilePath) + assert.NoError(suite.T(), err) + _, err = os.Stat(tempFilePath) + assert.True(suite.T(), os.IsNotExist(err)) +} + +func (suite *UtilsTestSuite) TestCreateFolder() { + tempDir := "testdir" + err := utils.CreateFolder(tempDir) + assert.NoError(suite.T(), err) + _, err = os.Stat(tempDir) + assert.NoError(suite.T(), err) + assert.True(suite.T(), err == nil) + os.RemoveAll(tempDir) +} + +func (suite *UtilsTestSuite) TestPanicIfNeeded() { + assert.PanicsWithValue(suite.T(), "test error", func() { + utils.PanicIfNeeded("test error") + }) + + assert.NotPanics(suite.T(), func() { + utils.PanicIfNeeded(nil) + }) +} + +func (suite *UtilsTestSuite) TestStrToFloat64() { + assert.Equal(suite.T(), 123.45, utils.StrToFloat64("123.45")) + assert.Equal(suite.T(), 0.0, utils.StrToFloat64("invalid")) + assert.Equal(suite.T(), 0.0, utils.StrToFloat64("")) +} + +func (suite *UtilsTestSuite) TestGetMetaDataFromURL() { + // Use httptest.NewServer to mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`Test Title`)) + })) + defer server.Close() // Ensure the server is closed when the test ends + + meta := utils.GetMetaDataFromURL(server.URL) + assert.Equal(suite.T(), "Test Title", meta.Title) + assert.Equal(suite.T(), "Test Description", meta.Description) + assert.Equal(suite.T(), "http://example.com/image.jpg", meta.Image) +} + +func (suite *UtilsTestSuite) TestDownloadImageFromURL() { + // Use httptest.NewServer to mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/image.jpg" { + w.Header().Set("Content-Type", "image/jpeg") // Set content type to image + w.Write([]byte("image data")) + } else { + http.NotFound(w, r) + } + })) + defer server.Close() // Ensure the server is closed when the test ends + + imageData, fileName, err := utils.DownloadImageFromURL(server.URL + "/image.jpg") + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), []byte("image data"), imageData) + assert.Equal(suite.T(), "image.jpg", fileName) +} + +func TestUtilsTestSuite(t *testing.T) { + suite.Run(t, new(UtilsTestSuite)) +} diff --git a/src/pkg/whatsapp/init.go b/src/pkg/whatsapp/init.go index 65afe25..c81835a 100644 --- a/src/pkg/whatsapp/init.go +++ b/src/pkg/whatsapp/init.go @@ -12,6 +12,7 @@ import ( "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/sirupsen/logrus" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" @@ -149,6 +150,8 @@ func handler(rawEvt interface{}) { } log.Infof("Received message %s from %s (%s): %+v", evt.Info.ID, evt.Info.SourceString(), strings.Join(metaParts, ", "), evt.Message) + message := ExtractMessageText(evt) + utils.RecordMessage(evt.Info.ID, evt.Info.Sender.String(), message) if img := evt.Message.GetImageMessage(); img != nil { if path, err := ExtractMedia(config.PathStorages, img); err != nil { @@ -164,7 +167,7 @@ func handler(rawEvt interface{}) { _, _ = cli.SendMessage(context.Background(), evt.Info.Sender, &waE2E.Message{Conversation: proto.String(config.WhatsappAutoReplyMessage)}) } - if config.WhatsappWebhook != "" && + if len(config.WhatsappWebhook) > 0 && !strings.Contains(evt.Info.SourceString(), "broadcast") && !isFromMySelf(evt.Info.SourceString()) { go func(evt *events.Message) { diff --git a/src/pkg/whatsapp/utils.go b/src/pkg/whatsapp/utils.go index f737009..5f3b9a6 100644 --- a/src/pkg/whatsapp/utils.go +++ b/src/pkg/whatsapp/utils.go @@ -250,3 +250,114 @@ func buildForwarded(evt *events.Message) bool { } return false } + +func ExtractMessageText(evt *events.Message) string { + messageText := evt.Message.GetConversation() + if extendedText := evt.Message.GetExtendedTextMessage(); extendedText != nil { + messageText = extendedText.GetText() + } else if protocolMessage := evt.Message.GetProtocolMessage(); protocolMessage != nil { + if editedMessage := protocolMessage.GetEditedMessage(); editedMessage != nil { + if extendedText := editedMessage.GetExtendedTextMessage(); extendedText != nil { + messageText = extendedText.GetText() + } + } + } else if imageMessage := evt.Message.GetImageMessage(); imageMessage != nil { + messageText = imageMessage.GetCaption() + if messageText == "" { + messageText = "🖼️ Image" + } else { + messageText = "🖼️ " + messageText + } + } else if documentMessage := evt.Message.GetDocumentMessage(); documentMessage != nil { + messageText = documentMessage.GetCaption() + if messageText == "" { + messageText = "📄 Document" + } else { + messageText = "📄 " + messageText + } + } else if videoMessage := evt.Message.GetVideoMessage(); videoMessage != nil { + messageText = videoMessage.GetCaption() + if messageText == "" { + messageText = "🎥 Video" + } else { + messageText = "🎥 " + messageText + } + } else if liveLocationMessage := evt.Message.GetLiveLocationMessage(); liveLocationMessage != nil { + messageText = liveLocationMessage.GetCaption() + if messageText == "" { + messageText = "📍 Live Location" + } else { + messageText = "📍 " + messageText + } + } else if locationMessage := evt.Message.GetLocationMessage(); locationMessage != nil { + messageText = locationMessage.GetName() + if messageText == "" { + messageText = "📍 Location" + } else { + messageText = "📍 " + messageText + } + } else if stickerMessage := evt.Message.GetStickerMessage(); stickerMessage != nil { + messageText = "🎨 Sticker" + if stickerMessage.GetIsAnimated() { + messageText = "✨ Animated Sticker" + } + if stickerMessage.GetAccessibilityLabel() != "" { + messageText += " - " + stickerMessage.GetAccessibilityLabel() + } + } else if contactMessage := evt.Message.GetContactMessage(); contactMessage != nil { + messageText = contactMessage.GetDisplayName() + if messageText == "" { + messageText = "👤 Contact" + } else { + messageText = "👤 " + messageText + } + } else if listMessage := evt.Message.GetListMessage(); listMessage != nil { + messageText = listMessage.GetTitle() + if messageText == "" { + messageText = "📝 List" + } else { + messageText = "📝 " + messageText + } + } else if orderMessage := evt.Message.GetOrderMessage(); orderMessage != nil { + messageText = orderMessage.GetOrderTitle() + if messageText == "" { + messageText = "🛍️ Order" + } else { + messageText = "🛍️ " + messageText + } + } else if paymentMessage := evt.Message.GetPaymentInviteMessage(); paymentMessage != nil { + messageText = paymentMessage.GetServiceType().String() + if messageText == "" { + messageText = "💳 Payment" + } else { + messageText = "💳 " + messageText + } + } else if audioMessage := evt.Message.GetAudioMessage(); audioMessage != nil { + messageText = "🎧 Audio" + if audioMessage.GetPTT() { + messageText = "🎤 Voice Message" + } + } else if pollMessageV3 := evt.Message.GetPollCreationMessageV3(); pollMessageV3 != nil { + messageText = pollMessageV3.GetName() + if messageText == "" { + messageText = "📊 Poll" + } else { + messageText = "📊 " + messageText + } + } else if pollMessageV4 := evt.Message.GetPollCreationMessageV4(); pollMessageV4 != nil { + messageText = pollMessageV4.GetMessage().GetConversation() + if messageText == "" { + messageText = "📊 Poll" + } else { + messageText = "📊 " + messageText + } + } else if pollMessageV5 := evt.Message.GetPollCreationMessageV5(); pollMessageV5 != nil { + messageText = pollMessageV5.GetMessage().GetConversation() + if messageText == "" { + messageText = "📊 Poll" + } else { + messageText = "📊 " + messageText + } + } + return messageText +} diff --git a/src/pkg/whatsapp/webhook.go b/src/pkg/whatsapp/webhook.go index 104d177..307b79d 100644 --- a/src/pkg/whatsapp/webhook.go +++ b/src/pkg/whatsapp/webhook.go @@ -21,8 +21,10 @@ func forwardToWebhook(evt *events.Message) error { return err } - if err = submitWebhook(payload); err != nil { - return err + for _, url := range config.WhatsappWebhook { + if err = submitWebhook(payload, url); err != nil { + return err + } } logrus.Info("Event forwarded to webhook") @@ -126,7 +128,7 @@ func createPayload(evt *events.Message) (map[string]interface{}, error) { return body, nil } -func submitWebhook(payload map[string]interface{}) error { +func submitWebhook(payload map[string]interface{}, url string) error { client := &http.Client{Timeout: 10 * time.Second} postBody, err := json.Marshal(payload) @@ -134,7 +136,7 @@ func submitWebhook(payload map[string]interface{}) error { return pkgError.WebhookError(fmt.Sprintf("Failed to marshal body: %v", err)) } - req, err := http.NewRequest(http.MethodPost, config.WhatsappWebhook, bytes.NewBuffer(postBody)) + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(postBody)) if err != nil { return pkgError.WebhookError(fmt.Sprintf("error when create http object %v", err)) } diff --git a/src/services/app.go b/src/services/app.go index 8aca9b9..a52b66a 100644 --- a/src/services/app.go +++ b/src/services/app.go @@ -4,6 +4,11 @@ import ( "context" "errors" "fmt" + "os" + "path/filepath" + "strings" + "time" + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" domainApp "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" @@ -14,10 +19,6 @@ import ( "go.mau.fi/libsignal/logger" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/store/sqlstore" - "os" - "path/filepath" - "strings" - "time" ) type serviceApp struct { @@ -98,11 +99,14 @@ func (service serviceApp) LoginWithCode(ctx context.Context, phoneNumber string) } // detect is already logged in - if service.WaCli.IsLoggedIn() { + if service.WaCli.Store.ID != nil { logrus.Warn("User is already logged in") return loginCode, pkgError.ErrAlreadyLoggedIn } + // reconnect first + _ = service.Reconnect(ctx) + loginCode, err = service.WaCli.PairPhone(phoneNumber, true, whatsmeow.PairClientChrome, "Chrome (Linux)") if err != nil { logrus.Errorf("Error when pairing phone: %s", err.Error()) diff --git a/src/services/message.go b/src/services/message.go index 2e40aa0..1a9eaaa 100644 --- a/src/services/message.go +++ b/src/services/message.go @@ -3,6 +3,8 @@ package services import ( "context" "fmt" + "time" + "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp" @@ -15,7 +17,6 @@ import ( "go.mau.fi/whatsmeow/proto/waSyncAction" "go.mau.fi/whatsmeow/types" "google.golang.org/protobuf/proto" - "time" ) type serviceMessage struct { @@ -157,3 +158,27 @@ func (service serviceMessage) UpdateMessage(ctx context.Context, request domainM response.Status = fmt.Sprintf("Update message success %s (server timestamp: %s)", request.Phone, ts.Timestamp) return response, nil } + +// StarMessage implements message.IMessageService. +func (service serviceMessage) StarMessage(ctx context.Context, request domainMessage.StarRequest) (err error) { + if err = validations.ValidateStarMessage(ctx, request); err != nil { + return err + } + + dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone) + if err != nil { + return err + } + + isFromMe := true + if len(request.MessageID) > 22 { + isFromMe = false + } + + patchInfo := appstate.BuildStar(dataWaRecipient.ToNonAD(), *service.WaCli.Store.ID, request.MessageID, isFromMe, request.IsStarred) + + if err = service.WaCli.SendAppState(patchInfo); err != nil { + return err + } + return nil +} diff --git a/src/services/send.go b/src/services/send.go index f465d0e..0e82961 100644 --- a/src/services/send.go +++ b/src/services/send.go @@ -3,6 +3,10 @@ package services import ( "context" "fmt" + "net/http" + "os" + "os/exec" + "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" @@ -19,9 +23,6 @@ import ( "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" "google.golang.org/protobuf/proto" - "net/http" - "os" - "os/exec" ) type serviceSend struct { @@ -36,6 +37,18 @@ func NewSendService(waCli *whatsmeow.Client, appService app.IAppService) domainS } } +// wrapSendMessage wraps the message sending process with message ID saving +func (service serviceSend) wrapSendMessage(ctx context.Context, recipient types.JID, msg *waE2E.Message, content string) (whatsmeow.SendResponse, error) { + ts, err := service.WaCli.SendMessage(ctx, recipient, msg) + if err != nil { + return whatsmeow.SendResponse{}, err + } + + utils.RecordMessage(ts.ID, service.WaCli.Store.ID.String(), content) + + return ts, nil +} + func (service serviceSend) SendText(ctx context.Context, request domainSend.MessageRequest) (response domainSend.GenericResponse, err error) { err = validations.ValidateSendMessage(ctx, request) if err != nil { @@ -62,32 +75,28 @@ func (service serviceSend) SendText(ctx context.Context, request domainSend.Mess // Reply message if request.ReplyMessageID != nil && *request.ReplyMessageID != "" { - participantJID := dataWaRecipient.String() - if len(*request.ReplyMessageID) < 28 { - firstDevice, err := service.appService.FirstDevice(ctx) - if err != nil { - return response, err - } - participantJID = firstDevice.Device - } - - msg.ExtendedTextMessage = &waE2E.ExtendedTextMessage{ - Text: proto.String(request.Message), - ContextInfo: &waE2E.ContextInfo{ - StanzaID: request.ReplyMessageID, - Participant: proto.String(participantJID), - QuotedMessage: &waE2E.Message{ - Conversation: proto.String(request.Message), + record, err := utils.FindRecordFromStorage(*request.ReplyMessageID) + if err == nil { // Only set reply context if we found the message ID + msg.ExtendedTextMessage = &waE2E.ExtendedTextMessage{ + Text: proto.String(request.Message), + ContextInfo: &waE2E.ContextInfo{ + StanzaID: request.ReplyMessageID, + Participant: proto.String(record.JID), + QuotedMessage: &waE2E.Message{ + Conversation: proto.String(record.MessageContent), + }, }, - }, - } + } - if len(parsedMentions) > 0 { - msg.ExtendedTextMessage.ContextInfo.MentionedJID = parsedMentions + if len(parsedMentions) > 0 { + msg.ExtendedTextMessage.ContextInfo.MentionedJID = parsedMentions + } + } else { + logrus.Warnf("Reply message ID %s not found in storage, continuing without reply context", *request.ReplyMessageID) } } - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, request.Message) if err != nil { return response, err } @@ -110,14 +119,31 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima var ( imagePath string imageThumbnail string + imageName string deletedItems []string + oriImagePath string ) - // Save image to server - oriImagePath := fmt.Sprintf("%s/%s", config.PathSendItems, request.Image.Filename) - err = fasthttp.SaveMultipartFile(request.Image, oriImagePath) - if err != nil { - return response, err + if request.ImageURL != nil && *request.ImageURL != "" { + // Download image from URL + imageData, fileName, err := utils.DownloadImageFromURL(*request.ImageURL) + oriImagePath = fmt.Sprintf("%s/%s", config.PathSendItems, fileName) + if err != nil { + return response, pkgError.InternalServerError(fmt.Sprintf("failed to download image from URL %v", err)) + } + imageName = fileName + err = os.WriteFile(oriImagePath, imageData, 0644) + if err != nil { + return response, pkgError.InternalServerError(fmt.Sprintf("failed to save downloaded image %v", err)) + } + } else if request.Image != nil { + // Save image to server + oriImagePath = fmt.Sprintf("%s/%s", config.PathSendItems, request.Image.Filename) + err = fasthttp.SaveMultipartFile(request.Image, oriImagePath) + if err != nil { + return response, err + } + imageName = request.Image.Filename } deletedItems = append(deletedItems, oriImagePath) @@ -129,7 +155,7 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima // Resize Thumbnail resizedImage := imaging.Resize(srcImage, 100, 0, imaging.Lanczos) - imageThumbnail = fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, request.Image.Filename) + imageThumbnail = fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, imageName) if err = imaging.Save(resizedImage, imageThumbnail); err != nil { return response, pkgError.InternalServerError(fmt.Sprintf("failed to save thumbnail %v", err)) } @@ -142,7 +168,7 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima return response, pkgError.InternalServerError(fmt.Sprintf("failed to open image %v", err)) } newImage := imaging.Resize(openImageBuffer, 600, 0, imaging.Lanczos) - newImagePath := fmt.Sprintf("%s/new-%s", config.PathSendItems, request.Image.Filename) + newImagePath := fmt.Sprintf("%s/new-%s", config.PathSendItems, imageName) if err = imaging.Save(newImage, newImagePath); err != nil { return response, pkgError.InternalServerError(fmt.Sprintf("failed to save image %v", err)) } @@ -180,7 +206,12 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima FileLength: proto.Uint64(uint64(len(dataWaImage))), ViewOnce: proto.Bool(request.ViewOnce), }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + + caption := "🖼️ Image" + if request.Caption != "" { + caption = "🖼️ " + request.Caption + } + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, caption) go func() { errDelete := utils.RemoveFile(0, deletedItems...) if errDelete != nil { @@ -228,7 +259,11 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File DirectPath: proto.String(uploadedFile.DirectPath), Caption: proto.String(request.Caption), }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + caption := "📄 Document" + if request.Caption != "" { + caption = "📄 " + request.Caption + } + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, caption) if err != nil { return response, err } @@ -336,7 +371,11 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid ThumbnailSHA256: dataWaThumbnail, ThumbnailDirectPath: proto.String(uploaded.DirectPath), }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + caption := "🎥 Video" + if request.Caption != "" { + caption = "🎥 " + request.Caption + } + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, caption) go func() { errDelete := utils.RemoveFile(1, deletedItems...) if errDelete != nil { @@ -368,7 +407,10 @@ func (service serviceSend) SendContact(ctx context.Context, request domainSend.C DisplayName: proto.String(request.ContactName), Vcard: proto.String(msgVCard), }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + + content := "👤 " + request.ContactName + + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content) if err != nil { return response, err } @@ -391,13 +433,18 @@ func (service serviceSend) SendLink(ctx context.Context, request domainSend.Link getMetaDataFromURL := utils.GetMetaDataFromURL(request.Link) msg := &waE2E.Message{ExtendedTextMessage: &waE2E.ExtendedTextMessage{ - Text: proto.String(fmt.Sprintf("%s\n%s", request.Caption, request.Link)), - Title: proto.String(getMetaDataFromURL.Title), - CanonicalURL: proto.String(request.Link), - MatchedText: proto.String(request.Link), - Description: proto.String(getMetaDataFromURL.Description), + Text: proto.String(fmt.Sprintf("%s\n%s", request.Caption, request.Link)), + Title: proto.String(getMetaDataFromURL.Title), + MatchedText: proto.String(request.Link), + Description: proto.String(getMetaDataFromURL.Description), + JPEGThumbnail: getMetaDataFromURL.ImageThumb, }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + + content := "🔗 " + request.Link + if request.Caption != "" { + content = "🔗 " + request.Caption + } + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content) if err != nil { return response, err } @@ -425,8 +472,10 @@ func (service serviceSend) SendLocation(ctx context.Context, request domainSend. }, } + content := "📍 " + request.Latitude + ", " + request.Longitude + // Send WhatsApp Message Proto - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content) if err != nil { return response, err } @@ -467,7 +516,9 @@ func (service serviceSend) SendAudio(ctx context.Context, request domainSend.Aud }, } - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + content := "🎵 Audio" + + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, msg, content) if err != nil { return response, err } @@ -487,7 +538,9 @@ func (service serviceSend) SendPoll(ctx context.Context, request domainSend.Poll return response, err } - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, service.WaCli.BuildPollCreation(request.Question, request.Options, request.MaxAnswer)) + content := "📊 " + request.Question + + ts, err := service.wrapSendMessage(ctx, dataWaRecipient, service.WaCli.BuildPollCreation(request.Question, request.Options, request.MaxAnswer), content) if err != nil { return response, err } @@ -497,6 +550,22 @@ func (service serviceSend) SendPoll(ctx context.Context, request domainSend.Poll return response, nil } +func (service serviceSend) SendPresence(ctx context.Context, request domainSend.PresenceRequest) (response domainSend.GenericResponse, err error) { + err = validations.ValidateSendPresence(ctx, request) + if err != nil { + return response, err + } + + err = service.WaCli.SendPresence(types.Presence(request.Type)) + if err != nil { + return response, err + } + + response.MessageID = "presence" + response.Status = fmt.Sprintf("Send presence success %s", request.Type) + return response, nil +} + func (service serviceSend) getMentionFromText(_ context.Context, messages string) (result []string) { mentions := utils.ContainsMention(messages) for _, mention := range mentions { @@ -506,7 +575,6 @@ func (service serviceSend) getMentionFromText(_ context.Context, messages string } } return result - } func (service serviceSend) uploadMedia(ctx context.Context, mediaType whatsmeow.MediaType, media []byte, recipient types.JID) (uploaded whatsmeow.UploadResponse, err error) { diff --git a/src/services/user.go b/src/services/user.go index fba20f6..5267534 100644 --- a/src/services/user.go +++ b/src/services/user.go @@ -1,16 +1,20 @@ package services import ( + "bytes" "context" "errors" "fmt" + "image" + "time" + domainUser "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/user" 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" "go.mau.fi/whatsmeow/types" - "time" ) type userService struct { @@ -190,3 +194,57 @@ func (service userService) MyPrivacySetting(_ context.Context) (response domainU response.Profile = string(resp.Profile) return response, nil } + +func (service userService) ChangeAvatar(ctx context.Context, request domainUser.ChangeAvatarRequest) (err error) { + whatsapp.MustLogin(service.WaCli) + + file, err := request.Avatar.Open() + if err != nil { + return err + } + defer file.Close() + + // Read original image + srcImage, err := imaging.Decode(file) + if err != nil { + return fmt.Errorf("failed to decode image: %v", err) + } + + // Get original dimensions + bounds := srcImage.Bounds() + width := bounds.Dx() + height := bounds.Dy() + + // Calculate new dimensions for 1:1 aspect ratio + size := width + if height < width { + size = height + } + if size > 640 { + size = 640 + } + + // Create a square crop from the center + left := (width - size) / 2 + top := (height - size) / 2 + croppedImage := imaging.Crop(srcImage, image.Rect(left, top, left+size, top+size)) + + // Resize if needed + if size > 640 { + croppedImage = imaging.Resize(croppedImage, 640, 640, imaging.Lanczos) + } + + // Convert to bytes + var buf bytes.Buffer + err = imaging.Encode(&buf, croppedImage, imaging.JPEG, imaging.JPEGQuality(80)) + if err != nil { + return fmt.Errorf("failed to encode image: %v", err) + } + + _, err = service.WaCli.SetGroupPhoto(types.JID{}, buf.Bytes()) + if err != nil { + return err + } + + return nil +} diff --git a/src/validations/message_validation.go b/src/validations/message_validation.go index 358cce2..35bff40 100644 --- a/src/validations/message_validation.go +++ b/src/validations/message_validation.go @@ -2,6 +2,7 @@ package validations import ( "context" + "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" @@ -75,3 +76,17 @@ func ValidateDeleteMessage(ctx context.Context, request domainMessage.DeleteRequ return nil } + +func ValidateStarMessage(ctx context.Context, request domainMessage.StarRequest) error { + err := validation.ValidateStructWithContext(ctx, &request, + validation.Field(&request.Phone, validation.Required), + validation.Field(&request.MessageID, validation.Required), + validation.Field(&request.IsStarred, validation.Required), + ) + + if err != nil { + return pkgError.ValidationError(err.Error()) + } + + return nil +} diff --git a/src/validations/send_validation.go b/src/validations/send_validation.go index 137704c..9304faf 100644 --- a/src/validations/send_validation.go +++ b/src/validations/send_validation.go @@ -3,6 +3,8 @@ package validations import ( "context" "fmt" + "sort" + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" @@ -26,21 +28,37 @@ func ValidateSendMessage(ctx context.Context, request domainSend.MessageRequest) func ValidateSendImage(ctx context.Context, request domainSend.ImageRequest) error { err := validation.ValidateStructWithContext(ctx, &request, validation.Field(&request.Phone, validation.Required), - validation.Field(&request.Image, validation.Required), ) if err != nil { return pkgError.ValidationError(err.Error()) } - availableMimes := map[string]bool{ - "image/jpeg": true, - "image/jpg": true, - "image/png": true, + if request.Image == nil && (request.ImageURL == nil || *request.ImageURL == "") { + return pkgError.ValidationError("either Image or ImageURL must be provided") } - if !availableMimes[request.Image.Header.Get("Content-Type")] { - return pkgError.ValidationError("your image is not allowed. please use jpg/jpeg/png") + if request.Image != nil { + availableMimes := map[string]bool{ + "image/jpeg": true, + "image/jpg": true, + "image/png": true, + } + + if !availableMimes[request.Image.Header.Get("Content-Type")] { + return pkgError.ValidationError("your image is not allowed. please use jpg/jpeg/png") + } + } + + if request.ImageURL != nil { + if *request.ImageURL == "" { + return pkgError.ValidationError("ImageURL cannot be empty") + } + + err := validation.Validate(*request.ImageURL, is.URL) + if err != nil { + return pkgError.ValidationError("ImageURL must be a valid URL") + } } return nil @@ -145,18 +163,16 @@ func ValidateSendAudio(ctx context.Context, request domainSend.AudioRequest) err } availableMimes := map[string]bool{ - "audio/aac": true, - "audio/amr": true, - "audio/flac": true, - "audio/m4a": true, - "audio/m4r": true, - "audio/mp3": true, - "audio/mpeg": true, - "audio/ogg": true, - + "audio/aac": true, + "audio/amr": true, + "audio/flac": true, + "audio/m4a": true, + "audio/m4r": true, + "audio/mp3": true, + "audio/mpeg": true, + "audio/ogg": true, "audio/wma": true, "audio/x-ms-wma": true, - "audio/wav": true, "audio/vnd.wav": true, "audio/vnd.wave": true, @@ -165,7 +181,15 @@ func ValidateSendAudio(ctx context.Context, request domainSend.AudioRequest) err "audio/x-wav": true, } availableMimesStr := "" + + // Sort MIME types for consistent error message order + mimeKeys := make([]string, 0, len(availableMimes)) for k := range availableMimes { + mimeKeys = append(mimeKeys, k) + } + sort.Strings(mimeKeys) + + for _, k := range mimeKeys { availableMimesStr += k + "," } @@ -177,11 +201,15 @@ func ValidateSendAudio(ctx context.Context, request domainSend.AudioRequest) err } func ValidateSendPoll(ctx context.Context, request domainSend.PollRequest) error { + // Validate options first to ensure it is not blank before validating MaxAnswer + if len(request.Options) == 0 { + return pkgError.ValidationError("options: cannot be blank.") + } + err := validation.ValidateStructWithContext(ctx, &request, validation.Field(&request.Phone, validation.Required), validation.Field(&request.Question, validation.Required), - validation.Field(&request.Options, validation.Required), validation.Field(&request.Options, validation.Each(validation.Required)), validation.Field(&request.MaxAnswer, validation.Required), @@ -203,5 +231,16 @@ func ValidateSendPoll(ctx context.Context, request domainSend.PollRequest) error } return nil +} +func ValidateSendPresence(ctx context.Context, request domainSend.PresenceRequest) error { + err := validation.ValidateStructWithContext(ctx, &request, + validation.Field(&request.Type, validation.In("available", "unavailable")), + ) + + if err != nil { + return pkgError.ValidationError(err.Error()) + } + + return nil } diff --git a/src/validations/send_validation_test.go b/src/validations/send_validation_test.go index a9810cd..cccf463 100644 --- a/src/validations/send_validation_test.go +++ b/src/validations/send_validation_test.go @@ -2,12 +2,13 @@ package validations import ( "context" + "mime/multipart" + "testing" + domainMessage "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/message" domainSend "github.com/aldinokemal/go-whatsapp-web-multidevice/domains/send" pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error" "github.com/stretchr/testify/assert" - "mime/multipart" - "testing" ) func TestValidateSendMessage(t *testing.T) { @@ -91,7 +92,7 @@ func TestValidateSendImage(t *testing.T) { Phone: "1728937129312@s.whatsapp.net", Image: nil, }}, - err: pkgError.ValidationError("image: cannot be blank."), + err: pkgError.ValidationError("either Image or ImageURL must be provided"), }, { name: "should error with invalid image type", @@ -528,3 +529,183 @@ func TestValidateSendLocation(t *testing.T) { }) } } + +func TestValidateSendAudio(t *testing.T) { + audio := &multipart.FileHeader{ + Filename: "sample-audio.mp3", + Size: 100, + Header: map[string][]string{"Content-Type": {"audio/mp3"}}, + } + + type args struct { + request domainSend.AudioRequest + } + tests := []struct { + name string + args args + err any + }{ + { + name: "should success with normal condition", + args: args{request: domainSend.AudioRequest{ + Phone: "1728937129312@s.whatsapp.net", + Audio: audio, + }}, + err: nil, + }, + { + name: "should error with empty phone", + args: args{request: domainSend.AudioRequest{ + Phone: "", + Audio: audio, + }}, + err: pkgError.ValidationError("phone: cannot be blank."), + }, + { + name: "should error with empty audio", + args: args{request: domainSend.AudioRequest{ + Phone: "1728937129312@s.whatsapp.net", + Audio: nil, + }}, + err: pkgError.ValidationError("audio: cannot be blank."), + }, + { + name: "should error with invalid audio type", + args: args{request: domainSend.AudioRequest{ + Phone: "1728937129312@s.whatsapp.net", + Audio: &multipart.FileHeader{ + Filename: "sample-audio.txt", + Size: 100, + Header: map[string][]string{"Content-Type": {"text/plain"}}, + }, + }}, + err: pkgError.ValidationError("your audio type is not allowed. please use (audio/aac,audio/amr,audio/flac,audio/m4a,audio/m4r,audio/mp3,audio/mpeg,audio/ogg,audio/vnd.wav,audio/vnd.wave,audio/wav,audio/wave,audio/wma,audio/x-ms-wma,audio/x-pn-wav,audio/x-wav,)"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSendAudio(context.Background(), tt.args.request) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestValidateSendPoll(t *testing.T) { + type args struct { + request domainSend.PollRequest + } + tests := []struct { + name string + args args + err any + }{ + { + name: "should success with normal condition", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "What is your favorite color?", + Options: []string{"Red", "Blue", "Green"}, + MaxAnswer: 1, + }}, + err: nil, + }, + { + name: "should error with empty phone", + args: args{request: domainSend.PollRequest{ + Phone: "", + Question: "What is your favorite color?", + Options: []string{"Red", "Blue", "Green"}, + MaxAnswer: 1, + }}, + err: pkgError.ValidationError("phone: cannot be blank."), + }, + { + name: "should error with empty question", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "", + Options: []string{"Red", "Blue", "Green"}, + MaxAnswer: 1, + }}, + err: pkgError.ValidationError("question: cannot be blank."), + }, + { + name: "should error with empty options", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "What is your favorite color?", + Options: []string{}, + MaxAnswer: 5, + }}, + err: pkgError.ValidationError("options: cannot be blank."), + }, + { + name: "should error with duplicate options", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "What is your favorite color?", + Options: []string{"Red", "Red", "Green"}, + MaxAnswer: 1, + }}, + err: pkgError.ValidationError("options should be unique"), + }, + { + name: "should error with max answer greater than options", + args: args{request: domainSend.PollRequest{ + Phone: "1728937129312@s.whatsapp.net", + Question: "What is your favorite color?", + Options: []string{"Red", "Blue", "Green"}, + MaxAnswer: 5, + }}, + err: pkgError.ValidationError("max_answer: must be no greater than 3."), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSendPoll(context.Background(), tt.args.request) + assert.Equal(t, tt.err, err) + }) + } +} + +func TestValidateSendPresence(t *testing.T) { + type args struct { + request domainSend.PresenceRequest + } + tests := []struct { + name string + args args + err any + }{ + { + name: "should success with available type", + args: args{request: domainSend.PresenceRequest{ + Type: "available", + }}, + err: nil, + }, + { + name: "should success with unavailable type", + args: args{request: domainSend.PresenceRequest{ + Type: "unavailable", + }}, + err: nil, + }, + { + name: "should error with invalid type", + args: args{request: domainSend.PresenceRequest{ + Type: "invalid", + }}, + err: pkgError.ValidationError("type: must be a valid value."), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSendPresence(context.Background(), tt.args.request) + assert.Equal(t, tt.err, err) + }) + } +} diff --git a/src/views/assets/app.css b/src/views/assets/app.css new file mode 100644 index 0000000..f9ce165 --- /dev/null +++ b/src/views/assets/app.css @@ -0,0 +1,282 @@ +:root { + --primary-color: #00A884; /* WhatsApp's new brand green */ + --secondary-color: #008069; /* WhatsApp's darker green */ + --tertiary-color: #075E54; /* WhatsApp's darkest green */ + --background-color: #EFEAE2; /* WhatsApp's authentic background */ + --card-hover-color: #ffffff; + --text-color: #111B21; /* WhatsApp's text color */ + --gradient-start: #00A884; + --gradient-end: #008069; + --message-background: #FFFFFF; + --success-green: #00A884; +} + +body { + background-color: var(--background-color) !important; + color: var(--text-color) !important; + font-family: "Helvetica Neue", "Helvetica Neue", Helvetica, Arial, sans-serif !important; +} + +.container { + padding: 2em 1em !important; + max-width: 1400px !important; + margin: 0 auto !important; +} + +.main-header { + background: var(--primary-color) !important; + color: white !important; + padding: 1.5em 2em !important; + border-radius: 24px !important; + margin-bottom: 2em !important; + box-shadow: + 0 20px 40px rgba(0, 168, 132, 0.2), + inset 0 0 80px rgba(255, 255, 255, 0.15) !important; + position: relative; + overflow: hidden; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s ease-in-out; +} + +.main-header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient( + circle at top right, + rgba(255,255,255,0.2) 0%, + rgba(255,255,255,0) 60% + ), + linear-gradient( + 45deg, + rgba(255,255,255,0.1) 0%, + rgba(255,255,255,0) 70% + ); + opacity: 0.8; + z-index: 1; +} + +.main-header::after { + content: ''; + position: absolute; + top: -50%; + left: -50%; + right: -50%; + bottom: -50%; + background: + radial-gradient( + circle, + rgba(255,255,255,0.1) 0%, + transparent 70% + ); + animation: rotate 20s linear infinite; + z-index: 0; +} + +@keyframes rotate { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.main-header .ui.header { + position: relative; + z-index: 2; + margin: 0 !important; + font-weight: 700 !important; + font-size: clamp(1.5em, 3vw, 1.8em) !important; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + letter-spacing: 0.5px; +} + +.main-header .title-container { + display: flex; + align-items: center; + gap: 8px; + color: white; +} + +.main-header .whatsapp.icon { + font-size: 1em !important; + animation: float 4s ease-in-out infinite; + backdrop-filter: blur(5px); + transition: all 0.3s ease; +} + +.main-header .whatsapp.icon:hover { + transform: scale(1.1); + background: rgba(255, 255, 255, 0.2); +} + +.main-header .version-label { + margin-top: 4px !important; + background: rgba(255,255,255,0.12); + color: white; + font-size: 0.5em; + padding: 4px 10px; + border-radius: 12px; + backdrop-filter: blur(4px); + border: 1px solid rgba(255, 255, 255, 0.15); + transition: all 0.25s ease; + letter-spacing: 0.5px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.main-header .version-label:hover { + background: rgba(255,255,255,0.2); + transform: translateY(-2px); +} + +@media (min-width: 768px) { + .main-header { + padding: 2em !important; + } + + .main-header .ui.header { + flex-direction: column; + } + + .main-header .version-label { + margin-top: 4px !important; + } +} + +@keyframes float { + 0% { transform: translateY(0px) rotate(0deg); } + 50% { transform: translateY(-8px) rotate(5deg); } + 100% { transform: translateY(0px) rotate(0deg); } +} + +.ui.header { + font-weight: 600 !important; + color: var(--text-color) !important; +} + +.ui.cards > .card { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05) !important; + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; + border-radius: 16px !important; + margin: 0.7em !important; + background-color: var(--message-background) !important; + border: 1px solid rgba(18, 140, 126, 0.1); +} + +.ui.cards > .card:hover { + transform: translateY(-8px) !important; + box-shadow: 0 12px 24px rgba(18, 140, 126, 0.15) !important; + background-color: var(--card-hover-color) !important; +} + +.ui.horizontal.divider { + font-size: 1.3em !important; + color: var(--secondary-color) !important; + margin: 2.5em 0 !important; + font-weight: 700 !important; + text-transform: uppercase; + letter-spacing: 1px; +} + +.ui.success.message { + border-radius: 16px !important; + box-shadow: 0 4px 8px rgba(37, 211, 102, 0.1) !important; + border: 1px solid rgba(37, 211, 102, 0.2) !important; + background-color: rgba(231, 247, 232, 0.8) !important; + color: var(--success-green) !important; + animation: slideIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); + backdrop-filter: blur(10px); +} + +@keyframes slideIn { + from { transform: translateX(-30px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +.ui.button { + border-radius: 12px !important; + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; + /* background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%) !important; */ + background: var(--primary-color) !important; + color: white !important; + font-weight: 600 !important; + letter-spacing: 0.5px; +} + +.ui.button:hover { + transform: translateY(-3px) !important; + box-shadow: 0 8px 16px rgba(0, 168, 132, 0.2) !important; + filter: brightness(0.95); +} + +.ui.form input, .ui.form textarea { + border-radius: 12px !important; + border: 2px solid rgba(18, 140, 126, 0.1) !important; + transition: all 0.3s ease; +} + +.ui.form input:focus, .ui.form textarea:focus { + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 3px rgba(0, 168, 132, 0.1) !important; +} + +.ui.toast-container { + padding: 1.5em !important; +} + +.ui.toast { + border-radius: 16px !important; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1) !important; + background-color: var(--message-background) !important; + border: 1px solid rgba(18, 140, 126, 0.1); +} + +.ui.grid { + margin: -0.7em !important; +} + +.ui.grid > .column { + padding: 0.7em !important; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); } +} + +.ui.cards { + animation: fadeIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +.ui.modal { + border-radius: 20px !important; + background-color: var(--message-background) !important; + overflow: hidden; +} + +.ui.modal > .header { + border-radius: 20px 20px 0 0 !important; + background: var(--gradient-start) !important; + color: white !important; + padding: 1.5em !important; +} + +.ui.modal > .actions { + border-radius: 0 0 20px 20px !important; + background-color: rgba(248, 248, 248, 0.8) !important; + backdrop-filter: blur(10px); +} + +.ui.modal > .actions > .ui.button { + background: var(--primary-color) !important; + color: white !important; + font-weight: 600 !important; + letter-spacing: 0.5px; +} \ No newline at end of file diff --git a/src/views/components/AccountAvatar.js b/src/views/components/AccountAvatar.js index e9ab15f..9d7b292 100644 --- a/src/views/components/AccountAvatar.js +++ b/src/views/components/AccountAvatar.js @@ -25,7 +25,18 @@ export default { this.handleReset(); $('#modalUserAvatar').modal('show'); }, + isValidForm() { + if (!this.phone.trim()) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { await this.submitApi(); showSuccessInfo("Avatar fetched") @@ -56,6 +67,7 @@ export default { template: `
+ Account
Avatar
You can search someone avatar by phone @@ -89,8 +101,8 @@ export default {
- diff --git a/src/views/components/AccountChangeAvatar.js b/src/views/components/AccountChangeAvatar.js new file mode 100644 index 0000000..6c8596b --- /dev/null +++ b/src/views/components/AccountChangeAvatar.js @@ -0,0 +1,113 @@ +export default { + name: 'AccountChangeAvatar', + data() { + return { + loading: false, + selected_file: null, + preview_url: null + } + }, + methods: { + openModal() { + $('#modalChangeAvatar').modal({ + onApprove: function () { + return false; + } + }).modal('show'); + }, + isValidForm() { + return this.selected_file !== null; + }, + async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + + try { + let response = await this.submitApi() + showSuccessInfo(response) + $('#modalChangeAvatar').modal('hide'); + } catch (err) { + showErrorInfo(err) + } + }, + async submitApi() { + this.loading = true; + try { + let payload = new FormData(); + payload.append('avatar', $("#file_avatar")[0].files[0]) + + let response = await window.http.post(`/user/avatar`, payload) + this.handleReset(); + return response.data.message; + } catch (error) { + if (error.response) { + throw new Error(error.response.data.message); + } + throw new Error(error.message); + } finally { + this.loading = false; + } + }, + handleReset() { + this.preview_url = null; + this.selected_file = null; + $("#file_avatar").val(''); + }, + handleImageChange(event) { + const file = event.target.files[0]; + if (file) { + this.preview_url = URL.createObjectURL(file); + this.selected_file = file.name; + } + } + }, + template: ` +
+
+ Account +
Change Avatar
+
+ Update your profile picture +
+
+
+ + + + ` +} diff --git a/src/views/components/AccountPrivacy.js b/src/views/components/AccountPrivacy.js index 46061b8..58d35ab 100644 --- a/src/views/components/AccountPrivacy.js +++ b/src/views/components/AccountPrivacy.js @@ -30,6 +30,7 @@ export default { template: `
+ Account
My Privacy Setting
Get your privacy settings diff --git a/src/views/components/AccountUserInfo.js b/src/views/components/AccountUserInfo.js index 8e307bf..56b5494 100644 --- a/src/views/components/AccountUserInfo.js +++ b/src/views/components/AccountUserInfo.js @@ -28,7 +28,18 @@ export default { this.handleReset(); $('#modalUserInfo').modal('show'); }, + isValidForm() { + if (!this.phone.trim()) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { await this.submitApi(); showSuccessInfo("Info fetched") @@ -63,6 +74,7 @@ export default { template: `
+ Account
User Info
You can search someone user info by phone @@ -81,8 +93,8 @@ export default {
- diff --git a/src/views/components/AppLogin.js b/src/views/components/AppLogin.js index 3300af6..76de2d6 100644 --- a/src/views/components/AppLogin.js +++ b/src/views/components/AppLogin.js @@ -41,6 +41,7 @@ export default { template: `
+ App
Login
Scan your QR code to access all API capabilities. diff --git a/src/views/components/AppLoginWithCode.js b/src/views/components/AppLoginWithCode.js index 8421e02..8495e42 100644 --- a/src/views/components/AppLoginWithCode.js +++ b/src/views/components/AppLoginWithCode.js @@ -2,8 +2,8 @@ export default { name: 'AppLoginWithCode', props: { connected: { - type: Boolean, - default: false, + type: Array, + default: [], } }, watch: { @@ -62,6 +62,7 @@ export default { template: `
+ App
Login with Code
Enter your pairing code to log in and access your devices. diff --git a/src/views/components/AppLogout.js b/src/views/components/AppLogout.js index 1e87c8f..41956eb 100644 --- a/src/views/components/AppLogout.js +++ b/src/views/components/AppLogout.js @@ -28,6 +28,7 @@ export default { template: `
+ App
Logout
Remove your login session in application diff --git a/src/views/components/AppReconnect.js b/src/views/components/AppReconnect.js index 1d41451..695f3f1 100644 --- a/src/views/components/AppReconnect.js +++ b/src/views/components/AppReconnect.js @@ -26,6 +26,7 @@ export default { template: `
+ App
Reconnect
Please reconnect to the WhatsApp service if your API doesn't work or if your app is down. diff --git a/src/views/components/GroupCreate.js b/src/views/components/GroupCreate.js index e4c66ed..af14d03 100644 --- a/src/views/components/GroupCreate.js +++ b/src/views/components/GroupCreate.js @@ -15,6 +15,17 @@ export default { } }).modal('show'); }, + isValidForm() { + if (!this.title.trim()) { + return false; + } + + if (this.participants.length < 1 || this.participants.every(participant => !participant.trim())) { + return false; + } + + return true; + }, handleAddParticipant() { this.participants.push('') }, @@ -22,6 +33,9 @@ export default { this.participants.splice(index, 1) }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } try { let response = await this.submitApi() showSuccessInfo(response) @@ -103,11 +117,11 @@ export default {
-
+
+
` diff --git a/src/views/components/GroupJoinWithLink.js b/src/views/components/GroupJoinWithLink.js index 77eb9bf..88639af 100644 --- a/src/views/components/GroupJoinWithLink.js +++ b/src/views/components/GroupJoinWithLink.js @@ -14,7 +14,24 @@ export default { } }).modal('show'); }, + isValidForm() { + if (!this.link.trim()) { + return false; + } + + // should valid URL + try { + new URL(this.link); + } catch (error) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } try { let response = await this.submitApi() showSuccessInfo(response) @@ -72,11 +89,11 @@ export default {
-
+
+
` diff --git a/src/views/components/GroupManageParticipants.js b/src/views/components/GroupManageParticipants.js index 3c49ff9..483b0d9 100644 --- a/src/views/components/GroupManageParticipants.js +++ b/src/views/components/GroupManageParticipants.js @@ -21,6 +21,13 @@ export default { }, }).modal('show'); }, + isValidForm() { + if (this.participants.length < 1 || this.participants.every(participant => !participant.trim())) { + return false; + } + + return true; + }, handleAddParticipant() { this.participants.push(''); }, @@ -28,6 +35,10 @@ export default { this.participants.splice(index, 1); }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { let response = await this.submitApi(); showSuccessInfo(response); @@ -137,11 +148,11 @@ export default {
-
+
+
`, diff --git a/src/views/components/MessageDelete.js b/src/views/components/MessageDelete.js index 12cd6e6..15f1bc9 100644 --- a/src/views/components/MessageDelete.js +++ b/src/views/components/MessageDelete.js @@ -26,7 +26,22 @@ export default { } }).modal('show'); }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.message_id.trim()) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { let response = await this.submitApi() showSuccessInfo(response) @@ -89,11 +104,11 @@ export default {
-
+
+
` diff --git a/src/views/components/MessageReact.js b/src/views/components/MessageReact.js index fdc23eb..3aa94a6 100644 --- a/src/views/components/MessageReact.js +++ b/src/views/components/MessageReact.js @@ -27,7 +27,21 @@ export default { } }).modal('show'); }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.message_id.trim() || !this.emoji.trim()) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } try { let response = await this.submitApi() showSuccessInfo(response) @@ -94,11 +108,11 @@ export default {
-
Send -
+
` diff --git a/src/views/components/MessageRevoke.js b/src/views/components/MessageRevoke.js index 834706f..4a4f709 100644 --- a/src/views/components/MessageRevoke.js +++ b/src/views/components/MessageRevoke.js @@ -26,7 +26,21 @@ export default { } }).modal('show'); }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.message_id.trim()) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } try { let response = await this.submitApi() showSuccessInfo(response) @@ -86,11 +100,11 @@ export default {
-
Send -
+
` diff --git a/src/views/components/MessageUpdate.js b/src/views/components/MessageUpdate.js index a949772..eba8ad2 100644 --- a/src/views/components/MessageUpdate.js +++ b/src/views/components/MessageUpdate.js @@ -27,7 +27,21 @@ export default { } }).modal('show'); }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.message_id.trim() || !this.new_message.trim()) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } try { let response = await this.submitApi() showSuccessInfo(response) @@ -95,11 +109,11 @@ export default {
-
Update -
+
` diff --git a/src/views/components/SendAudio.js b/src/views/components/SendAudio.js index c237e1a..b68f63e 100644 --- a/src/views/components/SendAudio.js +++ b/src/views/components/SendAudio.js @@ -10,6 +10,7 @@ export default { phone: '', type: window.TYPEUSER, loading: false, + selectedFileName: null } }, computed: { @@ -25,7 +26,22 @@ export default { } }).modal('show'); }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.selectedFileName) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { let response = await this.submitApi() showSuccessInfo(response) @@ -56,7 +72,14 @@ export default { this.phone = ''; this.type = window.TYPEUSER; $("#file_audio").val(''); + this.selectedFileName = null; }, + handleFileChange(event) { + const file = event.target.files[0]; + if (file) { + this.selectedFileName = file.name; + } + } }, template: `
@@ -80,20 +103,26 @@ export default {
- + +
+
+ + Selected file: {{ selectedFileName }} +
+
-
+
+
` diff --git a/src/views/components/SendContact.js b/src/views/components/SendContact.js index 21b899d..7758302 100644 --- a/src/views/components/SendContact.js +++ b/src/views/components/SendContact.js @@ -27,6 +27,21 @@ export default { } }).modal('show'); }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.card_name.trim()) { + return false; + } + + if (!this.card_phone.trim()) { + return false; + } + + return true; + }, async handleSubmit() { try { this.loading = true; @@ -40,6 +55,10 @@ export default { } }, async submitApi() { + if (!this.isValidForm() || this.loading) { + return; + } + this.loading = true; try { const payload = { @@ -100,11 +119,11 @@ export default {
-
+
+
` diff --git a/src/views/components/SendFile.js b/src/views/components/SendFile.js index affbb4c..a5de1dc 100644 --- a/src/views/components/SendFile.js +++ b/src/views/components/SendFile.js @@ -17,6 +17,7 @@ export default { type: window.TYPEUSER, phone: '', loading: false, + selectedFileName: null } }, computed: { @@ -32,7 +33,22 @@ export default { } }).modal('show'); }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.selectedFileName) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { let response = await this.submitApi() showSuccessInfo(response) @@ -64,8 +80,15 @@ export default { this.caption = ''; this.phone = ''; this.type = window.TYPEUSER; + this.selectedFileName = null; $("#file_file").val(''); }, + handleFileChange(event) { + const file = event.target.files[0]; + if (file) { + this.selectedFileName = file.name; + } + } }, template: `
@@ -96,20 +119,26 @@ export default {
- + +
+
+ + Selected file: {{ selectedFileName }} +
+
-
+
+
` diff --git a/src/views/components/SendImage.js b/src/views/components/SendImage.js index 5c276c9..0827842 100644 --- a/src/views/components/SendImage.js +++ b/src/views/components/SendImage.js @@ -13,13 +13,15 @@ export default { caption: '', type: window.TYPEUSER, loading: false, - selected_file: null + selected_file: null, + image_url: null, + preview_url: null } }, computed: { phone_id() { return this.phone + this.type; - } + }, }, methods: { openModal() { @@ -29,7 +31,25 @@ export default { } }).modal('show'); }, + isShowAttributes() { + return this.type !== window.TYPESTATUS; + }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.selected_file && !this.image_url) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { let response = await this.submitApi() showSuccessInfo(response) @@ -46,8 +66,16 @@ export default { payload.append("view_once", this.view_once) payload.append("compress", this.compress) payload.append("caption", this.caption) - payload.append('image', $("#file_image")[0].files[0]) - + + const fileInput = $("#file_image"); + if (fileInput.length > 0 && fileInput[0].files.length > 0) { + const file = fileInput[0].files[0]; + payload.append('image', file); + } + if (this.image_url) { + payload.append('image_url', this.image_url) + } + let response = await window.http.post(`/send/image`, payload) this.handleReset(); return response.data.message; @@ -65,9 +93,25 @@ export default { this.compress = false; this.phone = ''; this.caption = ''; - this.type = window.TYPEUSER; + this.preview_url = null; + this.selected_file = null; + this.image_url = null; $("#file_image").val(''); }, + handleImageChange(event) { + const file = event.target.files[0]; + if (file) { + this.preview_url = URL.createObjectURL(file); + // Add small delay to allow DOM update before scrolling + setTimeout(() => { + const modalContent = document.querySelector('#modalSendImage .content'); + if (modalContent) { + modalContent.scrollTop = modalContent.scrollHeight; + } + this.selected_file = file.name; + }, 100); + } + } }, template: `
@@ -88,45 +132,55 @@ export default {
Send Image
-
+
- +
-
+
-
+
+
+ + +
+
or you can upload image from your device
- + +
+ +
-
+
+
` diff --git a/src/views/components/SendLocation.js b/src/views/components/SendLocation.js index 68c3be2..226bbc1 100644 --- a/src/views/components/SendLocation.js +++ b/src/views/components/SendLocation.js @@ -17,6 +17,22 @@ export default { computed: { phone_id() { return this.phone + this.type; + }, + isValidForm() { + // Validate phone number is not empty except for status type + const isPhoneValid = this.type === window.TYPESTATUS || this.phone.trim().length > 0; + + // Validate latitude is between -90 and 90 + const isLatitudeValid = !isNaN(this.latitude) && + parseFloat(this.latitude) >= -90 && + parseFloat(this.latitude) <= 90; + + // Validate longitude is between -180 and 180 + const isLongitudeValid = !isNaN(this.longitude) && + parseFloat(this.longitude) >= -180 && + parseFloat(this.longitude) <= 180; + + return isPhoneValid && isLatitudeValid && isLongitudeValid; } }, methods: { @@ -87,22 +103,22 @@ export default {
-
-
-
+
+
` diff --git a/src/views/components/SendMessage.js b/src/views/components/SendMessage.js index 63c1616..2fffa25 100644 --- a/src/views/components/SendMessage.js +++ b/src/views/components/SendMessage.js @@ -17,7 +17,7 @@ export default { computed: { phone_id() { return this.phone + this.type; - } + }, }, methods: { openModal() { @@ -27,13 +27,29 @@ export default { } }).modal('show'); }, + isShowReplyId() { + return this.type !== window.TYPESTATUS; + }, + isValidForm() { + // Validate phone number is not empty except for status type + const isPhoneValid = this.type === window.TYPESTATUS || this.phone.trim().length > 0; + + // Validate message is not empty and has reasonable length + const isMessageValid = this.text.trim().length > 0 && this.text.length <= 4096; + + return isPhoneValid && isMessageValid + }, async handleSubmit() { + // Add validation check here to prevent submission when form is invalid + if (!this.isValidForm() || this.loading) { + return; + } try { - let response = await this.submitApi() - showSuccessInfo(response) + const response = await this.submitApi(); + showSuccessInfo(response); $('#modalSendMessage').modal('hide'); } catch (err) { - showErrorInfo(err) + showErrorInfo(err); } }, async submitApi() { @@ -41,20 +57,20 @@ export default { try { const payload = { phone: this.phone_id, - message: this.text, - } + message: this.text.trim(), + }; if (this.reply_message_id !== '') { payload.reply_message_id = this.reply_message_id; } - let response = await window.http.post(`/send/message`, payload) + const response = await window.http.post('/send/message', payload); this.handleReset(); return response.data.message; } catch (error) { - if (error.response) { + if (error.response?.data?.message) { throw new Error(error.response.data.message); } - throw new Error(error.message); + throw error; } finally { this.loading = false; } @@ -62,7 +78,6 @@ export default { handleReset() { this.phone = ''; this.text = ''; - this.type = window.TYPEUSER; this.reply_message_id = ''; }, }, @@ -85,8 +100,8 @@ export default {
- -
+ +
-
+
+
` diff --git a/src/views/components/SendPoll.js b/src/views/components/SendPoll.js index ebedf32..eec01bd 100644 --- a/src/views/components/SendPoll.js +++ b/src/views/components/SendPoll.js @@ -29,7 +29,26 @@ export default { } }).modal('show'); }, + isValidForm() { + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + return false; + } + + if (!this.question.trim()) { + return false; + } + + if (this.options.some(option => option.trim() === '')) { + return false; + } + + return true; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { let response = await this.submitApi() window.showSuccessInfo(response) @@ -124,11 +143,11 @@ export default {
-
+
+
` diff --git a/src/views/components/SendPresence.js b/src/views/components/SendPresence.js new file mode 100644 index 0000000..1e57764 --- /dev/null +++ b/src/views/components/SendPresence.js @@ -0,0 +1,86 @@ +export default { + name: 'SendPresence', + data() { + return { + type: 'available', + loading: false, + } + }, + methods: { + openModal() { + $('#modalSendPresence').modal({ + onApprove: function () { + return false; + } + }).modal('show'); + }, + async handleSubmit() { + if (this.loading) { + return; + } + + try { + let response = await this.submitApi() + showSuccessInfo(response) + $('#modalSendPresence').modal('hide'); + } catch (err) { + showErrorInfo(err) + } + }, + async submitApi() { + this.loading = true; + try { + let payload = { + type: this.type + } + let response = await window.http.post(`/send/presence`, payload) + return response.data.message; + } catch (error) { + if (error.response) { + throw new Error(error.response.data.message); + } + throw new Error(error.message); + } finally { + this.loading = false; + } + } + }, + template: ` +
+
+ Send +
Send Presence
+
+ Set
available
or
unavailable
+
+
+
+ + + + ` +} \ No newline at end of file diff --git a/src/views/components/SendVideo.js b/src/views/components/SendVideo.js index 6a93593..d00dc60 100644 --- a/src/views/components/SendVideo.js +++ b/src/views/components/SendVideo.js @@ -5,7 +5,6 @@ export default { components: { FormRecipient }, - // define props props: { maxVideoSize: { type: String, @@ -20,12 +19,13 @@ export default { type: window.TYPEUSER, phone: '', loading: false, + selectedFileName: null, } }, computed: { phone_id() { return this.phone + this.type; - } + }, }, methods: { openModal() { @@ -35,7 +35,36 @@ export default { } }).modal('show'); }, + isShowAttributes() { + return this.type !== window.TYPESTATUS; + }, + isValidForm() { + let isValid = true; + + if (this.type !== window.TYPESTATUS && !this.phone.trim()) { + isValid = false; + } + + const fileInput = $("#file_video")[0]; + if (!fileInput || !fileInput.files || !fileInput.files[0]) { + console.log('fileInput', fileInput); + isValid = false; + } else { + const videoFile = fileInput.files[0]; + // Validate file type + if (!videoFile.type.startsWith('video/')) { + isValid = false; + console.log('videoFile', videoFile); + } + } + + return isValid; + }, async handleSubmit() { + if (!this.isValidForm() || this.loading) { + return; + } + try { let response = await this.submitApi() showSuccessInfo(response) @@ -49,7 +78,7 @@ export default { try { let payload = new FormData(); payload.append("phone", this.phone_id) - payload.append("caption", this.caption) + payload.append("caption", this.caption.trim()) payload.append("view_once", this.view_once) payload.append("compress", this.compress) payload.append('video', $("#file_video")[0].files[0]) @@ -70,9 +99,15 @@ export default { this.view_once = false; this.compress = false; this.phone = ''; - this.type = window.TYPEUSER; + this.selectedFileName = null; $("#file_video").val(''); }, + handleFileChange(event) { + const file = event.target.files[0]; + if (file) { + this.selectedFileName = file.name; + } + } }, template: `
@@ -96,21 +131,21 @@ export default {
- +
-
+
-
+
@@ -119,20 +154,27 @@ export default {
- + +
+
+ + Selected file: {{ selectedFileName }} +
+
-
+
+
` diff --git a/src/views/components/generic/FormRecipient.js b/src/views/components/generic/FormRecipient.js index d763e52..e937de7 100644 --- a/src/views/components/generic/FormRecipient.js +++ b/src/views/components/generic/FormRecipient.js @@ -9,6 +9,10 @@ export default { type: String, required: true }, + showStatus: { + type: Boolean, + default: false + } }, data() { return { @@ -18,18 +22,33 @@ export default { computed: { phone_id() { return this.phone + this.type; + }, + showPhoneInput() { + return this.type !== window.TYPESTATUS; + }, + filteredRecipientTypes() { + return this.recipientTypes.filter(type => { + if (!this.showStatus && type.value === window.TYPESTATUS) { + return false; + } + return true; + }); } }, mounted() { this.recipientTypes = [ { value: window.TYPEUSER, text: 'Private Message' }, { value: window.TYPEGROUP, text: 'Group Message' }, - { value: window.TYPENEWSLETTER, text: 'Newsletter' } + { value: window.TYPENEWSLETTER, text: 'Newsletter' }, + { value: window.TYPESTATUS, text: 'Status' } ]; }, methods: { updateType(event) { this.$emit('update:type', event.target.value); + if (event.target.value === window.TYPESTATUS) { + this.$emit('update:phone', ''); + } }, updatePhone(event) { this.$emit('update:phone', event.target.value); @@ -39,11 +58,11 @@ export default {
-
+
diff --git a/src/views/index.html b/src/views/index.html index bdcb961..08301b7 100644 --- a/src/views/index.html +++ b/src/views/index.html @@ -8,6 +8,47 @@ + + @@ -15,21 +56,33 @@ - Whatsapp API Multi Device {{ .AppVersion }} - - + WhatsApp API {{ .AppVersion }} -
-

Whatsapp API Multi Device ({{ .AppVersion }})

+
+ +

WhatsApp API

+
+
+ +