diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/docs/openapi.yaml b/docs/openapi.yaml index c278281..30056b7 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -26,18 +26,18 @@ paths: - app summary: Login to whatsapp server responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/LoginResponse' - '500': + $ref: "#/components/schemas/LoginResponse" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /app/login-with-code: get: operationId: appLoginWithCode @@ -49,21 +49,21 @@ paths: in: query schema: type: string - example: '628912344551' + example: "628912344551" description: Your phone number responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/LoginWithCodeResponse' - '500': + $ref: "#/components/schemas/LoginWithCodeResponse" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /app/logout: get: operationId: appLogout @@ -71,18 +71,18 @@ paths: - app summary: Remove database and logout responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/GenericResponse' - '500': + $ref: "#/components/schemas/GenericResponse" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /app/reconnect: get: operationId: appReconnect @@ -90,18 +90,18 @@ paths: - app summary: Reconnecting to whatsapp server responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/GenericResponse' - '500': + $ref: "#/components/schemas/GenericResponse" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /app/devices: get: operationId: appDevices @@ -109,18 +109,18 @@ paths: - app summary: Get list connected devices responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/DeviceResponse' - '500': + $ref: "#/components/schemas/DeviceResponse" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /user/info: get: operationId: userInfo @@ -132,26 +132,26 @@ paths: in: query schema: type: integer - example: '6289685028129@s.whatsapp.net' + example: "6289685028129@s.whatsapp.net" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/UserInfoResponse' - '400': + $ref: "#/components/schemas/UserInfoResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /user/avatar: get: operationId: userAvatar @@ -163,31 +163,31 @@ paths: in: query schema: type: integer - example: '6289685028129@s.whatsapp.net' + example: "6289685028129@s.whatsapp.net" - name: is_preview in: query schema: type: boolean example: true responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/UserAvatarResponse' - '400': + $ref: "#/components/schemas/UserAvatarResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /user/my/privacy: get: operationId: userMyPrivacy @@ -195,18 +195,18 @@ paths: - user summary: User My Privacy Setting responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/UserPrivacyResponse' - '500': + $ref: "#/components/schemas/UserPrivacyResponse" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /user/my/groups: get: operationId: userMyGroups @@ -214,18 +214,18 @@ paths: - user summary: User My List Groups responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/UserGroupResponse' - '500': + $ref: "#/components/schemas/UserGroupResponse" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /user/my/newsletters: get: operationId: userMyNewsletter @@ -233,18 +233,18 @@ paths: - user summary: User My List Groups responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/NewsletterResponse' - '500': + $ref: "#/components/schemas/NewsletterResponse" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/message: post: operationId: sendMessage @@ -259,7 +259,7 @@ paths: properties: phone: type: string - example: '6289685028129@s.whatsapp.net' + example: "6289685028129@s.whatsapp.net" description: Phone number with country code message: type: string @@ -270,24 +270,24 @@ paths: example: 3EB089B9D6ADD58153C561 description: Message ID that you want reply responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/image: post: operationId: sendImage @@ -302,7 +302,7 @@ paths: properties: phone: type: string - example: '6289685028129@s.whatsapp.net' + example: "6289685028129@s.whatsapp.net" description: Phone number with country code caption: type: string @@ -320,25 +320,29 @@ paths: type: boolean example: false description: Compress image + image_url: + type: string + example: false + description: "Optional URL to download image from" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/audio: post: operationId: sendAudio @@ -353,31 +357,31 @@ paths: properties: phone: type: string - example: '6289685028129@s.whatsapp.net' + example: "6289685028129@s.whatsapp.net" description: Phone number with country code audio: type: string format: binary description: Audio to send responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/file: post: operationId: sendFile @@ -392,7 +396,7 @@ paths: properties: phone: type: string - example: '6289685028129@s.whatsapp.net' + example: "6289685028129@s.whatsapp.net" description: Phone number with country code caption: type: string @@ -402,25 +406,29 @@ paths: type: string format: binary description: File to send + file_url: + type: string + format: uri + description: "Optional URL to download file from" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/video: post: operationId: sendVideo @@ -435,7 +443,7 @@ paths: properties: phone: type: string - example: '6289685028129@s.whatsapp.net' + example: "6289685028129@s.whatsapp.net" description: Phone number with country code caption: type: string @@ -443,7 +451,7 @@ paths: description: Caption to send view_once: type: boolean - example: 'false' + example: "false" description: View once video: type: string @@ -451,27 +459,31 @@ paths: description: Video to send compress: type: boolean - example: 'false' + example: "false" description: Compress video + video_url: + type: string + format: uri + description: "Optional URL to download video from" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/contact: post: operationId: sendContact @@ -486,7 +498,7 @@ paths: properties: phone: type: string - example: '6289685024051@s.whatsapp.net' + example: "6289685024051@s.whatsapp.net" description: Phone number with country code contact_name: type: string @@ -494,27 +506,27 @@ paths: description: Contact name contact_phone: type: string - example: '6289685024992' + example: "6289685024992" description: Contact phone number responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/link: post: operationId: sendLink @@ -529,7 +541,7 @@ paths: properties: phone: type: string - example: '6289685024051@s.whatsapp.net' + example: "6289685024051@s.whatsapp.net" description: Phone number with country code link: type: string @@ -537,27 +549,27 @@ paths: description: Link to send caption: type: string - example: 'Halo ini contoh caption' + example: "Halo ini contoh caption" description: Caption to send responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/location: post: operationId: sendLocation @@ -572,7 +584,7 @@ paths: properties: phone: type: string - example: '6289685024051@s.whatsapp.net' + example: "6289685024051@s.whatsapp.net" description: Phone number with country code latitude: type: string @@ -580,27 +592,27 @@ paths: description: Latitude coordinate longitude: type: string - example: '110.370529' + example: "110.370529" description: Longitude coordinate responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /send/poll: post: operationId: sendPoll @@ -617,17 +629,17 @@ paths: phone: type: string description: The WhatsApp phone number to send the poll to, including the '@s.whatsapp.net' suffix. - example: '6289685024421@s.whatsapp.net' + example: "6289685024421@s.whatsapp.net" question: type: string description: The question for the poll. - example: 'Siapa Nama Avatar The Last Air Bender?' + example: "Siapa Nama Avatar The Last Air Bender?" options: type: array description: The options for the poll. items: type: string - example: [ 'Zuko', 'Aang', 'Katara' ] + example: ["Zuko", "Aang", "Katara"] max_answer: type: integer description: The maximum number of answers allowed for the poll. @@ -638,24 +650,24 @@ paths: - options - max_answer responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /message/{message_id}/revoke: post: operationId: revokeMessage @@ -677,27 +689,27 @@ paths: properties: phone: type: string - example: '6289685024051@s.whatsapp.net' + example: "6289685024051@s.whatsapp.net" description: Phone number with country code responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /message/{message_id}/delete: post: operationId: deleteMessage @@ -719,27 +731,27 @@ paths: properties: phone: type: string - example: '6289685024051@s.whatsapp.net' + example: "6289685024051@s.whatsapp.net" description: Phone number with country code responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /message/{message_id}/reaction: post: operationId: reactMessage @@ -761,31 +773,31 @@ paths: properties: phone: type: string - example: '6289685024051@s.whatsapp.net' + example: "6289685024051@s.whatsapp.net" description: Phone number with country code emoji: type: string example: "🙏" description: Emoji to react responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /message/{message_id}/update: post: operationId: updateMessage @@ -807,34 +819,34 @@ paths: properties: phone: type: string - example: '62819273192397132@s.whatsapp.net' + example: "62819273192397132@s.whatsapp.net" description: Phone number with country code message: type: string - example: 'Hello World' + example: "Hello World" description: New message to send required: - phone - message responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /message/{message_id}/read: post: operationId: readMessage @@ -856,29 +868,29 @@ paths: properties: phone: type: string - example: '62819273192397132@s.whatsapp.net' + example: "62819273192397132@s.whatsapp.net" description: Phone number with country code required: - phone responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/SendResponse' - '400': + $ref: "#/components/schemas/SendResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /group: post: operationId: createGroup @@ -893,38 +905,38 @@ paths: properties: title: type: string - example: 'Example Group Title' + example: "Example Group Title" participants: type: array items: type: string example: - - '6819241294719274' - - '6829241294719274' - - '6839241294719274' + - "6819241294719274" + - "6829241294719274" + - "6839241294719274" example: - - '6819241294719274' - - '6829241294719274' - - '6839241294719274' + - "6819241294719274" + - "6829241294719274" + - "6839241294719274" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/CreateGroupResponse' - '400': + $ref: "#/components/schemas/CreateGroupResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /group/participants: post: operationId: addParticipantToGroup @@ -935,26 +947,26 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ManageParticipantRequest' + $ref: "#/components/schemas/ManageParticipantRequest" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/ManageParticipantResponse' - '400': + $ref: "#/components/schemas/ManageParticipantResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /group/participants/remove: post: operationId: removeParticipantFromGroup @@ -965,26 +977,26 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ManageParticipantRequest' + $ref: "#/components/schemas/ManageParticipantRequest" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/ManageParticipantResponse' - '400': + $ref: "#/components/schemas/ManageParticipantResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /group/participants/promote: post: operationId: promoteParticipantToAdmin @@ -995,26 +1007,26 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ManageParticipantRequest' + $ref: "#/components/schemas/ManageParticipantRequest" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/ManageParticipantResponse' - '400': + $ref: "#/components/schemas/ManageParticipantResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /group/participants/demote: post: operationId: demoteParticipantToMember @@ -1025,26 +1037,26 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ManageParticipantRequest' + $ref: "#/components/schemas/ManageParticipantRequest" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/ManageParticipantResponse' - '400': + $ref: "#/components/schemas/ManageParticipantResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /group/join-with-link: post: operationId: joinGroupWithLink @@ -1059,26 +1071,26 @@ paths: properties: link: type: string - example: 'https://chat.whatsapp.com/whatsappKeyJoinGroup' + example: "https://chat.whatsapp.com/whatsappKeyJoinGroup" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/GenericResponse' - '400': + $ref: "#/components/schemas/GenericResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /group/leave: post: operationId: leaveGroup @@ -1093,26 +1105,26 @@ paths: properties: group_id: type: string - example: '120363024512399999@g.us' + example: "120363024512399999@g.us" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/GenericResponse' - '400': + $ref: "#/components/schemas/GenericResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" /newsletter/unfollow: post: operationId: unfollowNewsletter @@ -1127,26 +1139,26 @@ paths: properties: newsletter_id: type: string - example: '120363024512399999@newsletter' + example: "120363024512399999@newsletter" responses: - '200': + "200": description: OK content: application/json: schema: - $ref: '#/components/schemas/GenericResponse' - '400': + $ref: "#/components/schemas/GenericResponse" + "400": description: Bad Request content: application/json: schema: - $ref: '#/components/schemas/ErrorBadRequest' - '500': + $ref: "#/components/schemas/ErrorBadRequest" + "500": description: Internal Server Error content: application/json: schema: - $ref: '#/components/schemas/ErrorInternalServer' + $ref: "#/components/schemas/ErrorInternalServer" components: schemas: @@ -1176,9 +1188,9 @@ components: items: type: string example: - - '6819241294719274' - - '6829241294719274' - - '6839241294719274' + - "6819241294719274" + - "6829241294719274" + - "6839241294719274" ManageParticipantResponse: type: object properties: @@ -1194,7 +1206,7 @@ components: properties: participant: type: string - example: '6289987391723@s.whatsapp.net' + example: "6289987391723@s.whatsapp.net" status: type: string example: success @@ -1300,13 +1312,13 @@ components: properties: url: type: string - example: 'https://pps.whatsapp.net/v/t61.24694-24/181358562_385581386633509_6230178822944778044_n.jpg?ccb=11-4&oh=df36c5b990497b8a5758a0f1ad8118a8&oe=620AA726' + example: "https://pps.whatsapp.net/v/t61.24694-24/181358562_385581386633509_6230178822944778044_n.jpg?ccb=11-4&oh=df36c5b990497b8a5758a0f1ad8118a8&oe=620AA726" id: type: string - example: '1635239861' + example: "1635239861" type: type: string - example: 'image' + example: "image" UserPrivacyResponse: type: object properties: @@ -1348,10 +1360,10 @@ components: properties: message_id: type: string - example: '3EB0B430B6F8F1D0E053AC120E0A9E5C' + example: "3EB0B430B6F8F1D0E053AC120E0A9E5C" status: type: string - example: ' success ....' + example: " success ...." DeviceResponse: type: object properties: @@ -1368,10 +1380,10 @@ components: properties: name: type: string - example: 'Aldino Kemal' + example: "Aldino Kemal" device: type: string - example: '628960561XXX.0:64@s.whatsapp.net' + example: "628960561XXX.0:64@s.whatsapp.net" LoginWithCodeResponse: type: object properties: @@ -1404,7 +1416,7 @@ components: example: 30 qr_link: type: string - example: 'http://localhost:3000/statics/images/qrcode/scan-qr-b0b7bb43-9a22-455a-814f-5a225c743310.png' + example: "http://localhost:3000/statics/images/qrcode/scan-qr-b0b7bb43-9a22-455a-814f-5a225c743310.png" GenericResponse: type: object properties: @@ -1423,30 +1435,30 @@ components: code: type: string example: INTERNAL_SERVER_ERROR - description: 'SYSTEM_CODE_ERROR' + description: "SYSTEM_CODE_ERROR" message: type: string example: you are not loggin - description: 'Detail error message' + description: "Detail error message" results: type: object example: null - description: 'additional data' + description: "additional data" ErrorBadRequest: type: object properties: code: type: string example: 400 - description: 'HTTP Status Code' + description: "HTTP Status Code" message: type: string example: field cannot be blank - description: 'Detail error message' + description: "Detail error message" results: type: object example: null - description: 'additional data' + description: "additional data" NewsletterResponse: type: object properties: @@ -1462,7 +1474,7 @@ components: data: type: array items: - $ref: '#/components/schemas/Newsletter' + $ref: "#/components/schemas/Newsletter" Newsletter: type: object properties: @@ -1577,7 +1589,7 @@ components: data: type: array items: - $ref: '#/components/schemas/Group' + $ref: "#/components/schemas/Group" Group: type: object properties: @@ -1656,7 +1668,7 @@ components: Participants: type: array items: - $ref: '#/components/schemas/Participant' + $ref: "#/components/schemas/Participant" MemberAddMode: type: string example: "admin_add" @@ -1684,4 +1696,4 @@ components: example: 0 AddRequest: type: string - example: null \ No newline at end of file + example: null diff --git a/docs/sdk/config.yaml b/docs/sdk/config.yaml index 3b7b4c3..bf7f836 100644 --- a/docs/sdk/config.yaml +++ b/docs/sdk/config.yaml @@ -10,4 +10,4 @@ packageName: "SdkWhatsappWebMultiDevice" # sdk-php Configs composerPackageName: "SdkWhatsappWebMultiDevice" -invokerPackage: "SdkWhatsappWebMultiDevice" \ No newline at end of file +invokerPackage: "SdkWhatsappWebMultiDevice" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e7ea89c --- /dev/null +++ b/flake.lock @@ -0,0 +1,25 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1731676054, + "narHash": "sha256-OZiZ3m8SCMfh3B6bfGC/Bm4x3qc1m2SVEAlkV6iY7Yg=", + "rev": "5e4fbfb6b3de1aa2872b76d49fafc942626e2add", + "revCount": 708622, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.708622%2Brev-5e4fbfb6b3de1aa2872b76d49fafc942626e2add/0193363c-ab27-7bbd-af1d-3e6093ed5e2d/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..597acb5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + description = "A Nix-flake-based Go 1.22 development environment"; + + inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz"; + + outputs = { self, nixpkgs }: + let + goVersion = 22; # Change this to update the whole stack + + supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { + pkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + }; + }); + in + { + overlays.default = final: prev: { + go = final."go_1_${toString goVersion}"; + }; + + devShells = forEachSupportedSystem ({ pkgs }: { + default = pkgs.mkShell { + packages = with pkgs; [ + # go (version is specified by overlay) + go + + # goimports, godoc, etc. + gotools + + # https://github.com/golangci/golangci-lint + golangci-lint + ]; + }; + }); + }; +} diff --git a/readme.md b/readme.md index 4052b5c..d46aaf4 100644 --- a/readme.md +++ b/readme.md @@ -21,36 +21,38 @@ Now that we support ARM64 for Linux: - 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) - 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 @@ -75,13 +77,13 @@ Now that we support ARM64 for Linux: 2. Open the folder that was cloned via cmd/terminal. 3. run `cd src` 4. run - 1. Linux & MacOS: `go build -o whatsapp` - 2. Windows (CMD / PowerShell): `go build -o whatsapp.exe` + 1. Linux & MacOS: `go build -o whatsapp` + 2. Windows (CMD / PowerShell): `go build -o whatsapp.exe` 5. run - 1. Linux & MacOS: `./whatsapp` - 1. run `./whatsapp --help` for more detail flags - 2. Windows: `.\whatsapp.exe` or you can double-click it - 1. run `.\whatsapp.exe --help` for more detail flags + 1. Linux & MacOS: `./whatsapp` + 1. run `./whatsapp --help` for more detail flags + 2. Windows: `.\whatsapp.exe` or you can double-click it + 1. run `.\whatsapp.exe --help` for more detail flags 6. open `http://localhost:3000` in browser ### Production Mode (docker) @@ -103,41 +105,41 @@ You can fork or edit this source code ! to [SwaggerEditor](https://editor.swagger.io). - Furthermore you can generate HTTP Client from this API using [openapi-generator](https://openapi-generator.tech/#try) -| 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 | -| ✅ | User Info | GET | /user/info | -| ✅ | User Avatar | GET | /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 Contact | POST | /send/contact | -| ✅ | Send Link | POST | /send/link | -| ✅ | Send Location | POST | /send/location | -| ✅ | Send Poll / Vote | POST | /send/poll | -| ✅ | 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 | -| ✅ | Join Group With Link | POST | /group/join-with-link | -| ✅ | Leave Group | POST | /group/leave | -| ✅ | Create Group | POST | /group | -| ✅ | Add Participants in Group | POST | /group/participants | -| ✅ | Remove Participant in Group | POST | /group/participants/remove | -| ✅ | Promote Participant in Group | POST | /group/participants/promote | -| ✅ | Demote Participant in Group | POST | /group/participants/demote | -| ✅ | Unfollow Newsletter | POST | /newsletter/unfollow | +| 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 | +| ✅ | User Info | GET | /user/info | +| ✅ | User Avatar | GET | /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 Contact | POST | /send/contact | +| ✅ | Send Link | POST | /send/link | +| ✅ | Send Location | POST | /send/location | +| ✅ | Send Poll / Vote | POST | /send/poll | +| ✅ | 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 | +| ✅ | Join Group With Link | POST | /group/join-with-link | +| ✅ | Leave Group | POST | /group/leave | +| ✅ | Create Group | POST | /group | +| ✅ | Add Participants in Group | POST | /group/participants | +| ✅ | Remove Participant in Group | POST | /group/participants/remove | +| ✅ | Promote Participant in Group | POST | /group/participants/promote | +| ✅ | Demote Participant in Group | POST | /group/participants/demote | +| ✅ | Unfollow Newsletter | POST | /newsletter/unfollow | ``` ✅ = Available @@ -147,7 +149,7 @@ You can fork or edit this source code ! ### 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) | diff --git a/src/domains/send/file.go b/src/domains/send/file.go index ca57790..e0bcab6 100644 --- a/src/domains/send/file.go +++ b/src/domains/send/file.go @@ -6,4 +6,5 @@ type FileRequest struct { Phone string `json:"phone" form:"phone"` File *multipart.FileHeader `json:"file" form:"file"` Caption string `json:"caption" form:"caption"` + FileUrl string `json:"file_url" form:"file_url"` } diff --git a/src/domains/send/image.go b/src/domains/send/image.go index 783b030..19c7c70 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/video.go b/src/domains/send/video.go index 5ba739d..9a83c0b 100644 --- a/src/domains/send/video.go +++ b/src/domains/send/video.go @@ -8,4 +8,5 @@ type VideoRequest struct { Video *multipart.FileHeader `json:"video" form:"video"` ViewOnce bool `json:"view_once" form:"view_once"` Compress bool `json:"compress"` + VideoUrl string `json:"video_url" form:"video_url"` } diff --git a/src/internal/rest/send.go b/src/internal/rest/send.go index e0eb827..de576ee 100644 --- a/src/internal/rest/send.go +++ b/src/internal/rest/send.go @@ -5,6 +5,7 @@ import ( "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils" "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/whatsapp" "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" ) type Send struct { @@ -50,14 +51,31 @@ func (controller *Send) SendImage(c *fiber.Ctx) error { err := c.BodyParser(&request) utils.PanicIfNeeded(err) - file, err := c.FormFile("image") - utils.PanicIfNeeded(err) + // Add debug logging + logrus.WithFields(logrus.Fields{ + "body": c.Body(), + "form": c.FormValue("image_url"), + }).Debug("Image request received") + + request.ImageUrl = c.FormValue("image_url") + if request.ImageUrl == "" { + if file, err := c.FormFile("image"); err == nil { + request.Image = file + logrus.WithField("filename", file.Filename).Debug("Image file received") + } else { + logrus.WithError(err).Debug("No image file found") + } + } else { + logrus.WithField("url", request.ImageUrl).Debug("Image URL received") + } - request.Image = file whatsapp.SanitizePhone(&request.Phone) response, err := controller.Service.SendImage(c.UserContext(), request) - utils.PanicIfNeeded(err) + if err != nil { + logrus.WithError(err).Error("Failed to send image") + return err + } return c.JSON(utils.ResponseData{ Status: 200, @@ -72,10 +90,12 @@ func (controller *Send) SendFile(c *fiber.Ctx) error { err := c.BodyParser(&request) utils.PanicIfNeeded(err) - file, err := c.FormFile("file") - utils.PanicIfNeeded(err) - - request.File = file + request.FileUrl = c.FormValue("file_url") + if request.FileUrl == "" { + if file, err := c.FormFile("file"); err == nil { + request.File = file + } + } whatsapp.SanitizePhone(&request.Phone) response, err := controller.Service.SendFile(c.UserContext(), request) @@ -94,10 +114,12 @@ func (controller *Send) SendVideo(c *fiber.Ctx) error { err := c.BodyParser(&request) utils.PanicIfNeeded(err) - video, err := c.FormFile("video") - utils.PanicIfNeeded(err) - - request.Video = video + request.VideoUrl = c.FormValue("video_url") + if request.VideoUrl == "" { + if video, err := c.FormFile("video"); err == nil { + request.Video = video + } + } whatsapp.SanitizePhone(&request.Phone) response, err := controller.Service.SendVideo(c.UserContext(), request) diff --git a/src/services/send.go b/src/services/send.go index f465d0e..97b8c45 100644 --- a/src/services/send.go +++ b/src/services/send.go @@ -3,6 +3,11 @@ package services import ( "context" "fmt" + "io" + "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 +24,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 +38,23 @@ func NewSendService(waCli *whatsmeow.Client, appService app.IAppService) domainS } } +func downloadFileFromURL(url, path string) error { + resp, err := http.Get(url) + if (err != nil) { + return err + } + defer resp.Body.Close() + + out, err := os.Create(path) + if (err != nil) { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} + func (service serviceSend) SendText(ctx context.Context, request domainSend.MessageRequest) (response domainSend.GenericResponse, err error) { err = validations.ValidateSendMessage(ctx, request) if err != nil { @@ -98,53 +117,72 @@ func (service serviceSend) SendText(ctx context.Context, request domainSend.Mess } func (service serviceSend) SendImage(ctx context.Context, request domainSend.ImageRequest) (response domainSend.GenericResponse, err error) { + logrus.WithFields(logrus.Fields{ + "phone": request.Phone, + "url": request.ImageUrl, + "file": request.Image != nil, + }).Debug("SendImage request received") + err = validations.ValidateSendImage(ctx, request) if err != nil { return response, err } + dataWaRecipient, err := whatsapp.ValidateJidWithLogin(service.WaCli, request.Phone) if err != nil { return response, err } var ( + oriImagePath string imagePath string imageThumbnail string deletedItems []string + filename 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 + // Generate unique filename + filename = fmt.Sprintf("image-%s%s", fiberUtils.UUIDv4(), ".jpg") + oriImagePath = fmt.Sprintf("%s/%s", config.PathSendItems, filename) + + // Handle image from URL or file + if request.ImageUrl != "" { + logrus.WithField("url", request.ImageUrl).Debug("Downloading image from URL") + err = downloadFileFromURL(request.ImageUrl, oriImagePath) + if err != nil { + return response, fmt.Errorf("failed to download image: %v", err) + } + deletedItems = append(deletedItems, oriImagePath) + } else if request.Image != nil { + logrus.WithField("filename", request.Image.Filename).Debug("Saving uploaded image") + err = fasthttp.SaveMultipartFile(request.Image, oriImagePath) + if err != nil { + return response, fmt.Errorf("failed to save image: %v", err) + } + deletedItems = append(deletedItems, oriImagePath) + } else { + return response, pkgError.ValidationError("either ImageUrl or Image must be provided") } - deletedItems = append(deletedItems, oriImagePath) - /* Generate thumbnail with smalled image size */ + // Generate thumbnail + imageThumbnail = fmt.Sprintf("%s/thumb-%s", config.PathSendItems, filename) srcImage, err := imaging.Open(oriImagePath) if err != nil { return response, pkgError.InternalServerError(fmt.Sprintf("failed to open image %v", err)) } - - // Resize Thumbnail resizedImage := imaging.Resize(srcImage, 100, 0, imaging.Lanczos) - imageThumbnail = fmt.Sprintf("%s/thumbnails-%s", config.PathSendItems, request.Image.Filename) if err = imaging.Save(resizedImage, imageThumbnail); err != nil { return response, pkgError.InternalServerError(fmt.Sprintf("failed to save thumbnail %v", err)) } deletedItems = append(deletedItems, imageThumbnail) + // Handle compression if needed if request.Compress { - // Resize image - openImageBuffer, err := imaging.Open(oriImagePath) - if err != nil { - 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) + logrus.Debug("Compressing image") + newImagePath := fmt.Sprintf("%s/compressed-%s", config.PathSendItems, filename) + newImage := imaging.Resize(srcImage, 600, 0, imaging.Lanczos) if err = imaging.Save(newImage, newImagePath); err != nil { - return response, pkgError.InternalServerError(fmt.Sprintf("failed to save image %v", err)) + return response, pkgError.InternalServerError(fmt.Sprintf("failed to save compressed image %v", err)) } deletedItems = append(deletedItems, newImagePath) imagePath = newImagePath @@ -152,25 +190,26 @@ func (service serviceSend) SendImage(ctx context.Context, request domainSend.Ima imagePath = oriImagePath } - // Send to WA server - dataWaCaption := request.Caption + // Send to WhatsApp + logrus.Debug("Uploading to WhatsApp") dataWaImage, err := os.ReadFile(imagePath) if err != nil { return response, err } uploadedImage, err := service.uploadMedia(ctx, whatsmeow.MediaImage, dataWaImage, dataWaRecipient) if err != nil { - fmt.Printf("failed to upload file: %v", err) - return response, err + return response, fmt.Errorf("failed to upload image: %v", err) } + dataWaThumbnail, err := os.ReadFile(imageThumbnail) if err != nil { return response, pkgError.InternalServerError(fmt.Sprintf("failed to read thumbnail %v", err)) } + // Prepare and send message msg := &waE2E.Message{ImageMessage: &waE2E.ImageMessage{ JPEGThumbnail: dataWaThumbnail, - Caption: proto.String(dataWaCaption), + Caption: proto.String(request.Caption), URL: proto.String(uploadedImage.URL), DirectPath: proto.String(uploadedImage.DirectPath), MediaKey: uploadedImage.MediaKey, @@ -180,19 +219,21 @@ 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) - go func() { - errDelete := utils.RemoveFile(0, deletedItems...) - if errDelete != nil { - fmt.Println("error when deleting picture: ", errDelete) - } - }() if err != nil { return response, err } + // Cleanup files + go func() { + if err := utils.RemoveFile(0, deletedItems...); err != nil { + logrus.WithError(err).Error("Failed to cleanup files") + } + }() + response.MessageID = ts.ID - response.Status = fmt.Sprintf("Message sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String()) + response.Status = fmt.Sprintf("Image sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp) return response, nil } @@ -206,36 +247,77 @@ func (service serviceSend) SendFile(ctx context.Context, request domainSend.File return response, err } - fileBytes := helpers.MultipartFormFileHeaderToBytes(request.File) - fileMimeType := http.DetectContentType(fileBytes) + if request.FileUrl != "" { + fileBytes, err := http.Get(request.FileUrl) + if err != nil { + return response, err + } + defer fileBytes.Body.Close() + data, err := io.ReadAll(fileBytes.Body) + if err != nil { + return response, err + } + fileMimeType := http.DetectContentType(data) + uploadedFile, err := service.uploadMedia(ctx, whatsmeow.MediaDocument, data, dataWaRecipient) + if err != nil { + fmt.Printf("Failed to upload file: %v", err) + return response, err + } - // Send to WA server - uploadedFile, err := service.uploadMedia(ctx, whatsmeow.MediaDocument, fileBytes, dataWaRecipient) - if err != nil { - fmt.Printf("Failed to upload file: %v", err) - return response, err - } + msg := &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{ + URL: proto.String(uploadedFile.URL), + Mimetype: proto.String(fileMimeType), + Title: proto.String(request.File.Filename), + FileSHA256: uploadedFile.FileSHA256, + FileLength: proto.Uint64(uploadedFile.FileLength), + MediaKey: uploadedFile.MediaKey, + FileName: proto.String(request.File.Filename), + FileEncSHA256: uploadedFile.FileEncSHA256, + DirectPath: proto.String(uploadedFile.DirectPath), + Caption: proto.String(request.Caption), + }} + ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + if err != nil { + return response, err + } - msg := &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{ - URL: proto.String(uploadedFile.URL), - Mimetype: proto.String(fileMimeType), - Title: proto.String(request.File.Filename), - FileSHA256: uploadedFile.FileSHA256, - FileLength: proto.Uint64(uploadedFile.FileLength), - MediaKey: uploadedFile.MediaKey, - FileName: proto.String(request.File.Filename), - FileEncSHA256: uploadedFile.FileEncSHA256, - DirectPath: proto.String(uploadedFile.DirectPath), - Caption: proto.String(request.Caption), - }} - ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) - if err != nil { - return response, err - } + response.MessageID = ts.ID + response.Status = fmt.Sprintf("Document sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String()) + return response, nil + } else if request.File != nil { + fileBytes := helpers.MultipartFormFileHeaderToBytes(request.File) + fileMimeType := http.DetectContentType(fileBytes) - response.MessageID = ts.ID - response.Status = fmt.Sprintf("Document sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String()) - return response, nil + // Send to WA server + uploadedFile, err := service.uploadMedia(ctx, whatsmeow.MediaDocument, fileBytes, dataWaRecipient) + if err != nil { + fmt.Printf("Failed to upload file: %v", err) + return response, err + } + + msg := &waE2E.Message{DocumentMessage: &waE2E.DocumentMessage{ + URL: proto.String(uploadedFile.URL), + Mimetype: proto.String(fileMimeType), + Title: proto.String(request.File.Filename), + FileSHA256: uploadedFile.FileSHA256, + FileLength: proto.Uint64(uploadedFile.FileLength), + MediaKey: uploadedFile.MediaKey, + FileName: proto.String(request.File.Filename), + FileEncSHA256: uploadedFile.FileEncSHA256, + DirectPath: proto.String(uploadedFile.DirectPath), + Caption: proto.String(request.Caption), + }} + ts, err := service.WaCli.SendMessage(ctx, dataWaRecipient, msg) + if err != nil { + return response, err + } + + response.MessageID = ts.ID + response.Status = fmt.Sprintf("Document sent to %s (server timestamp: %s)", request.Phone, ts.Timestamp.String()) + return response, nil + } else { + return response, pkgError.ValidationError("either FileUrl or File must be provided") + } } func (service serviceSend) SendVideo(ctx context.Context, request domainSend.VideoRequest) (response domainSend.GenericResponse, err error) { @@ -249,6 +331,7 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid } var ( + oriVideoPath string videoPath string videoThumbnail string deletedItems []string @@ -256,10 +339,20 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid generateUUID := fiberUtils.UUIDv4() // Save video to server - oriVideoPath := fmt.Sprintf("%s/%s", config.PathSendItems, generateUUID+request.Video.Filename) - err = fasthttp.SaveMultipartFile(request.Video, oriVideoPath) - if err != nil { - return response, pkgError.InternalServerError(fmt.Sprintf("failed to store video in server %v", err)) + if request.VideoUrl != "" { + oriVideoPath = fmt.Sprintf("%s/url-video-%s.mp4", config.PathSendItems, generateUUID) + err = downloadFileFromURL(request.VideoUrl, oriVideoPath) + if err != nil { + return response, err + } + } else if request.Video != nil { + oriVideoPath = fmt.Sprintf("%s/%s", config.PathSendItems, generateUUID+request.Video.Filename) + err = fasthttp.SaveMultipartFile(request.Video, oriVideoPath) + if err != nil { + return response, pkgError.InternalServerError(fmt.Sprintf("failed to store video in server %v", err)) + } + } else { + return response, pkgError.ValidationError("either VideoUrl or Video must be provided") } // Check if ffmpeg is installed @@ -296,7 +389,7 @@ func (service serviceSend) SendVideo(ctx context.Context, request domainSend.Vid cmdCompress := exec.Command("ffmpeg", "-i", oriVideoPath, "-strict", "-2", compresVideoPath) err = cmdCompress.Run() - if err != nil { + if (err != nil) { return response, pkgError.InternalServerError("failed to compress video") } diff --git a/src/validations/send_validation.go b/src/validations/send_validation.go index 137704c..5a72a76 100644 --- a/src/validations/send_validation.go +++ b/src/validations/send_validation.go @@ -26,69 +26,68 @@ 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, + // Skip mime validation for URL + if request.ImageUrl != "" { + return nil } - 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") + } } - return nil } func ValidateSendFile(ctx context.Context, request domainSend.FileRequest) error { err := validation.ValidateStructWithContext(ctx, &request, validation.Field(&request.Phone, validation.Required), - validation.Field(&request.File, validation.Required), ) - if err != nil { return pkgError.ValidationError(err.Error()) } - if request.File.Size > config.WhatsappSettingMaxFileSize { // 10MB - maxSizeString := humanize.Bytes(uint64(config.WhatsappSettingMaxFileSize)) - return pkgError.ValidationError(fmt.Sprintf("max file upload is %s, please upload in cloud and send via text if your file is higher than %s", maxSizeString, maxSizeString)) + if request.File != nil { + if request.File.Size > config.WhatsappSettingMaxFileSize { // 10MB + maxSizeString := humanize.Bytes(uint64(config.WhatsappSettingMaxFileSize)) + return pkgError.ValidationError(fmt.Sprintf("max file upload is %s, please upload in cloud and send via text if your file is higher than %s", maxSizeString, maxSizeString)) + } } - return nil } func ValidateSendVideo(ctx context.Context, request domainSend.VideoRequest) error { err := validation.ValidateStructWithContext(ctx, &request, validation.Field(&request.Phone, validation.Required), - validation.Field(&request.Video, validation.Required), ) - if err != nil { return pkgError.ValidationError(err.Error()) } - availableMimes := map[string]bool{ - "video/mp4": true, - "video/x-matroska": true, - "video/avi": true, - } - - if !availableMimes[request.Video.Header.Get("Content-Type")] { - return pkgError.ValidationError("your video type is not allowed. please use mp4/mkv/avi") - } - - if request.Video.Size > config.WhatsappSettingMaxVideoSize { // 30MB - maxSizeString := humanize.Bytes(uint64(config.WhatsappSettingMaxVideoSize)) - return pkgError.ValidationError(fmt.Sprintf("max video upload is %s, please upload in cloud and send via text if your file is higher than %s", maxSizeString, maxSizeString)) + if request.Video != nil { + availableMimes := map[string]bool{ + "video/mp4": true, + "video/x-matroska": true, + "video/avi": true, + } + if !availableMimes[request.Video.Header.Get("Content-Type")] { + return pkgError.ValidationError("your video type is not allowed. please use mp4/mkv/avi") + } + if request.Video.Size > config.WhatsappSettingMaxVideoSize { // 30MB + maxSizeString := humanize.Bytes(uint64(config.WhatsappSettingMaxVideoSize)) + return pkgError.ValidationError(fmt.Sprintf("max video upload is %s, please upload in cloud and send via text if your file is higher than %s", maxSizeString, maxSizeString)) + } } - return nil } diff --git a/src/views/components/SendFile.js b/src/views/components/SendFile.js index affbb4c..8a90f5a 100644 --- a/src/views/components/SendFile.js +++ b/src/views/components/SendFile.js @@ -1,73 +1,86 @@ import FormRecipient from "./generic/FormRecipient.js"; export default { - name: 'SendFile', - components: { - FormRecipient + name: "SendFile", + components: { + FormRecipient, + }, + props: { + maxFileSize: { + type: String, + required: true, }, - props: { - maxFileSize: { - type: String, - required: true, - } + }, + data() { + return { + caption: "", + type: window.TYPEUSER, + phone: "", + loading: false, + file_url: "", + }; + }, + computed: { + phone_id() { + return this.phone + this.type; + }, + }, + methods: { + openModal() { + $("#modalSendFile") + .modal({ + onApprove: function () { + return false; + }, + }) + .modal("show"); }, - data() { - return { - caption: '', - type: window.TYPEUSER, - phone: '', - loading: false, + async handleSubmit() { + try { + if (!this.file_url && !$("#file_input")[0].files[0]) { + throw new Error("Please provide either a file URL or upload a file."); } + let response = await this.submitApi(); + showSuccessInfo(response); + $("#modalSendFile").modal("hide"); + } catch (err) { + showErrorInfo(err); + } }, - computed: { - phone_id() { - return this.phone + this.type; + async submitApi() { + this.loading = true; + try { + let payload = new FormData(); + payload.append("caption", this.caption); + payload.append("phone", this.phone_id); + + if (this.file_url) { + payload.append("file_url", this.file_url); + } else { + payload.append("file", $("#file_input")[0].files[0]); } + + let response = await window.http.post(`/send/file`, 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; + } }, - methods: { - openModal() { - $('#modalSendFile').modal({ - onApprove: function () { - return false; - } - }).modal('show'); - }, - async handleSubmit() { - try { - let response = await this.submitApi() - showSuccessInfo(response) - $('#modalSendFile').modal('hide'); - } catch (err) { - showErrorInfo(err) - } - }, - async submitApi() { - this.loading = true; - try { - let payload = new FormData(); - payload.append("caption", this.caption) - payload.append("phone", this.phone_id) - payload.append("file", $("#file_file")[0].files[0]) - let response = await window.http.post(`/send/file`, 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.caption = ''; - this.phone = ''; - this.type = window.TYPEUSER; - $("#file_file").val(''); - }, + handleReset() { + this.caption = ""; + this.phone = ""; + this.type = window.TYPEUSER; + this.file_url = ""; + $("#file_input").val(""); }, - template: ` + }, + template: `
Send @@ -94,10 +107,14 @@ export default {
+
+ + +
- -
- ` -} \ No newline at end of file + `, +}; diff --git a/src/views/components/SendImage.js b/src/views/components/SendImage.js index 5c276c9..7611477 100644 --- a/src/views/components/SendImage.js +++ b/src/views/components/SendImage.js @@ -1,75 +1,89 @@ import FormRecipient from "./generic/FormRecipient.js"; export default { - name: 'SendImage', - components: { - FormRecipient + name: "SendImage", + components: { + FormRecipient, + }, + data() { + return { + phone: "", + view_once: false, + compress: false, + caption: "", + type: window.TYPEUSER, + loading: false, + selected_file: null, + image_url: "", + }; + }, + computed: { + phone_id() { + return this.phone + this.type; }, - data() { - return { - phone: '', - view_once: false, - compress: false, - caption: '', - type: window.TYPEUSER, - loading: false, - selected_file: null - } + }, + methods: { + openModal() { + $("#modalSendImage") + .modal({ + onApprove: function () { + return false; + }, + }) + .modal("show"); }, - computed: { - phone_id() { - return this.phone + this.type; + async handleSubmit() { + try { + if (!this.image_url && !$("#file_image")[0].files[0]) { + throw new Error( + "Please provide either an image URL or upload an image file." + ); } + let response = await this.submitApi(); + showSuccessInfo(response); + $("#modalSendImage").modal("hide"); + } catch (err) { + showErrorInfo(err); + } }, - methods: { - openModal() { - $('#modalSendImage').modal({ - onApprove: function () { - return false; - } - }).modal('show'); - }, - async handleSubmit() { - try { - let response = await this.submitApi() - showSuccessInfo(response) - $('#modalSendImage').modal('hide'); - } catch (err) { - showErrorInfo(err) - } - }, - async submitApi() { - this.loading = true; - try { - let payload = new FormData(); - payload.append("phone", this.phone_id) - 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]) + async submitApi() { + this.loading = true; + try { + let payload = new FormData(); + payload.append("phone", this.phone_id); + payload.append("view_once", this.view_once); + payload.append("compress", this.compress); + payload.append("caption", this.caption); + + if (this.image_url) { + payload.append("image_url", this.image_url); + } else { + payload.append("image", $("#file_image")[0].files[0]); + } - let response = await window.http.post(`/send/image`, 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.view_once = false; - this.compress = false; - this.phone = ''; - this.caption = ''; - this.type = window.TYPEUSER; - $("#file_image").val(''); - }, + let response = await window.http.post(`/send/image`, 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; + } }, - template: ` + handleReset() { + this.view_once = false; + this.compress = false; + this.phone = ""; + this.caption = ""; + this.type = window.TYPEUSER; + this.image_url = ""; + $("#file_image").val(""); + }, + }, + template: `
Send @@ -111,6 +125,10 @@ export default {
+
+ + +
@@ -129,5 +147,5 @@ export default {
- ` -} \ No newline at end of file + `, +}; diff --git a/src/views/components/SendVideo.js b/src/views/components/SendVideo.js index 6a93593..38c9f31 100644 --- a/src/views/components/SendVideo.js +++ b/src/views/components/SendVideo.js @@ -1,80 +1,95 @@ import FormRecipient from "./generic/FormRecipient.js"; export default { - name: 'SendVideo', - components: { - FormRecipient + name: "SendVideo", + components: { + FormRecipient, + }, + // define props + props: { + maxVideoSize: { + type: String, + required: true, }, - // define props - props: { - maxVideoSize: { - type: String, - required: true, - } + }, + data() { + return { + caption: "", + view_once: false, + compress: false, + type: window.TYPEUSER, + phone: "", + loading: false, + video_url: "", + }; + }, + computed: { + phone_id() { + return this.phone + this.type; + }, + }, + methods: { + openModal() { + $("#modalSendVideo") + .modal({ + onApprove: function () { + return false; + }, + }) + .modal("show"); }, - data() { - return { - caption: '', - view_once: false, - compress: false, - type: window.TYPEUSER, - phone: '', - loading: false, + async handleSubmit() { + try { + if (!this.video_url && !$("#file_video")[0].files[0]) { + throw new Error( + "Please provide either a video URL or upload a video file." + ); } + let response = await this.submitApi(); + showSuccessInfo(response); + $("#modalSendVideo").modal("hide"); + } catch (err) { + showErrorInfo(err); + } }, - computed: { - phone_id() { - return this.phone + this.type; + async submitApi() { + this.loading = true; + try { + let payload = new FormData(); + payload.append("phone", this.phone_id); + payload.append("caption", this.caption); + payload.append("view_once", this.view_once); + payload.append("compress", this.compress); + + if (this.video_url) { + payload.append("video_url", this.video_url); + } else { + payload.append("video", $("#file_video")[0].files[0]); } + + let response = await window.http.post(`/send/video`, 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; + } }, - methods: { - openModal() { - $('#modalSendVideo').modal({ - onApprove: function () { - return false; - } - }).modal('show'); - }, - async handleSubmit() { - try { - let response = await this.submitApi() - showSuccessInfo(response) - $('#modalSendVideo').modal('hide'); - } catch (err) { - showErrorInfo(err) - } - }, - async submitApi() { - this.loading = true; - try { - let payload = new FormData(); - payload.append("phone", this.phone_id) - payload.append("caption", this.caption) - payload.append("view_once", this.view_once) - payload.append("compress", this.compress) - payload.append('video', $("#file_video")[0].files[0]) - let response = await window.http.post(`/send/video`, 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.caption = ''; - this.view_once = false; - this.compress = false; - this.phone = ''; - this.type = window.TYPEUSER; - $("#file_video").val(''); - }, + handleReset() { + this.caption = ""; + this.view_once = false; + this.compress = false; + this.phone = ""; + this.type = window.TYPEUSER; + this.video_url = ""; + $("#file_video").val(""); }, - template: ` + }, + template: `
Send @@ -117,6 +132,10 @@ export default {
+
+ + +
@@ -135,5 +154,5 @@ export default {
- ` -} \ No newline at end of file + `, +};