diff --git a/.gitignore b/.gitignore index 3b71055..3c51b40 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* +logs/error-log.txt # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/assets/feature-request.json b/assets/feature-request.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/assets/feature-request.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/package.json b/package.json index 2949ecd..44c8fdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "whatsapp-bot", - "version": "1.1.0", + "version": "1.3.0", "description": "WhatsApp Bot Replyer, Powered by Node.js+whatsapp-web.js", "main": "dist/index.js", "type": "commonjs", @@ -45,4 +45,4 @@ "tough-cookie": "^4.1.3", "whatsapp-web.js": "^1.23.0" } -} +} \ No newline at end of file diff --git a/src/command-hive.ts b/src/command-hive.ts index e3a3bd5..fcdd195 100644 --- a/src/command-hive.ts +++ b/src/command-hive.ts @@ -9,7 +9,8 @@ import { log } from '@services/internal/log' import { indoSlangQuote } from '@services/internal/quote-indo-slang' import { getPpCouple } from '@services/internal/pp-couple' import { ghola } from '@services/external/ghola' -import { tiktokDownloader } from '@services/external/tiktok-downloader' +import { requestInfo } from '@services/internal/request-info' +import { instagramDownloader } from '@services/external/instagram-downloader' type Commands = { [key: string]: (client: Client, message: Message) => any @@ -21,6 +22,9 @@ const commands: Commands = { // ! Administrative commands '.log': log, + // * Request Feature + '.request': requestInfo, + // * Help '.help': commandGuide, @@ -28,8 +32,9 @@ const commands: Commands = { '.quotes': getForismaticQuotes, '.indoquotes': indoSlangQuote, + // * Converter - '.tiktok': tiktokDownloader, + '.ig': instagramDownloader, // * Random Image '.ppcouple': getPpCouple, diff --git a/src/command-router.ts b/src/command-router.ts index 756d5fd..26f8824 100644 --- a/src/command-router.ts +++ b/src/command-router.ts @@ -11,7 +11,7 @@ const routeCommand = async (client: Client, message: Message) => { const type: string = message.type; const command: string = message.body; - // 1.0, Currently only listen to text/chat or image messages, otherwise ignore it + // From 1.0, for now the bot only listen to text/chat or image messages, otherwise ignore it if (['chat', 'image', 'video'].includes(type) == false) { return 0; } @@ -25,7 +25,7 @@ const routeCommand = async (client: Client, message: Message) => { wweb.sendMessage( client, message.from, - `${config.botShortName} tidak mengerti. Harap ketik \`.help\` untuk mengetahui yang ${config.botShortName} pahami, ${contact?.pushname ?? ''}!` + `${config.botShortName} gak ngerti maksudnya apa😒. Coba ketik \`.help\` biar tau yang ${config.botShortName} paham, makasi ${contact?.pushname ?? ''}☺!` ) } } diff --git a/src/env.ts b/src/env.ts index c65810b..a861ce7 100644 --- a/src/env.ts +++ b/src/env.ts @@ -18,6 +18,11 @@ interface EnvironmentConfiguration { * Bot codename */ botCodeName: string, + + /** + * Owner name of the bot + */ + ownerName: string } const environmentConfiguration: EnvironmentConfiguration = { @@ -25,7 +30,8 @@ const environmentConfiguration: EnvironmentConfiguration = { botName: 'Sora Erlyana', botShortName: 'Sora', - botCodeName: 'SoraErlyana' + botCodeName: 'SoraErlyana', + ownerName: 'Genesaret Johnes' } export default environmentConfiguration \ No newline at end of file diff --git a/src/services/external/instagram-downloader.ts b/src/services/external/instagram-downloader.ts new file mode 100644 index 0000000..301ab92 --- /dev/null +++ b/src/services/external/instagram-downloader.ts @@ -0,0 +1,91 @@ +import { MessageMedia } from 'whatsapp-web.js'; +import { Executor } from '@/command-hive'; +import axios, { AxiosError } from 'axios'; +import * as wweb from '@utils/wweb'; +import * as logger from '@utils/logger'; +import * as cheerio from 'cheerio'; +import config from '@/env'; + +const IG_DOWNLOADER_URL = 'https://v3.igdownloader.app/api/ajaxSearch'; +const SPOOFED_USER_AGENT = 'Mozilla (Firefox Inc.)'; + +type InstagramDom = { + status: string, + v?: string, + data: string +} + +type InstagramMedia = string | undefined; + +const retrieveInstagramDom = async (instagramUrl: string) => { + const response = await axios.post(IG_DOWNLOADER_URL, { + q: instagramUrl, + }, { + withCredentials: true, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'User-Agent': SPOOFED_USER_AGENT + } + }); + + return response.data; +} + +const processInstagramDom = (instagramDom: InstagramDom) => { + if (instagramDom.v === undefined) return undefined; + + const $ = cheerio.load(instagramDom.data); + const instagramMediaUrl: InstagramMedia = $('.download-items__btn').find('a.abutton').attr('href'); + + return instagramMediaUrl; +} + +const downloadMedia = async (instagramMediaUrl: string) => { + const response = await axios.get(instagramMediaUrl, { + responseType: 'arraybuffer', + headers: { + 'User-Agent': SPOOFED_USER_AGENT + } + }); + + return { + media: Buffer.from(response.data, 'binary').toString('base64'), + mime_type: response.headers['content-type'] + }; +} + +const instagramDownloader: Executor = async (client, message) => { + const instagramUrl = message.body.split(' ')[1]; + + if (instagramUrl == undefined) { + wweb.replyMessage(message, `${config.botShortName} tidak melihat adanya URL video IG kamu :(.\n\nGunakan format: \`.ig [URL video IG]\` ya!`); + return 0; + } + + try { + const instagramDom: InstagramDom = await retrieveInstagramDom(instagramUrl); + const instagramMediaUrl: InstagramMedia = processInstagramDom(instagramDom); + + if (instagramMediaUrl === undefined) { + wweb.replyMessage(message, `${config.botShortName} tidak dapat memproses link IG kamu, coba salin ulang lagi dari Instagram nya ya !`); + return 0; + } + + wweb.replyMessage(message, `Tunggu sebentar ya, ${config.botShortName} sedang memproses link IG kamu...`); + // const instagramMedia = await downloadMedia(instagramMediaUrl); + + downloadMedia(instagramMediaUrl).then(instagramMedia => { + wweb.replyMessage(message, new MessageMedia(instagramMedia.mime_type, instagramMedia.media)); + }); + } catch (error) { + + const err = error as AxiosError; + const contact = await message.getContact(); + logger.logError('instagramDownloader - ' + err.cause ?? err.message + ' by ' + contact?.pushname ?? 'unknown'); + wweb.replyMessage(message, `Maaf, ${config.botShortName} mengalami kegagalan saat memprosesnya. Silahkan coba kembali nanti ya! πŸ™`); + } +} + +export { + instagramDownloader +} \ No newline at end of file diff --git a/src/services/external/tiktok-downloader.ts b/src/services/external/tiktok-downloader.ts index b4f694c..4e8c49a 100644 --- a/src/services/external/tiktok-downloader.ts +++ b/src/services/external/tiktok-downloader.ts @@ -64,6 +64,11 @@ const downloadTikTokVideo = async (tiktokUrl: string) => { } const tiktokDownloader: Executor = async (client, message) => { + + // ! Hold the feature, it did not work well + wweb.replyMessage(message, 'Fitur ini sedang dilakukan perbaikan, harap stay tuned untuk informasi selanjutnya :).'); + return 0; + const tiktokUrl = message.body.split(' ')[1]; if ((tiktokUrl == undefined) || (tiktokUrl == '')) { @@ -71,6 +76,7 @@ const tiktokDownloader: Executor = async (client, message) => { message, `${config.botShortName} tidak melihat adanya URL video TikTok kamu :(.\n\nGunakan format: \`.tiktok [URL video TikTok] ya!\`` ); + return 0; } try { diff --git a/src/services/internal/bot-info.ts b/src/services/internal/bot-info.ts new file mode 100644 index 0000000..e0bda6d --- /dev/null +++ b/src/services/internal/bot-info.ts @@ -0,0 +1,20 @@ +import { Executor } from '@/command-hive'; +import * as wweb from '@utils/wweb'; +import config from '@/env'; + +const botInfo: Executor = async (client, message) => { + const informationArray = [ + `⚑Informasi tentang ${config.botName}⚑`, + `Nama: ${config.botName}`, + `Nama Panggilan: ${config.botShortName}`, + `Owner: ${config.ownerName}\n`, + + 'Tahun Rilis:' + 2024, + 'Versi Saat Ini:' + '1.3.0', + 'Teknologi Digunakan :', + '- NodeJS', + '- TypeScript', + '- whatsapp-web.js (wwebjs.dev)', + ]; + wweb.sendMessage(client, message.from, informationArray.join('\n')); +} \ No newline at end of file diff --git a/src/services/internal/command-guide.ts b/src/services/internal/command-guide.ts index cb297b4..ffad406 100644 --- a/src/services/internal/command-guide.ts +++ b/src/services/internal/command-guide.ts @@ -7,11 +7,13 @@ const commandGuide: Executor = async (client, message) => { const commandListMessage: Array = [ `Hai, aku ${config.botName}, siap membantu kamu untuk kegiatan harianmu`, 'Btw, dibawah ini list command yang tersedia: (Bisa ketik perintahnya aja untuk informasi setiap perintahnya ya)\n', + 'β„Ή *Informasi*', + `.botinfo - Informasi tentang ${config.botName}\n`, '🦜 *Quotes*', '.quotes - Quotes Formal', '.indoquotes - Quotes Slang Indonesia\n', - 'πŸŽ₯ *Converter*', - '.tiktok [link TikTok] - Convert link TikTok ke video (dalam tahap pengembangan)\n', + // 'πŸŽ₯ *Converter*', + // '.ig [link Instagram] - Convert link postingan/reels Instagram\n', '🎲 *Random*', '.ppcouple - Gambar PP couple random\n', 'πŸ€– *Fitur AI*', @@ -23,7 +25,6 @@ const commandGuide: Executor = async (client, message) => { '.s (kirim bersama dengan gambarnya)', '.st [teks] (kirim bersama dengan gambarnya)\n', 'πŸ–Ό *TextPro (Buat teks jadi gambar, dengan gaya)*', - '⚠ *DALAM MASA PERBAIKAN*', '.neon', '.lunar', '.thunder', @@ -43,10 +44,12 @@ const commandGuide: Executor = async (client, message) => { '[βš™] Tiktok Downloader', '[βš™] Instagram Downloader', '[βš™] YouTube Downloader', - '[βš™] Bot Information (versi, tech used, etc.)', - '[βš™] Feedback Request Fitur', + '[βœ…] Bot Information (versi, tech used, etc.)', + '[βœ…] Feedback Request Fitur', '[βš™] Donasi Semi-Kemanusiaan (Apaan nih ? Coming soon yaπŸ˜„)\n', - `${config.botShortName} masih tahap pengembangan, banyak perintah juga nantinya ${config.botShortName} bisa lakuin loh >_<, stay tuned yaa. Oh iya, saat ini kamu juga bisa request fitur langsung ke creator ${config.botName}. Bye bye~\n\n`, + `😎 Kamu juga bisa request fitur yang belum ada lho, atau punya saran tertentu, bisa langsung kirim ke ${config.botShortName} dengan format :`, + '`.request [request fitur/saran perbaikan]`', + `Request akan langsung ${config.botShortName} informasikan ke creator. 🫑\n`, `Detail of ${config.botShortName} : https://gensart.notion.site/SoraErlyana-WhatsApp-Bot-7248504bbe18476e912912a9426b9bad` ]; diff --git a/src/services/internal/request-info.ts b/src/services/internal/request-info.ts new file mode 100644 index 0000000..dbc0995 --- /dev/null +++ b/src/services/internal/request-info.ts @@ -0,0 +1,30 @@ +import { Executor } from '@/command-hive'; +import * as wweb from '@utils/wweb'; +import fs from 'fs/promises'; + +const requestInfo: Executor = async (client, message) => { + let request = message.body.split(' ').slice(1).join(' '); + + if (request == '') { + wweb.replyMessage(message, 'Request/sarannya apa nih πŸ˜₯?') + return 0; + } + + const contact = await message.getContact(); + request = request + ' | oleh ' + contact?.pushname ?? 'unknown' + await recordRequest(request) + wweb.replyMessage(message, 'Terimakasih ! 😁'); +} + +const recordRequest = async (text: string) => { + fs.readFile('assets/feature-request.json', 'utf-8') + .then(data => { + const records = JSON.parse(data); + records.push(text) + fs.writeFile('assets/feature-request.json', JSON.stringify(records), 'utf-8') + }) +} + +export { + requestInfo +} \ No newline at end of file diff --git a/src/utils/textpro.ts b/src/utils/textpro.ts index 32b263b..7a1f81d 100644 --- a/src/utils/textpro.ts +++ b/src/utils/textpro.ts @@ -41,6 +41,8 @@ type TextProImageObject = { mimetype: string } +const SPOOFED_USER_AGENT = 'Mozilla (Firefox Inc.)'; + /** * Retrieves an image from a given URL using the provided Axios instance. * @@ -51,7 +53,10 @@ type TextProImageObject = { const getImageFromTextPro = async (imageUrl: string, axiosInstance: AxiosInstance): Promise => { const textProUrl = 'https://textpro.me'; const imageResponse = await axiosInstance.get(textProUrl + imageUrl, { - 'responseType': 'arraybuffer' + responseType: 'arraybuffer', + headers: { + 'User-Agent': SPOOFED_USER_AGENT + } }); const imageBase64: string = Buffer.from(imageResponse.data, 'binary').toString('base64'); @@ -79,7 +84,8 @@ const getImageUrlFromTextPro = async (metadata: object, axiosInstance: AxiosInst const response = await axiosInstance.post(textProImageHiveUrl, metadata, { headers: { - "Content-Type": "application/x-www-form-urlencoded", + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': SPOOFED_USER_AGENT } }); @@ -106,6 +112,7 @@ const getImageMetadataFromTextPro = async (metadata: TextProMetadata): Promise} the token retrieved from the TextPro page */ const getTokenFromTextProPage = async (textProUrl: string, axiosInstance: AxiosInstance): Promise => { - const response = await axiosInstance.get(textProUrl); + const response = await axiosInstance.get(textProUrl, { + headers: { + 'User-Agent': SPOOFED_USER_AGENT + } + }); if (response.status == 200) { const $ = cheerio.load(response.data);