├── .env.example ├── .eslintrc.js ├── .github └── workflows │ └── deploy.yaml.disable-for-now ├── .gitignore ├── .hiddens └── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── QUICK_START.md ├── README.md ├── bun.lock ├── bun.lockb ├── ecosystem.config.js ├── etc ├── screen_auto_updater.sh └── supervisor │ └── personal-whatsapp-bot.conf ├── package.json ├── services ├── downloader │ └── tiktok │ │ ├── diioffc.ts │ │ ├── index.ts │ │ ├── nasirxml.ts │ │ ├── ssateam.test.ts │ │ ├── ssateam.ts │ │ ├── tiklydown.test.ts │ │ ├── tiklydown.ts │ │ ├── tiktok.d.ts │ │ ├── ttsave.test.ts │ │ ├── ttsave.ts │ │ └── vapis.ts ├── gemini │ ├── gemini.ts │ └── index.ts └── user-agent │ ├── index.ts │ └── user-agents.txt ├── src ├── cli │ ├── index.ts │ └── session-manager.ts ├── infrastructure │ ├── config │ │ ├── bootstrap.config.ts │ │ └── consts.config.ts │ ├── logger │ │ ├── console.logger.ts │ │ └── service.logger.ts │ ├── supports │ │ ├── boolean.support.ts │ │ ├── file.support.ts │ │ ├── promise.support.ts │ │ ├── regex.ts │ │ ├── string.support.ts │ │ └── whatsapp.support.ts │ └── whatsapp │ │ ├── make-in-memory-store.ts │ │ ├── make-ordered-dictionary.ts │ │ └── whatsapp-client.ts ├── main.ts └── modules │ ├── ai │ └── gemini.handler.ts │ ├── always-executed │ ├── anti-edit-message.handler.ts │ ├── anti-viewonce.handler.ts │ ├── auto-reveal-delete-message.handler.ts │ └── auto-reveal-viewonce-when-quoted.handler.ts │ ├── group │ ├── extract-phoneNumber.handler.ts │ └── mention.handler.ts │ ├── information │ ├── help.handler.ts │ ├── ping.handler.ts │ └── startup.handler.ts │ ├── owner │ └── shell.handler.ts │ └── random │ ├── downloader │ └── tiktok.handler.ts │ ├── img-to-sticker.handler.ts │ └── sticker-to-img.handler.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=file:./dev.db 2 | COMMAND_SIGN=. 3 | GEMINI_API_KEY= 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml.disable-for-now: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: 3 | push: 4 | paths: 5 | - 'libs/**' 6 | - 'src/**' 7 | - '.github/workflows/deploy.yaml' 8 | branches: 9 | - main 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Tailscale 17 | uses: tailscale/github-action@v2 18 | with: 19 | oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} 20 | oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} 21 | tags: tag:githubdeploy 22 | 23 | - name: Deploy 24 | uses: appleboy/ssh-action@v1.0.3 25 | with: 26 | host: ${{ secrets.HOSTNAME }} 27 | username: root 28 | key: ${{ secrets.SSH_PRIVATE_KEY }} 29 | port: 22 30 | script_stop: true 31 | script: | 32 | cd ${{ secrets.SERVER_CWD }} 33 | git pull origin main 34 | /root/.bun/bin/bun i --no-save --frozen-lockfile 35 | /root/.bun/bin/bun start 36 | supervisorctl restart personal-whatsapp-bot 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | .env 3 | .auths 4 | .data_store 5 | node_modules 6 | database.json 7 | dist 8 | dev.db 9 | .idea 10 | .DS_Store 11 | 12 | # Session storage 13 | .hiddens/ 14 | 15 | # Logs 16 | logs/ 17 | *.log 18 | 19 | # PM2 20 | .pm2/ -------------------------------------------------------------------------------- /.hiddens/.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | !.gitignore -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "non-relative", 3 | "yaml.schemas": { 4 | "https://json.schemastore.org/github-workflow.json": "file:///Users/mymac/Workspace/binsar/personal-assistant/.github/workflows/deploy.yaml" 5 | } 6 | } -------------------------------------------------------------------------------- /QUICK_START.md: -------------------------------------------------------------------------------- 1 | # 🚀 Quick Start Guide - Personal Assistant 2 | 3 | ## 📋 Menu Utama Interactive 4 | 5 | ```bash 6 | bun run interactive 7 | ``` 8 | 9 | **Menu yang tersedia:** 10 | 11 | 1. **🔍 Pilih Session yang Ada** - Jalankan session yang sudah ada 12 | 2. **🆕 Buat Session Baru** - Buat session baru dengan nama custom 13 | 3. **🗑️ Hapus Session** - Hapus session yang tidak diperlukan 14 | 4. **❌ Keluar** - Keluar dari aplikasi 15 | 16 | ## 🎯 Common Commands 17 | 18 | | Command | Description | 19 | | ---------------------------------- | -------------------------------------- | 20 | | `bun run interactive` | **Mode interactive dengan menu utama** | 21 | | `bun run sessions` | **Lihat semua session** | 22 | | `bun run start -s mybot -m qrcode` | **Jalankan session langsung** | 23 | | `bun run start --help` | **Bantuan lengkap** | 24 | 25 | ## 🔧 Setup Session Baru 26 | 27 | ### 1. Interactive Mode (Recommended) 28 | 29 | ```bash 30 | bun run interactive 31 | # Pilih: 2. 🆕 Buat Session Baru 32 | # Input nama: "my-whatsapp-bot" 33 | # Pilih mode: QR Code atau Pairing 34 | ``` 35 | 36 | ### 2. Direct Mode 37 | 38 | ```bash 39 | # QR Code 40 | bun run start -s "my-whatsapp-bot" -m qrcode 41 | 42 | # Pairing Code 43 | bun run start -s "my-whatsapp-bot" -m pairing -p +6281234567890 44 | ``` 45 | 46 | ## 📊 Session Status 47 | 48 | - 🟢 **Active**: Siap digunakan 49 | - 🔴 **Inactive**: Belum ada credentials 50 | - 🟡 **Corrupted**: Auth store tidak lengkap 51 | 52 | ## 🗑️ Hapus Session 53 | 54 | ### Interactive Mode 55 | 56 | ```bash 57 | bun run interactive 58 | # Pilih: 3. 🗑️ Hapus Session 59 | # Pilih session dari daftar 60 | ``` 61 | 62 | ### Manual Delete 63 | 64 | ```bash 65 | rm -rf .hiddens/session-name 66 | ``` 67 | 68 | ## 🚀 Production dengan PM2 69 | 70 | ```bash 71 | # Install PM2 72 | npm install -g pm2 73 | 74 | # Jalankan single session 75 | pm2 start "bun run start -s mybot -m qrcode" --name "whatsapp-bot" 76 | 77 | # Jalankan multiple sessions 78 | pm2 start ecosystem.config.js 79 | 80 | # Monitor 81 | pm2 logs whatsapp-bot 82 | pm2 status 83 | ``` 84 | 85 | ## 🆘 Troubleshooting 86 | 87 | ### Session Corrupted (🟡) 88 | 89 | ```bash 90 | # Hapus dan buat ulang 91 | rm -rf .hiddens/session-name 92 | bun run interactive 93 | # Pilih: 2. 🆕 Buat Session Baru 94 | ``` 95 | 96 | ### Bot Tidak Merespons 97 | 98 | ```bash 99 | # Cek status session 100 | bun run sessions 101 | 102 | # Restart dengan PM2 103 | pm2 restart whatsapp-bot 104 | ``` 105 | 106 | ### Prompt CLI Terganggu Log 107 | 108 | - Masalah sudah diperbaiki: Log decorator dipindah setelah CLI selesai 109 | - Jika masih terjadi, pastikan tidak ada console.log sebelum CLI 110 | 111 | ## 📱 Contoh Workflow 112 | 113 | 1. **First Time Setup**: 114 | ```bash 115 | bun run interactive 116 | # → Pilih "2. 🆕 Buat Session Baru" 117 | # → Input nama: "production-bot" 118 | # → Pilih "1. QR Code" 119 | # → Scan QR di WhatsApp 120 | ``` 121 | 122 | 2. **Daily Usage**: 123 | ```bash 124 | bun run interactive 125 | # → Pilih "1. 🔍 Pilih Session yang Ada" 126 | # → Pilih "production-bot" 127 | # → Bot running! 128 | ``` 129 | 130 | 3. **Production Deployment**: 131 | ```bash 132 | pm2 start "bun run start -s production-bot -m qrcode" --name "wa-bot" 133 | ``` 134 | 135 | ## 🎉 Tips 136 | 137 | - Gunakan **interactive mode** untuk setup yang mudah 138 | - Nama session bisa bebas (misal: `customer-service`, `personal-bot`, dll) 139 | - Session disimpan di folder `.hiddens/` 140 | - Backup session penting sebelum menghapus 141 | - Gunakan PM2 untuk production deployment 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Personal Assistant - WhatsApp Bot 2 | 3 | Personal Assistant adalah bot WhatsApp yang dibangun dengan TypeScript dan Bun, 4 | menggunakan library Baileys untuk koneksi WhatsApp Web. 5 | 6 | ## 🚀 Fitur 7 | 8 | - **Multi-Session Support**: Kelola beberapa session WhatsApp sekaligus 9 | - **Interactive CLI**: Setup mudah dengan antarmuka CLI yang user-friendly 10 | - **Session Management**: Tampilkan, pilih, dan kelola session dengan detail 11 | - **Auto Keep-Alive**: Bot tetap aktif dengan sistem monitoring otomatis 12 | - **PM2 Ready**: Siap dijalankan dengan PM2 untuk production 13 | 14 | ## 📋 Persyaratan 15 | 16 | - Node.js 17+ atau Bun 17 | - WhatsApp account untuk linking 18 | 19 | ## 🛠️ Instalasi 20 | 21 | ```bash 22 | # Clone repository 23 | git clone 24 | cd personal-assistant 25 | 26 | # Install dependencies 27 | bun install 28 | 29 | # Atau dengan npm 30 | npm install 31 | ``` 32 | 33 | ## 🎯 Penggunaan 34 | 35 | ### Mode Interactive (Recommended) 36 | 37 | Mode ini memberikan pengalaman setup yang mudah dengan GUI CLI dan menu utama: 38 | 39 | ```bash 40 | # Jalankan mode interactive 41 | bun run interactive 42 | 43 | # Atau 44 | bun run start --interactive 45 | ``` 46 | 47 | Mode interactive akan menampilkan **Menu Utama** dengan pilihan: 48 | 49 | 1. **🔍 Pilih Session yang Ada** - Memilih dari session yang sudah ada 50 | 2. **🆕 Buat Session Baru** - Membuat session baru dengan nama custom 51 | 3. **🗑️ Hapus Session** - Menghapus session yang tidak diperlukan 52 | 4. **❌ Keluar** - Keluar dari aplikasi 53 | 54 | Setelah memilih action, sistem akan: 55 | 56 | - Menampilkan semua session yang tersedia dengan detail 57 | - Memungkinkan pemilihan session 58 | - Meminta detail koneksi jika diperlukan 59 | - Menjalankan bot dengan konfigurasi yang dipilih 60 | 61 | ### Mode Direct 62 | 63 | Untuk penggunaan langsung dengan parameter: 64 | 65 | ```bash 66 | # QR Code mode 67 | bun run start --session mybot --mode qrcode 68 | 69 | # Pairing Code mode 70 | bun run start --session mybot --mode pairing --phone +6281234567890 71 | ``` 72 | 73 | ### Manajemen Session 74 | 75 | ```bash 76 | # Lihat semua session 77 | bun run sessions 78 | 79 | # Atau 80 | bun run start --list 81 | 82 | # Mode interactive (dengan menu utama) 83 | bun run interactive 84 | 85 | # Lihat bantuan 86 | bun run start --help 87 | ``` 88 | 89 | ## 📊 Session Management 90 | 91 | ### Melihat Daftar Session 92 | 93 | ```bash 94 | bun run sessions 95 | ``` 96 | 97 | Output akan menampilkan: 98 | 99 | - 🟢 **Active**: Session siap digunakan 100 | - 🔴 **Inactive**: Belum ada kredensial 101 | - 🟡 **Corrupted**: Auth store tidak lengkap 102 | 103 | ### Detail Session 104 | 105 | Setiap session menampilkan: 106 | 107 | - **Path**: Lokasi penyimpanan session 108 | - **Last Modified**: Waktu terakhir digunakan 109 | - **Size**: Ukuran data session 110 | - **Auth Store**: Status auth store 111 | - **Credentials**: Status kredensial 112 | - **Status**: Status keseluruhan session 113 | 114 | ## 🔧 Production dengan PM2 115 | 116 | > **Catatan:** Mulai versi terbaru, CLI _tidak lagi menjalankan atau mengelola 117 | > PM2 secara otomatis_. Semua perintah PM2 harus dijalankan manual oleh user. 118 | > Ini demi keamanan, transparansi, dan best practice production. 119 | 120 | ### Setup PM2 121 | 122 | ```bash 123 | # Install PM2 globally 124 | npm install -g pm2 125 | 126 | # Jalankan dengan PM2 (manual, sesuai session yang diinginkan) 127 | pm2 start "bun run start -s mybot -m qrcode" --name "whatsapp-bot" 128 | 129 | # Atau dengan config file 130 | pm2 start ecosystem.config.js 131 | ``` 132 | 133 | ### Ecosystem Config 134 | 135 | Buat file `ecosystem.config.js`: 136 | 137 | ```javascript 138 | module.exports = { 139 | apps: [{ 140 | name: "whatsapp-bot", 141 | script: "bun", 142 | args: "run start -s mybot -m qrcode", 143 | instances: 1, 144 | autorestart: true, 145 | watch: false, 146 | max_memory_restart: "1G", 147 | env: { 148 | NODE_ENV: "production", 149 | }, 150 | }], 151 | }; 152 | ``` 153 | 154 | > **Untuk melihat log:** Jalankan manual: `pm2 logs whatsapp-bot` 155 | 156 | ## 🎮 CLI Commands 157 | 158 | | Command | Description | 159 | | ---------------------- | --------------------------------------------------- | 160 | | `bun run start` | Jalankan bot (interactive mode jika tidak ada args) | 161 | | `bun run interactive` | Mode interactive dengan menu utama | 162 | | `bun run sessions` | Lihat semua session | 163 | | `bun run start --help` | Bantuan lengkap | 164 | | `bun run wa:logout` | Hapus session default | 165 | 166 | ## 📝 CLI Options 167 | 168 | | Option | Short | Description | 169 | | ------------------ | ----- | ---------------------------- | 170 | | `--session ` | `-s` | Nama session | 171 | | `--mode ` | `-m` | Mode koneksi: qrcode/pairing | 172 | | `--phone ` | `-p` | Nomor telepon untuk pairing | 173 | | `--interactive` | `-i` | Mode interactive | 174 | | `--list` | `-l` | Tampilkan daftar session | 175 | | `--help` | `-h` | Bantuan | 176 | 177 | ## 🔍 Contoh Penggunaan 178 | 179 | ### Setup Session Baru 180 | 181 | ```bash 182 | # Interactive mode (recommended) - dengan menu utama 183 | bun run interactive 184 | 185 | # Pilih menu "2. 🆕 Buat Session Baru" 186 | # Masukkan nama session: "bot-customer-service" 187 | # Pilih mode koneksi (QR Code/Pairing) 188 | 189 | # Direct mode 190 | bun run start -s "bot-customer-service" -m qrcode 191 | ``` 192 | 193 | ### Menjalankan Session yang Ada 194 | 195 | ```bash 196 | # Interactive mode - pilih dari menu utama 197 | bun run interactive 198 | 199 | # Pilih menu "1. 🔍 Pilih Session yang Ada" 200 | # Pilih session dari daftar 201 | 202 | # Langsung jalankan session tertentu 203 | bun run start -s "bot-customer-service" -m qrcode 204 | ``` 205 | 206 | ### Menghapus Session 207 | 208 | ```bash 209 | # Interactive mode - menu utama 210 | bun run interactive 211 | 212 | # Pilih menu "3. 🗑️ Hapus Session" 213 | # Pilih session yang ingin dihapus dari daftar 214 | 215 | # Manual delete (hati-hati!) 216 | rm -rf .hiddens/session-name 217 | ``` 218 | 219 | ### Monitoring Session 220 | 221 | ```bash 222 | # Lihat semua session 223 | bun run sessions 224 | 225 | # Output contoh: 226 | # 📱 Daftar Session WhatsApp: 227 | # ================================================================================ 228 | # 1. 🟢 bot-customer-service 229 | # 📁 Path: .hiddens/bot-customer-service 230 | # 📅 Last Modified: 15/01/2024 10:30:45 231 | # 💾 Size: 2.5 MB 232 | # 🔐 Auth Store: ✅ 233 | # 🔑 Credentials: ✅ 234 | # 📊 Status: 🟢 Active (Ready to use) 235 | ``` 236 | 237 | ## 🚨 Troubleshooting 238 | 239 | ### Session Corrupted 240 | 241 | Jika session menunjukkan status 🟡 Corrupted: 242 | 243 | ```bash 244 | # Hapus session yang bermasalah 245 | rm -rf .hiddens/session-name 246 | 247 | # Buat ulang session 248 | bun run start -s session-name -m qrcode 249 | ``` 250 | 251 | ### Bot Tidak Merespons 252 | 253 | 1. Periksa status session dengan `bun run sessions` 254 | 2. Pastikan WhatsApp Web tidak login di browser lain 255 | 3. Restart bot dengan PM2: `pm2 restart whatsapp-bot` 256 | 257 | ### Koneksi Gagal 258 | 259 | 1. Pastikan internet stabil 260 | 2. Coba mode pairing jika QR code gagal 261 | 3. Periksa log untuk error detail 262 | 263 | ## 🤝 Contributing 264 | 265 | 1. Fork repository 266 | 2. Buat branch feature (`git checkout -b feature/amazing-feature`) 267 | 3. Commit changes (`git commit -m 'Add amazing feature'`) 268 | 4. Push ke branch (`git push origin feature/amazing-feature`) 269 | 5. Buat Pull Request 270 | 271 | ## 📄 License 272 | 273 | Distributed under the MIT License. See `LICENSE` for more information. 274 | 275 | ## 🙏 Acknowledgments 276 | 277 | - [Baileys](https://github.com/WhiskeySockets/Baileys) - WhatsApp Web API 278 | - [Bun](https://bun.sh/) - Fast JavaScript runtime 279 | - [PM2](https://pm2.keymetrics.io/) - Process manager 280 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binsarjr/personal-assistant/55ca27aece268b02e9f17812a7063c2867fcceae/bun.lockb -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'personal-assistant-main', 5 | script: 'bun', 6 | args: 'run start -s main -m qrcode', 7 | instances: 1, 8 | autorestart: true, 9 | watch: false, 10 | max_memory_restart: '1G', 11 | env: { 12 | NODE_ENV: 'production', 13 | BOT_NAME: 'Personal Assistant Main' 14 | }, 15 | env_production: { 16 | NODE_ENV: 'production', 17 | BOT_NAME: 'Personal Assistant Main' 18 | }, 19 | error_file: 'logs/personal-assistant-main-error.log', 20 | out_file: 'logs/personal-assistant-main-out.log', 21 | log_file: 'logs/personal-assistant-main.log', 22 | time: true 23 | }, 24 | { 25 | name: 'personal-assistant-backup', 26 | script: 'bun', 27 | args: 'run start -s backup -m pairing -p +6281234567890', 28 | instances: 1, 29 | autorestart: true, 30 | watch: false, 31 | max_memory_restart: '1G', 32 | env: { 33 | NODE_ENV: 'production', 34 | BOT_NAME: 'Personal Assistant Backup' 35 | }, 36 | env_production: { 37 | NODE_ENV: 'production', 38 | BOT_NAME: 'Personal Assistant Backup' 39 | }, 40 | error_file: 'logs/personal-assistant-backup-error.log', 41 | out_file: 'logs/personal-assistant-backup-out.log', 42 | log_file: 'logs/personal-assistant-backup.log', 43 | time: true 44 | } 45 | ] 46 | }; -------------------------------------------------------------------------------- /etc/screen_auto_updater.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # RUN with crontab 4 | 5 | # ================== KONFIGURASI ================== 6 | PROJECT_NAME="PersonalAssistant" 7 | PROJECT_DIR="/root/personal-assistant" 8 | BRANCH="main" 9 | LOG_FILE="/var/log/deploy_personal_assistant.log" 10 | DEPLOY_LOCK="/tmp/deploy_personal_assistant.lock" 11 | SCREEN_NAME="PersonalAssistant" 12 | BUN_PATH="/root/.bun/bin" 13 | ENV_FILE=".env" 14 | # ================================================ 15 | export PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.bun/bin 16 | export PATH=$PATH:$BUN_PATH 17 | 18 | # Log waktu pemanggilan deploy 19 | echo "$(date) - Checking for updates..." >> "$LOG_FILE" 20 | 21 | # Masuk ke direktori proyek 22 | cd "$PROJECT_DIR" || exit 23 | 24 | # Pastikan di branch yang benar 25 | git checkout "$BRANCH" 26 | 27 | # Cek apakah proses deploy sedang berjalan 28 | cleanup() { 29 | rm -f "$DEPLOY_LOCK" 30 | } 31 | 32 | if [[ -f "$DEPLOY_LOCK" ]]; then 33 | echo "$(date) - Deployment is already in progress. Skipping new deployment." >> "$LOG_FILE" 34 | exit 0 35 | fi 36 | 37 | touch "$DEPLOY_LOCK" 38 | trap cleanup EXIT # Pastikan lock file dihapus setelah selesai 39 | 40 | # Ambil argumen untuk opsi --force 41 | FORCE_DEPLOY=false 42 | if [[ "$1" == "--force" ]]; then 43 | FORCE_DEPLOY=true 44 | fi 45 | 46 | # Simpan hash commit sebelum pull 47 | PREV_COMMIT=$(git rev-parse HEAD) 48 | 49 | # Cek perubahan dari git pull 50 | GIT_OUTPUT=$(git pull) 51 | NEW_COMMIT=$(git rev-parse HEAD) 52 | 53 | if [[ "$GIT_OUTPUT" == *"Already up to date."* && "$FORCE_DEPLOY" == false && "$PREV_COMMIT" == "$NEW_COMMIT" ]]; then 54 | echo "$(date) - No updates found. Skipping restart." >> "$LOG_FILE" 55 | exit 0 56 | else 57 | echo "$(date) - Updates detected or forced deployment triggered. Running Bun install..." >> "$LOG_FILE" 58 | 59 | # Install dependensi 60 | bun install 61 | 62 | # Restart aplikasi 63 | echo "$(date) - Restarting application..." >> "$LOG_FILE" 64 | 65 | screen -S "$SCREEN_NAME" -X quit 66 | pkill -f "bun start" 67 | screen -wipe 68 | 69 | # Jalankan aplikasi di dalam screen baru 70 | export $(cat "$ENV_FILE" | xargs) 71 | screen -dmS "$SCREEN_NAME" bun start 72 | 73 | echo "$(date) - Deployment finished successfully!" >> "$LOG_FILE" 74 | fi -------------------------------------------------------------------------------- /etc/supervisor/personal-whatsapp-bot.conf: -------------------------------------------------------------------------------- 1 | [program:personal-whatsapp-bot] 2 | command=/root/.nvm/versions/node/v21.7.1/bin/node --env-file=.env dist/main.js 3 | directory=/personal-asistant 4 | autostart=false 5 | autorestart=true 6 | user=root 7 | redirect_stderr=true 8 | stdout_logfile=/logs/personal-whatsapp-bot.log 9 | stderr_logfile=/logs/personal-whatsapp-bot-error.log 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bunshint-agent", 3 | "version": "4.1.0", 4 | "module": "index.ts", 5 | "type": "module", 6 | "scripts": { 7 | "start": "bun run --bun src/main.ts", 8 | "dev": "bun run --bun --watch src/main.ts", 9 | "wa:logout": "rm -rf .hiddens/personal-assistant", 10 | "sessions": "bun run --bun src/main.ts --list", 11 | "interactive": "bun run --bun src/main.ts --interactive", 12 | "create": "bun run --bun src/main.ts --interactive", 13 | "delete": "bun run --bun src/main.ts --interactive" 14 | }, 15 | "devDependencies": { 16 | "@types/bun": "^1.2.18", 17 | "@types/minimist": "^1.2.5" 18 | }, 19 | "peerDependencies": { 20 | "typescript": "^5.8.3" 21 | }, 22 | "dependencies": { 23 | "@google/generative-ai": "^0.24.1", 24 | "@whiskeysockets/baileys": "^6.7.18", 25 | "baileys-decorators": "https://github.com/binsarjr/baileys-decorators", 26 | "cheerio": "^1.1.0", 27 | "croner": "^9.1.0", 28 | "libphonenumber-js": "^1.12.9", 29 | "libsignal": "^2.0.1", 30 | "minimist": "^1.2.8", 31 | "node-cache": "^5.1.2", 32 | "pino-pretty": "^13.0.0", 33 | "qrcode-terminal": "^0.12.0", 34 | "reflect-metadata": "^0.2.2", 35 | "sharp": "^0.34.3", 36 | "wa-sticker-formatter": "^4.4.4" 37 | }, 38 | "trustedDependencies": [ 39 | "@whiskeysockets/baileys", 40 | "baileys-decorators", 41 | "esbuild", 42 | "protobufjs", 43 | "sharp" 44 | ] 45 | } -------------------------------------------------------------------------------- /services/downloader/tiktok/diioffc.ts: -------------------------------------------------------------------------------- 1 | import type { TiktokDlResult } from '$services/downloader/tiktok/tiktok'; 2 | 3 | type Root = { 4 | status: boolean; 5 | creator: string; 6 | result: { 7 | id: string; 8 | region: string; 9 | title: string; 10 | cover: string; 11 | ai_dynamic_cover: string; 12 | origin_cover: string; 13 | duration: number; 14 | play: string; 15 | wmplay: string; 16 | hdplay: string; 17 | size: number; 18 | wm_size: number; 19 | hd_size: number; 20 | music: string; 21 | music_info: { 22 | id: string; 23 | title: string; 24 | play: string; 25 | cover: string; 26 | author: string; 27 | original: boolean; 28 | duration: number; 29 | album: string; 30 | }; 31 | play_count: number; 32 | digg_count: number; 33 | comment_count: number; 34 | share_count: number; 35 | download_count: number; 36 | collect_count: number; 37 | create_time: number; 38 | anchors: any; 39 | anchors_extras: string; 40 | is_ad: boolean; 41 | commerce_info: { 42 | adv_promotable: boolean; 43 | auction_ad_invited: boolean; 44 | branded_content_type: number; 45 | with_comment_filter_words: boolean; 46 | }; 47 | commercial_video_info: string; 48 | item_comment_settings: number; 49 | mentioned_users: string; 50 | author: { 51 | id: string; 52 | unique_id: string; 53 | nickname: string; 54 | avatar: string; 55 | }; 56 | images?: Array; 57 | }; 58 | }; 59 | 60 | export const diioffc = async (url: string): Promise => { 61 | const target = new URL('https://api.diioffc.web.id/api/tiktok'); 62 | target.searchParams.append('url', url); 63 | const resp = await fetch(target); 64 | if (!resp.ok) return undefined; 65 | 66 | const data: Root = await resp.json(); 67 | const caption = ` 68 | ${data.result.author.nickname} (${data.result.author.unique_id}) 69 | 70 | ${data.result.title} 71 | 72 | ▶️ ${data.result.play_count} 73 | 🤍 ${data.result.digg_count} 74 | 💬 ${data.result.comment_count} 75 | 🔖 ${data.result.download_count} 76 | 🔗 ${data.result.share_count} 77 | 78 | 🎶 ${data.result.music_info.title} (${data.result.music_info.author}) 79 | 80 | created at: ${new Date(data.result.create_time * 1000).toLocaleString()} 81 | ` 82 | .trim() 83 | .replace(/TiklyDown/gi, ''); 84 | 85 | return { 86 | caption, 87 | video: data.result?.images?.length ? undefined : data.result.hdplay, 88 | audio: data.result.music, 89 | slides: data.result.images?.length 90 | ? data.result.images.map((image) => image) 91 | : undefined, 92 | }; 93 | }; 94 | -------------------------------------------------------------------------------- /services/downloader/tiktok/index.ts: -------------------------------------------------------------------------------- 1 | import { diioffc } from '$services/downloader/tiktok/diioffc'; 2 | import { nasirxml } from '$services/downloader/tiktok/nasirxml'; 3 | import { ssateam } from '$services/downloader/tiktok/ssateam'; 4 | import { tiklydown } from '$services/downloader/tiktok/tiklydown'; 5 | import { ttsave } from '$services/downloader/tiktok/ttsave'; 6 | import { ttdlv, ttdlv2 } from '$services/downloader/tiktok/vapis'; 7 | 8 | export const tiktokdl = { 9 | ttsave, 10 | tiklydown, 11 | ssateam, 12 | vapis: { 13 | ttdlv2, 14 | ttdlv, 15 | }, 16 | nasirxml, 17 | diioffc, 18 | }; 19 | -------------------------------------------------------------------------------- /services/downloader/tiktok/nasirxml.ts: -------------------------------------------------------------------------------- 1 | import type { TiktokDlResult } from '$services/downloader/tiktok/tiktok'; 2 | 3 | type NasirxmlTiktok = { 4 | creator: string; 5 | status: number; 6 | result: { 7 | id: number; 8 | title: string; 9 | url: string; 10 | created_at: string; 11 | stats: { 12 | likeCount: string; 13 | commentCount: number; 14 | shareCount: number; 15 | playCount: string; 16 | saveCount: number; 17 | }; 18 | video?: { 19 | noWatermark: string; 20 | watermark: string; 21 | cover: string; 22 | dynamic_cover: string; 23 | origin_cover: string; 24 | width: number; 25 | height: number; 26 | durationFormatted: string; 27 | duration: number; 28 | ratio: string; 29 | }; 30 | images?: { 31 | url: string; 32 | width: number; 33 | height: number; 34 | }[]; 35 | music: { 36 | id: number; 37 | title: string; 38 | author: string; 39 | cover_hd: string; 40 | cover_large: string; 41 | cover_medium: string; 42 | cover_thumb: string; 43 | durationFormatted: string; 44 | duration: number; 45 | play_url: string; 46 | }; 47 | author: { 48 | id: string; 49 | name: string; 50 | unique_id: string; 51 | signature: string; 52 | avatar: string; 53 | avatar_thumb: string; 54 | }; 55 | }; 56 | }; 57 | 58 | const tiktok = async (url: string): Promise => { 59 | const target = new URL('https://api.nasirxml.my.id/api/tiktok'); 60 | target.searchParams.append('urls', url); 61 | target.searchParams.append('apiKey', 'root'); 62 | 63 | const resp = await fetch(target); 64 | if (!resp.ok) return undefined; 65 | 66 | const data: NasirxmlTiktok = await resp.json(); 67 | if (!data.status) return undefined; 68 | const caption = ` 69 | ${data.result.author.name} (${data.result.author.unique_id}) 70 | 71 | ${data.result.title} 72 | 73 | ▶️ ${data.result.stats.playCount} 74 | 🤍 ${data.result.stats.likeCount} 75 | 💬 ${data.result.stats.commentCount} 76 | 🔖 ${data.result.stats.saveCount} 77 | 🔗 ${data.result.stats.shareCount} 78 | 79 | 🎶 ${data.result.music.title} (${data.result.music.author}) 80 | 81 | created at: ${data.result.created_at} 82 | 83 | ` 84 | .trim() 85 | .replace(/TiklyDown/gi, ''); 86 | 87 | return { 88 | caption, 89 | video: data.result.video ? data.result.video.noWatermark : undefined, 90 | audio: data.result.music.play_url, 91 | slides: data.result.images?.length 92 | ? data.result.images.map((image) => image.url) 93 | : undefined, 94 | }; 95 | }; 96 | type Tiktok2 = { 97 | creator: string; 98 | status: number; 99 | result: { 100 | videoId: string; 101 | cover: string; 102 | description: string; 103 | author: { 104 | username: string; 105 | nickname: string; 106 | avatar: string; 107 | }; 108 | media: { 109 | images: Array<{ 110 | url: string; 111 | }>; 112 | videos: Array<{ 113 | type: string; 114 | format: string; 115 | size: string; 116 | url: string; 117 | }>; 118 | audios: Array<{ 119 | type: string; 120 | format: string; 121 | title: string; 122 | url: string; 123 | converting: boolean; 124 | }>; 125 | }; 126 | }; 127 | }; 128 | 129 | export const nasirxml = { 130 | tiktok, 131 | }; 132 | -------------------------------------------------------------------------------- /services/downloader/tiktok/ssateam.test.ts: -------------------------------------------------------------------------------- 1 | import { ssateam } from '$services/downloader/tiktok/ssateam'; 2 | import { expect, it } from 'bun:test'; 3 | 4 | it('ssateam try using valid url', async () => { 5 | const result = await ssateam('https://vt.tiktok.com/ZS6KHACMH/'); 6 | expect(result).toBeDefined(); 7 | console.log(result); 8 | expect(result).toHaveProperty('data'); 9 | }); 10 | 11 | it('ssateam try using invalid url', async () => { 12 | const result = await ssateam('https://vt.tiktok.com/ZSjA3BsadCS1/'); 13 | expect(result).toBeUndefined(); 14 | }); 15 | -------------------------------------------------------------------------------- /services/downloader/tiktok/ssateam.ts: -------------------------------------------------------------------------------- 1 | import type { TiktokDlResult } from '$services/downloader/tiktok/tiktok'; 2 | 3 | export type SSATeamTiktok = { 4 | creator: string; 5 | data: { 6 | music: { 7 | author: string; 8 | url: string; 9 | title: string; 10 | }; 11 | author: { 12 | avatar: string; 13 | username: string; 14 | name: string; 15 | }; 16 | status: string; 17 | content: { 18 | video?: string; 19 | images?: string[]; 20 | }; 21 | }; 22 | }; 23 | 24 | export const ssateam = async ( 25 | url: string, 26 | ): Promise => { 27 | const target = new URL('https://api.ssateam.my.id/api/tiktok'); 28 | 29 | target.searchParams.append('urls', url); 30 | target.searchParams.append('apiKey', 'root'); 31 | 32 | const resp = await fetch(target); 33 | if (!resp.ok) return undefined; 34 | 35 | const data: SSATeamTiktok = await resp.json(); 36 | const caption = ` 37 | ${data.data.author.name} (${data.data.author.username}) 38 | 39 | ${data.data.status.replace(/s\s{0,}/gi, '\n')} 40 | 41 | 🎶 ${data.data.music.author} - ${data.data.music.title} 42 | 43 | `.trim(); 44 | 45 | return { 46 | caption, 47 | video: data?.data?.content?.video ? data.data.content.video : undefined, 48 | audio: data?.data?.music?.url, 49 | slides: data?.data?.content?.images ? data.data.content.images : undefined, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /services/downloader/tiktok/tiklydown.test.ts: -------------------------------------------------------------------------------- 1 | import { tiktokdl } from '$services/downloader/tiktok'; 2 | import { expect, test } from 'bun:test'; 3 | test('tiklydown try using valid url', async () => { 4 | const result = await tiktokdl.tiklydown('https://vt.tiktok.com/ZSjA3BCS1/'); 5 | expect(result).toBeDefined(); 6 | console.log(result); 7 | expect(result).toHaveProperty('video'); 8 | }); 9 | 10 | test('tiklydown try using invalid url', async () => { 11 | const result = await tiktokdl.tiklydown( 12 | 'https://vt.tiktok.com/ZSjA3BsadCS1/', 13 | ); 14 | expect(result).toBeUndefined(); 15 | }); 16 | -------------------------------------------------------------------------------- /services/downloader/tiktok/tiklydown.ts: -------------------------------------------------------------------------------- 1 | import type { TiktokDlResult } from '$services/downloader/tiktok/tiktok'; 2 | import { getRandomUserAgent } from '$services/user-agent'; 3 | 4 | export type Tiklydown = { 5 | id: number; 6 | title: string; 7 | url: string; 8 | created_at: string; 9 | stats: { 10 | likeCount: number; 11 | commentCount: number; 12 | shareCount: number; 13 | playCount: string; 14 | saveCount: number; 15 | }; 16 | music: { 17 | id: number; 18 | title: string; 19 | author: string; 20 | cover_hd: any; 21 | cover_large: string; 22 | cover_medium: string; 23 | cover_thumb: string; 24 | durationFormatted: string; 25 | duration: number; 26 | play_url: string; 27 | }; 28 | author: { 29 | id: string; 30 | name: string; 31 | unique_id: string; 32 | signature: string; 33 | avatar: string; 34 | avatar_thumb: string; 35 | }; 36 | 37 | video?: { 38 | noWatermark: string; 39 | cover: string; 40 | dynamic_cover: string; 41 | origin_cover: string; 42 | width: number; 43 | height: number; 44 | durationFormatted: string; 45 | duration: number; 46 | ratio: string; 47 | }; 48 | images?: { 49 | url: string; 50 | width: number; 51 | height: number; 52 | }[]; 53 | }; 54 | 55 | export const tiklydown = async ( 56 | url: string, 57 | ): Promise => { 58 | const target = new URL('https://api.tiklydown.eu.org/api/download'); 59 | target.searchParams.append('url', url); 60 | 61 | const resp = await fetch(target, { 62 | headers: { 63 | 'user-agent': getRandomUserAgent(), 64 | accept: 'application/json', 65 | }, 66 | }); 67 | 68 | if (!resp.ok) { 69 | return undefined; 70 | } 71 | 72 | const data: Tiklydown = await resp.json(); 73 | const caption = ` 74 | ${data.author.name} (${data.author.unique_id}) 75 | 76 | ${data.title} 77 | 78 | ▶️ ${data.stats.playCount} 79 | 🤍 ${data.stats.likeCount} 80 | 💬 ${data.stats.commentCount} 81 | 🔖 ${data.stats.saveCount} 82 | 🔗 ${data.stats.shareCount} 83 | 84 | 🎶 ${data.music.title} (${data.music.author}) 85 | 86 | created at: ${data.created_at} 87 | 88 | ` 89 | .trim() 90 | .replace(/TiklyDown/gi, ''); 91 | 92 | return { 93 | caption, 94 | video: data.video ? data.video.noWatermark : undefined, 95 | audio: data.music.play_url, 96 | slides: data.images ? data.images.map((image) => image.url) : undefined, 97 | }; 98 | }; 99 | -------------------------------------------------------------------------------- /services/downloader/tiktok/tiktok.d.ts: -------------------------------------------------------------------------------- 1 | export type TiktokDlResult = 2 | | { 3 | caption: string; 4 | video?: string; 5 | slides?: string[]; 6 | audio?: string; 7 | } 8 | | undefined 9 | | null; 10 | -------------------------------------------------------------------------------- /services/downloader/tiktok/ttsave.test.ts: -------------------------------------------------------------------------------- 1 | import { ttsave } from '$services/downloader/tiktok/ttsave'; 2 | import { expect, test } from 'bun:test'; 3 | 4 | test('ttsave try using valid url', async () => { 5 | const result = await ttsave('https://vt.tiktok.com/ZS6KHACMH/'); 6 | console.log(result); 7 | expect(result).toBeDefined(); 8 | expect(result).toHaveProperty('video'); 9 | }); 10 | 11 | test('ttsave try using invalid url', async () => { 12 | const result = await ttsave('https://vt.tiktok.com/ZSjA3BsadCS1/'); 13 | expect(result).toBeUndefined(); 14 | }); 15 | -------------------------------------------------------------------------------- /services/downloader/tiktok/ttsave.ts: -------------------------------------------------------------------------------- 1 | import type { TiktokDlResult } from '$services/downloader/tiktok/tiktok'; 2 | import { getRandomUserAgent } from '$services/user-agent'; 3 | import { load } from 'cheerio'; 4 | 5 | export const ttsave = async ( 6 | url: string, 7 | ): Promise => { 8 | const body = await fetch('https://ttsave.app/download', { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | 'User-Agent': getRandomUserAgent(), 13 | origin: 'https://ttsave.app', 14 | }, 15 | body: JSON.stringify({ 16 | language_id: '1', 17 | query: url, 18 | }), 19 | }).then((res) => res.text()); 20 | 21 | const $ = load(body); 22 | const $div = $('div.flex'); 23 | const nickname = $div.find('h2').text(); 24 | const username = $div.find('a.font-extrabold').text(); 25 | const avatar = $div.find('a > img').attr('src'); 26 | const description = $div.find('p').text(); 27 | const $span = $div.find('div.flex > div.flex > span'); 28 | const played = $span.eq(0).text(); 29 | const commented = $span.eq(1).text(); 30 | const saved = $span.eq(2).text(); 31 | const shared = $span.eq(3).text(); 32 | const song = $div.find('div.flex > span').eq(4).text(); 33 | const noWatermark = $('#button-download-ready a[type="no-watermark"]').attr( 34 | 'href', 35 | ); 36 | 37 | const withWatermark = $('#button-download-ready a[type="watermark"]').attr( 38 | 'href', 39 | ); 40 | 41 | const audio = $('#button-download-ready a[type="audio"]').attr('href'); 42 | const thumbnail = $('#button-download-ready a[type="cover"]').attr('href'); 43 | 44 | const slides: string[] = []; 45 | 46 | $('#button-download-ready a[type="slide"]').each((i, el) => { 47 | slides.push($(el).attr('href') || ''); 48 | }); 49 | 50 | const result = { 51 | nickname, 52 | username, 53 | avatar, 54 | description, 55 | thumbnail, 56 | played, 57 | commented, 58 | saved, 59 | shared, 60 | song, 61 | video: { 62 | noWatermark, 63 | withWatermark, 64 | }, 65 | audio, 66 | slides, 67 | }; 68 | 69 | if (!audio) { 70 | return undefined; 71 | } 72 | 73 | const caption = ` 74 | ${result.nickname} (${result.username}) 75 | 76 | ${result.description} 77 | 78 | ▶️ ${result.played} 79 | 💬 ${result.commented} 80 | 🔖 ${result.saved} 81 | 🔗 ${result.shared} 82 | 83 | 🎶 ${result.song} 84 | `.trim(); 85 | 86 | return { 87 | caption, 88 | video: result.video ? result.video.noWatermark : undefined, 89 | audio: result.audio, 90 | slides: slides.length ? slides : undefined, 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /services/downloader/tiktok/vapis.ts: -------------------------------------------------------------------------------- 1 | import type { TiktokDlResult } from '$services/downloader/tiktok/tiktok'; 2 | 3 | type Ttdlv2 = { 4 | status: boolean; 5 | data: { 6 | type: string; 7 | uniqueId: string; 8 | nickname: string; 9 | profilePic: string; 10 | username: string; 11 | description: string; 12 | dlink: { 13 | nowm: string; 14 | wm: string; 15 | audio: string; 16 | profilePic: string; 17 | cover: string; 18 | }; 19 | stats: { 20 | plays: string; 21 | likes: string; 22 | comments: string; 23 | shares: string; 24 | }; 25 | songTitle: string; 26 | slides: Array<{ 27 | number: number; 28 | url: string; 29 | }>; 30 | videoInfo?: { 31 | nowm: string; 32 | wm: string; 33 | }; 34 | }; 35 | }; 36 | 37 | type Ttdlv = { 38 | status: boolean; 39 | data: { 40 | status: boolean; 41 | title: string; 42 | taken_at: string; 43 | region: string; 44 | id: string; 45 | durations: number; 46 | duration: string; 47 | cover: string; 48 | size_nowm: number; 49 | size_nowm_hd: number; 50 | data: Array<{ 51 | type: string; 52 | url: string; 53 | }>; 54 | music_info: { 55 | id: string; 56 | title: string; 57 | author: string; 58 | album: string; 59 | url: string; 60 | }; 61 | stats: { 62 | views: string; 63 | likes: string; 64 | comment: string; 65 | share: string; 66 | download: string; 67 | }; 68 | author: { 69 | id: string; 70 | fullname: string; 71 | nickname: string; 72 | avatar: string; 73 | }; 74 | }; 75 | }; 76 | 77 | export const ttdlv2 = async ( 78 | url: string, 79 | ): Promise => { 80 | const target = new URL('https://vapis.my.id/api/ttdlv2'); 81 | target.searchParams.append('url', url); 82 | const resp = await fetch(target); 83 | if (!resp.ok) return undefined; 84 | 85 | const data: Ttdlv2 = await resp.json(); 86 | 87 | const caption = ` 88 | ${data.data.nickname} (${data.data.username}) 89 | 90 | ${data.data.description} 91 | 92 | ▶️ ${data.data.stats.plays} 93 | 💬 ${data.data.stats.comments} 94 | 🔖 ${data.data.stats.shares} 95 | 🔗 ${data.data.stats.likes} 96 | 97 | 🎶 ${data.data.songTitle} 98 | 99 | ` 100 | .trim() 101 | .replace(/TiklyDown/gi, ''); 102 | 103 | return { 104 | caption, 105 | audio: data.data.dlink.audio, 106 | video: data.data.videoInfo ? data.data.videoInfo.nowm : undefined, 107 | slides: data.data.slides.map((slide) => slide.url), 108 | }; 109 | }; 110 | 111 | export const ttdlv = async ( 112 | url: string, 113 | ): Promise => { 114 | const target = new URL('https://vapis.my.id/api/ttdl'); 115 | target.searchParams.append('url', url); 116 | const resp = await fetch(target); 117 | if (!resp.ok) { 118 | console.log(await resp.text()); 119 | return undefined; 120 | } 121 | 122 | const { data }: Ttdlv = await resp.json(); 123 | 124 | const caption = ` 125 | ${data.author.fullname} (${data.author.nickname}) 126 | 127 | ${data.title} 128 | 129 | ▶️ ${data.stats.views} 130 | 💬 ${data.stats.comment} 131 | 🔖 ${data.stats.share} 132 | 🔗 ${data.stats.likes} 133 | 134 | 🎶 ${data.music_info.title} 135 | 136 | created at: ${data.taken_at} 137 | 138 | ` 139 | .trim() 140 | .replace(/TiklyDown/gi, ''); 141 | 142 | let video: string | undefined = undefined; 143 | let slides: string[] = []; 144 | 145 | data.data.forEach((item) => { 146 | if (item.type === 'nowatermark_hd') { 147 | video = item.url; 148 | } else if (item.type === 'nowatermark') { 149 | video = item.url; 150 | } else if (item.type === 'photo') { 151 | slides.push(item.url); 152 | } 153 | }); 154 | 155 | return { 156 | caption, 157 | audio: data.data[0].url, 158 | video, 159 | slides: slides.length ? slides : undefined, 160 | }; 161 | }; 162 | -------------------------------------------------------------------------------- /services/gemini/gemini.ts: -------------------------------------------------------------------------------- 1 | import { geminiLogger } from "$infrastructure/logger/service.logger"; 2 | import { 3 | type Content, 4 | type FunctionDeclaration, 5 | GenerativeModel, 6 | GoogleGenerativeAI, 7 | HarmBlockThreshold, 8 | HarmCategory, 9 | } from "@google/generative-ai"; 10 | 11 | export type GenerativeModelName = 12 | | "gemini-pro-vision" 13 | | "gemini-pro" 14 | | "gemini-1.5-pro" 15 | | "gemini-1.5-pro-latest" 16 | | "gemini-1.5-flash-latest" 17 | | "gemini-2.0-flash-exp" 18 | | "gemini-1.5-flash"; 19 | 20 | export class Gemini { 21 | protected systemInstruction?: string; 22 | protected modelName?: GenerativeModelName = "gemini-2.0-flash-exp"; 23 | protected model?: GenerativeModel; 24 | protected prompts: Content[] = []; 25 | protected __functionCalls: FunctionDeclaration[] = []; 26 | constructor(private readonly gemini: GoogleGenerativeAI) {} 27 | 28 | public static make(apiKey?: string): Gemini { 29 | if (!apiKey) { 30 | apiKey = process.env.GEMINI_API_KEY || ""; 31 | const apiKeys = apiKey.split(","); 32 | apiKey = apiKeys[Math.floor(Math.random() * apiKeys.length)]; 33 | } 34 | return new Gemini(new GoogleGenerativeAI(apiKey)); 35 | } 36 | 37 | public addFunctionCall(functionCall: FunctionDeclaration) { 38 | this.__functionCalls.push(functionCall); 39 | } 40 | 41 | public setModel(model: GenerativeModelName) { 42 | this.modelName = model; 43 | 44 | this.model = this.gemini.getGenerativeModel({ 45 | model: model, 46 | systemInstruction: this.systemInstruction, 47 | safetySettings: [ 48 | { 49 | category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, 50 | threshold: HarmBlockThreshold.BLOCK_NONE, 51 | }, 52 | { 53 | category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, 54 | threshold: HarmBlockThreshold.BLOCK_NONE, 55 | }, 56 | { 57 | category: HarmCategory.HARM_CATEGORY_HARASSMENT, 58 | threshold: HarmBlockThreshold.BLOCK_NONE, 59 | }, 60 | { 61 | category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, 62 | threshold: HarmBlockThreshold.BLOCK_NONE, 63 | }, 64 | ], 65 | generationConfig: { 66 | temperature: 1, 67 | topP: 0.95, 68 | topK: 64, 69 | }, 70 | }); 71 | return this; 72 | } 73 | 74 | public getModel() { 75 | return this.model; 76 | } 77 | 78 | public getPrompts() { 79 | return this.prompts; 80 | } 81 | 82 | public async setSystemInstruction(instruction: string) { 83 | this.systemInstruction = instruction; 84 | return this; 85 | } 86 | 87 | public addContent(content: Content) { 88 | this.prompts.push(content); 89 | return this; 90 | } 91 | 92 | public async generate(inJson = false) { 93 | if (!this.model) { 94 | this.setModel("gemini-2.0-flash-exp"); 95 | } 96 | geminiLogger.info(this.prompts, "Generating content"); 97 | 98 | return await this.model?.generateContent({ 99 | systemInstruction: this.systemInstruction, 100 | generationConfig: { 101 | temperature: 0.4, 102 | // topP: 0.95, 103 | // topK: 64, 104 | responseMimeType: inJson ? "application/json" : "text/plain", 105 | }, 106 | contents: this.prompts, 107 | tools: [ 108 | { 109 | functionDeclarations: this.__functionCalls, 110 | }, 111 | ], 112 | }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /services/gemini/index.ts: -------------------------------------------------------------------------------- 1 | import { Gemini } from "$services/gemini/gemini"; 2 | 3 | export const getResponseShio = async (text: string) => { 4 | const gemini = Gemini.make(Bun.env.GEMINI_KEY as string); 5 | gemini.setModel("gemini-2.0-flash-exp"); 6 | 7 | const model = gemini.getModel()!; 8 | 9 | const response = await model.generateContent({ 10 | systemInstruction: 11 | "kamu adalah orang yang paham shio, kamu akan menjelaskan shio kepadaku. berperanlah seperti manusia jangan seperti robot", 12 | generationConfig: { 13 | temperature: 1, 14 | }, 15 | contents: [ 16 | { 17 | role: "user", 18 | parts: [ 19 | { 20 | text: "kamu adalah orang yang paham shio. dibawh ini adalah hasil shio nya, beritakan kepada saya", 21 | }, 22 | { 23 | text: text, 24 | }, 25 | ], 26 | }, 27 | ], 28 | }); 29 | 30 | return response.response.text(); 31 | }; 32 | -------------------------------------------------------------------------------- /services/user-agent/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | 3 | const contents = await Bun.file(join(__dirname, "user-agents.txt")).text(); 4 | 5 | const userAgents = contents.split("\n").filter(Boolean); 6 | 7 | export const getRandomUserAgent = () => { 8 | const index = Math.floor(Math.random() * userAgents.length); 9 | return userAgents[index] + "" + Math.random() * 100; 10 | }; 11 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist' 2 | import { SessionManager } from './session-manager' 3 | 4 | export interface CLIOptions { 5 | session?: string; 6 | mode?: 'qrcode' | 'pairing'; 7 | phone?: string; 8 | interactive?: boolean; 9 | list?: boolean; 10 | help?: boolean; 11 | } 12 | 13 | export class CLI { 14 | private sessionManager = new SessionManager(); 15 | 16 | public async run(): Promise { 17 | const args = minimist(process.argv.slice(2)); 18 | 19 | // Parse arguments 20 | const options: CLIOptions = { 21 | session: args.session || args.s, 22 | mode: args.mode || args.m, 23 | phone: args.phone || args.p, 24 | interactive: args.interactive || args.i, 25 | list: args.list || args.l, 26 | help: args.help || args.h 27 | }; 28 | 29 | // Show help 30 | if (options.help) { 31 | this.showHelp(); 32 | return null; 33 | } 34 | 35 | // List sessions only 36 | if (options.list) { 37 | await this.sessionManager.displaySessions(); 38 | return null; 39 | } 40 | 41 | // Interactive mode atau jika tidak ada session argument 42 | if (options.interactive || !options.session) { 43 | console.log('🚀 Personal Assistant - Interactive Setup'); 44 | console.log('=' .repeat(50)); 45 | 46 | while (true) { 47 | // Tampilkan daftar session terlebih dahulu 48 | await this.sessionManager.displaySessions(); 49 | 50 | const action = await this.showMainMenu(); 51 | if (!action) { 52 | return null; 53 | } 54 | 55 | if (action === 'create') { 56 | const newSession = await this.sessionManager.createSessionPrompt(); 57 | if (!newSession) { 58 | console.log('❌ Gagal membuat session baru.'); 59 | continue; 60 | } 61 | options.session = newSession; 62 | // Prompt untuk connection details untuk session baru 63 | const connectionDetails = await this.sessionManager.promptConnectionDetails(); 64 | options.mode = connectionDetails.mode; 65 | options.phone = connectionDetails.phoneNumber; 66 | break; 67 | } else if (action === 'delete') { 68 | await this.sessionManager.deleteSessionPrompt(); 69 | // Setelah hapus, kembali ke menu utama 70 | continue; 71 | } else if (action === 'select') { 72 | const selectedSession = await this.sessionManager.selectSession(); 73 | if (!selectedSession) { 74 | console.log('❌ Tidak ada session yang dipilih. Keluar...'); 75 | return null; 76 | } 77 | 78 | options.session = selectedSession; 79 | // Jika session belum valid, prompt untuk connection details 80 | const isValid = await this.sessionManager.validateSession(selectedSession); 81 | if (!isValid) { 82 | console.log(`\n⚠️ Session '${selectedSession}' belum memiliki kredensial.`); 83 | console.log(' Perlu setup koneksi WhatsApp terlebih dahulu.'); 84 | const connectionDetails = await this.sessionManager.promptConnectionDetails(); 85 | options.mode = connectionDetails.mode; 86 | options.phone = connectionDetails.phoneNumber; 87 | } else { 88 | console.log(`\n✅ Session '${selectedSession}' sudah siap digunakan.`); 89 | // Untuk session yang sudah valid, gunakan mode default 90 | options.mode = 'qrcode'; 91 | } 92 | break; 93 | } 94 | } 95 | } 96 | 97 | // Validasi arguments 98 | if (!options.session) { 99 | console.error('❌ Session wajib dipilih!'); 100 | this.showHelp(); 101 | return null; 102 | } 103 | 104 | if (!options.mode) { 105 | console.error('❌ Mode koneksi wajib dipilih!'); 106 | this.showHelp(); 107 | return null; 108 | } 109 | 110 | if (options.mode === 'pairing' && !options.phone) { 111 | console.error('❌ Nomor telepon wajib diisi untuk mode pairing!'); 112 | return null; 113 | } 114 | 115 | return options; 116 | } 117 | 118 | private async showMainMenu(): Promise<'select' | 'create' | 'delete' | null> { 119 | const readline = require('readline'); 120 | const rl = readline.createInterface({ 121 | input: process.stdin, 122 | output: process.stdout, 123 | }); 124 | 125 | console.log('\n📋 Menu Utama:'); 126 | console.log('1. 🔍 Pilih Session yang Ada'); 127 | console.log('2. 🆕 Buat Session Baru'); 128 | console.log('3. 🗑️ Hapus Session'); 129 | console.log('4. ❌ Keluar'); 130 | 131 | return new Promise((resolve) => { 132 | const askChoice = () => { 133 | rl.question('\n🎯 Pilih menu (1-4): ', (answer: string) => { 134 | const choice = answer.trim(); 135 | 136 | switch (choice) { 137 | case '1': 138 | rl.close(); 139 | resolve('select'); 140 | break; 141 | case '2': 142 | rl.close(); 143 | resolve('create'); 144 | break; 145 | case '3': 146 | rl.close(); 147 | resolve('delete'); 148 | break; 149 | case '4': 150 | rl.close(); 151 | resolve(null); 152 | break; 153 | default: 154 | console.log('❌ Pilihan tidak valid. Pilih 1-4.'); 155 | askChoice(); 156 | break; 157 | } 158 | }); 159 | }; 160 | 161 | askChoice(); 162 | }); 163 | } 164 | 165 | private showHelp(): void { 166 | console.log(` 167 | 🚀 Personal Assistant - WhatsApp Bot 168 | 169 | Usage: 170 | bun run start [options] 171 | 172 | Options: 173 | -s, --session Session name (required) 174 | -m, --mode Connection mode: qrcode | pairing 175 | -p, --phone Phone number for pairing mode 176 | -i, --interactive Interactive mode (default if no session) 177 | -l, --list List all available sessions 178 | -h, --help Show this help message 179 | 180 | Examples: 181 | bun run start --interactive # Interactive mode 182 | bun run start --list # List sessions 183 | bun run start -s mybot -m qrcode # Direct mode with QR code 184 | bun run start -s mybot -m pairing -p +6281234567890 # Pairing mode 185 | 186 | Interactive Mode: 187 | - Shows main menu with options to: 188 | * Select existing session 189 | * Create new session 190 | * Delete session 191 | * Exit 192 | - Prompts for connection details if needed 193 | - Perfect for first-time setup 194 | 195 | PM2 Usage: 196 | pm2 start "bun run start -s mybot -m qrcode" --name "whatsapp-bot" 197 | `); 198 | } 199 | } 200 | 201 | export async function runCLI(): Promise { 202 | const cli = new CLI(); 203 | return await cli.run(); 204 | } -------------------------------------------------------------------------------- /src/cli/session-manager.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'fs' 2 | import { parsePhoneNumber } from 'libphonenumber-js' 3 | import { join } from 'path' 4 | 5 | export interface SessionDetail { 6 | id: string; 7 | name: string; 8 | path: string; 9 | lastModified: Date; 10 | hasAuthStore: boolean; 11 | hasCredentials: boolean; 12 | isValid: boolean; 13 | size: number; 14 | status: 'active' | 'inactive' | 'corrupted'; 15 | } 16 | 17 | export class SessionManager { 18 | private readonly sessionBaseDir = '.hiddens'; 19 | 20 | /** 21 | * Mendapatkan semua session yang tersedia 22 | */ 23 | public async getAllSessions(): Promise { 24 | try { 25 | if (!existsSync(this.sessionBaseDir)) { 26 | return []; 27 | } 28 | 29 | const sessions = readdirSync(this.sessionBaseDir) 30 | .filter(name => { 31 | const sessionPath = join(this.sessionBaseDir, name); 32 | return statSync(sessionPath).isDirectory() && !name.startsWith('.'); 33 | }) 34 | .map(sessionId => this.getSessionDetail(sessionId)) 35 | .filter(Boolean) as SessionDetail[]; 36 | 37 | return sessions.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()); 38 | } catch (error) { 39 | console.error('Error reading sessions:', error); 40 | return []; 41 | } 42 | } 43 | 44 | /** 45 | * Mendapatkan detail session berdasarkan ID 46 | */ 47 | public getSessionDetail(sessionId: string): SessionDetail | null { 48 | try { 49 | const sessionPath = join(this.sessionBaseDir, sessionId); 50 | const authStorePath = join(sessionPath, 'auth-store'); 51 | 52 | if (!existsSync(sessionPath)) { 53 | return null; 54 | } 55 | 56 | const stats = statSync(sessionPath); 57 | const hasAuthStore = existsSync(authStorePath); 58 | const hasCredentials = hasAuthStore && existsSync(join(authStorePath, 'creds.json')); 59 | 60 | // Hitung ukuran folder 61 | const size = this.calculateFolderSize(sessionPath); 62 | 63 | // Tentukan status 64 | let status: 'active' | 'inactive' | 'corrupted' = 'inactive'; 65 | if (hasCredentials) { 66 | status = 'active'; 67 | } else if (hasAuthStore) { 68 | status = 'corrupted'; 69 | } 70 | 71 | return { 72 | id: sessionId, 73 | name: sessionId, 74 | path: sessionPath, 75 | lastModified: stats.mtime, 76 | hasAuthStore, 77 | hasCredentials, 78 | isValid: hasCredentials, 79 | size, 80 | status 81 | }; 82 | } catch (error) { 83 | console.error(`Error getting session detail for ${sessionId}:`, error); 84 | return null; 85 | } 86 | } 87 | 88 | /** 89 | * Menampilkan daftar session dengan detail 90 | */ 91 | public async displaySessions(): Promise { 92 | const sessions = await this.getAllSessions(); 93 | 94 | if (sessions.length === 0) { 95 | console.log('📭 Tidak ada session yang ditemukan.'); 96 | console.log(' Buat session baru dengan menjalankan aplikasi menggunakan --session '); 97 | return; 98 | } 99 | 100 | console.log('📱 Daftar Session WhatsApp:'); 101 | console.log('=' .repeat(80)); 102 | 103 | sessions.forEach((session, index) => { 104 | const statusIcon = this.getStatusIcon(session.status); 105 | const sizeStr = this.formatSize(session.size); 106 | const dateStr = session.lastModified.toLocaleString('id-ID'); 107 | 108 | console.log(`${index + 1}. ${statusIcon} ${session.name}`); 109 | console.log(` 📁 Path: ${session.path}`); 110 | console.log(` 📅 Last Modified: ${dateStr}`); 111 | console.log(` 💾 Size: ${sizeStr}`); 112 | console.log(` 🔐 Auth Store: ${session.hasAuthStore ? '✅' : '❌'}`); 113 | console.log(` 🔑 Credentials: ${session.hasCredentials ? '✅' : '❌'}`); 114 | console.log(` 📊 Status: ${this.getStatusText(session.status)}`); 115 | console.log(' ' + '-'.repeat(70)); 116 | }); 117 | } 118 | 119 | /** 120 | * Interactive session selection 121 | */ 122 | public async selectSession(): Promise { 123 | const sessions = await this.getAllSessions(); 124 | 125 | if (sessions.length === 0) { 126 | return null; 127 | } 128 | 129 | // Tidak perlu display sessions lagi karena sudah ditampilkan di awal 130 | // await this.displaySessions(); 131 | 132 | const readline = require('readline'); 133 | const rl = readline.createInterface({ 134 | input: process.stdin, 135 | output: process.stdout, 136 | }); 137 | 138 | return new Promise((resolve) => { 139 | const askSelection = () => { 140 | rl.question( 141 | `\n🔍 Pilih session (1-${sessions.length}) atau 'q' untuk quit: `, 142 | (answer: string) => { 143 | const trimmed = answer.trim().toLowerCase(); 144 | 145 | if (trimmed === 'q' || trimmed === 'quit') { 146 | rl.close(); 147 | resolve(null); 148 | return; 149 | } 150 | 151 | const index = parseInt(trimmed) - 1; 152 | if (index >= 0 && index < sessions.length) { 153 | const selectedSession = sessions[index]; 154 | console.log(`\n✅ Session dipilih: ${selectedSession.name}`); 155 | rl.close(); 156 | resolve(selectedSession.id); 157 | } else { 158 | console.log('❌ Pilihan tidak valid. Coba lagi.'); 159 | askSelection(); 160 | } 161 | } 162 | ); 163 | }; 164 | 165 | askSelection(); 166 | }); 167 | } 168 | 169 | /** 170 | * Validasi session 171 | */ 172 | public async validateSession(sessionId: string): Promise { 173 | const session = this.getSessionDetail(sessionId); 174 | return session?.isValid ?? false; 175 | } 176 | 177 | /** 178 | * Prompt untuk mode dan phone number 179 | */ 180 | public async promptConnectionDetails(): Promise<{ 181 | mode: 'qrcode' | 'pairing'; 182 | phoneNumber?: string; 183 | }> { 184 | const readline = require('readline'); 185 | const rl = readline.createInterface({ 186 | input: process.stdin, 187 | output: process.stdout, 188 | }); 189 | 190 | return new Promise((resolve) => { 191 | const askMode = () => { 192 | rl.question( 193 | '\n🔗 Pilih mode koneksi:\n1. QR Code\n2. Pairing Code\nPilih (1/2): ', 194 | (answer: string) => { 195 | const choice = answer.trim(); 196 | 197 | if (choice === '1') { 198 | rl.close(); 199 | resolve({ mode: 'qrcode' }); 200 | } else if (choice === '2') { 201 | askPhoneNumber(); 202 | } else { 203 | console.log('❌ Pilihan tidak valid. Pilih 1 atau 2.'); 204 | askMode(); 205 | } 206 | } 207 | ); 208 | }; 209 | 210 | const askPhoneNumber = () => { 211 | rl.question( 212 | '\n📞 Masukkan nomor telepon (contoh: +6281234567890): ', 213 | (answer: string) => { 214 | const phone = answer.trim(); 215 | 216 | try { 217 | const parsed = parsePhoneNumber(phone); 218 | if (parsed.isValid()) { 219 | rl.close(); 220 | resolve({ mode: 'pairing', phoneNumber: parsed.number }); 221 | } else { 222 | console.log('❌ Nomor tidak valid. Coba lagi.'); 223 | askPhoneNumber(); 224 | } 225 | } catch (e) { 226 | console.log('❌ Format nomor tidak valid. Coba lagi.'); 227 | askPhoneNumber(); 228 | } 229 | } 230 | ); 231 | }; 232 | 233 | askMode(); 234 | }); 235 | } 236 | 237 | /** 238 | * Prompt untuk membuat session baru 239 | */ 240 | public async createSessionPrompt(): Promise { 241 | const readline = require('readline'); 242 | const rl = readline.createInterface({ 243 | input: process.stdin, 244 | output: process.stdout, 245 | }); 246 | 247 | return new Promise((resolve) => { 248 | rl.question('\n🆕 Masukkan nama session baru: ', async (answer: string) => { 249 | const sessionId = answer.trim(); 250 | if (!sessionId) { 251 | console.log('❌ Nama session tidak boleh kosong.'); 252 | rl.close(); 253 | resolve(null); 254 | return; 255 | } 256 | const sessionPath = join(this.sessionBaseDir, sessionId); 257 | if (existsSync(sessionPath)) { 258 | console.log('❌ Session sudah ada. Pilih nama lain.'); 259 | rl.close(); 260 | resolve(null); 261 | return; 262 | } 263 | try { 264 | mkdirSync(sessionPath, { recursive: true }); 265 | console.log(`✅ Session '${sessionId}' berhasil dibuat.`); 266 | rl.close(); 267 | resolve(sessionId); 268 | } catch (e) { 269 | console.log('❌ Gagal membuat session:', (e as Error).message); 270 | rl.close(); 271 | resolve(null); 272 | } 273 | }); 274 | }); 275 | } 276 | 277 | /** 278 | * Prompt untuk menghapus session 279 | */ 280 | public async deleteSessionPrompt(): Promise { 281 | const sessions = await this.getAllSessions(); 282 | if (sessions.length === 0) { 283 | console.log('📭 Tidak ada session yang bisa dihapus.'); 284 | return null; 285 | } 286 | // Tidak perlu displaySessions lagi karena sudah ditampilkan di awal 287 | // await this.displaySessions(); 288 | const readline = require('readline'); 289 | const rl = readline.createInterface({ 290 | input: process.stdin, 291 | output: process.stdout, 292 | }); 293 | return new Promise((resolve) => { 294 | rl.question(`\n🗑️ Pilih session yang ingin dihapus (1-${sessions.length}) atau 'q' untuk batal: `, (answer: string) => { 295 | const trimmed = answer.trim().toLowerCase(); 296 | if (trimmed === 'q' || trimmed === 'quit') { 297 | rl.close(); 298 | resolve(null); 299 | return; 300 | } 301 | const index = parseInt(trimmed) - 1; 302 | if (index >= 0 && index < sessions.length) { 303 | const selectedSession = sessions[index]; 304 | try { 305 | rmSync(selectedSession.path, { recursive: true, force: true }); 306 | console.log(`✅ Session '${selectedSession.name}' berhasil dihapus.`); 307 | rl.close(); 308 | resolve(selectedSession.id); 309 | } catch (e) { 310 | console.log('❌ Gagal menghapus session:', (e as Error).message); 311 | rl.close(); 312 | resolve(null); 313 | } 314 | } else { 315 | console.log('❌ Pilihan tidak valid.'); 316 | rl.close(); 317 | resolve(null); 318 | } 319 | }); 320 | }); 321 | } 322 | 323 | private calculateFolderSize(folderPath: string): number { 324 | try { 325 | let totalSize = 0; 326 | const files = readdirSync(folderPath); 327 | 328 | for (const file of files) { 329 | const filePath = join(folderPath, file); 330 | const stats = statSync(filePath); 331 | 332 | if (stats.isDirectory()) { 333 | totalSize += this.calculateFolderSize(filePath); 334 | } else { 335 | totalSize += stats.size; 336 | } 337 | } 338 | 339 | return totalSize; 340 | } catch (error) { 341 | return 0; 342 | } 343 | } 344 | 345 | private formatSize(bytes: number): string { 346 | const sizes = ['B', 'KB', 'MB', 'GB']; 347 | if (bytes === 0) return '0 B'; 348 | 349 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 350 | return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; 351 | } 352 | 353 | private getStatusIcon(status: 'active' | 'inactive' | 'corrupted'): string { 354 | switch (status) { 355 | case 'active': return '🟢'; 356 | case 'inactive': return '🔴'; 357 | case 'corrupted': return '🟡'; 358 | default: return '⚪'; 359 | } 360 | } 361 | 362 | private getStatusText(status: 'active' | 'inactive' | 'corrupted'): string { 363 | switch (status) { 364 | case 'active': return '🟢 Active (Ready to use)'; 365 | case 'inactive': return '🔴 Inactive (No credentials)'; 366 | case 'corrupted': return '🟡 Corrupted (Incomplete auth)'; 367 | default: return '⚪ Unknown'; 368 | } 369 | } 370 | } -------------------------------------------------------------------------------- /src/infrastructure/config/bootstrap.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | handlers: ["src/modules/**/*.handler.ts"], 3 | }; 4 | -------------------------------------------------------------------------------- /src/infrastructure/config/consts.config.ts: -------------------------------------------------------------------------------- 1 | export const ReadMoreUnicode = '͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏͏'; 2 | 3 | export const SHELL_COMMAND = '$'; 4 | export const PREFIX_COMMAND = Bun.env.COMMAND_SIGN || '.'; 5 | -------------------------------------------------------------------------------- /src/infrastructure/logger/console.logger.ts: -------------------------------------------------------------------------------- 1 | import P from "pino"; 2 | 3 | const logger = P({ 4 | timestamp: () => `,"time":"${new Date().toJSON()}"`, 5 | 6 | transport: { 7 | target: "pino-pretty", 8 | options: { 9 | colorize: true, 10 | }, 11 | }, 12 | }); 13 | logger.level = "trace"; 14 | 15 | export { logger }; 16 | -------------------------------------------------------------------------------- /src/infrastructure/logger/service.logger.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "$infrastructure/logger/console.logger"; 2 | 3 | export const serviceLogger = logger.child({ module: "service" }); 4 | 5 | export const geminiLogger = serviceLogger.child({ module: "gemini" }); 6 | -------------------------------------------------------------------------------- /src/infrastructure/supports/boolean.support.ts: -------------------------------------------------------------------------------- 1 | export const isShellOn = () => process.env.CMD?.toLowerCase() === 'true'; 2 | -------------------------------------------------------------------------------- /src/infrastructure/supports/file.support.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync } from 'fs'; 2 | import path from 'path'; 3 | 4 | export const base_path = (...filepaths: string[]) => { 5 | const baseDir = path.resolve(import.meta.dir, '../../../'); 6 | return path.join(baseDir, ...filepaths); 7 | }; 8 | 9 | export const hidden_path = (...filepaths: string[]) => { 10 | const dirPath = path.dirname(base_path('.hiddens', ...filepaths)); 11 | if (!existsSync(dirPath)) { 12 | mkdirSync(dirPath, { recursive: true }); 13 | } 14 | return base_path('.hiddens', ...filepaths); 15 | }; 16 | -------------------------------------------------------------------------------- /src/infrastructure/supports/promise.support.ts: -------------------------------------------------------------------------------- 1 | type Nullable = T | null | undefined; 2 | 3 | /** 4 | * Mengembalikan promise pertama yang memiliki data valid (bukan null/undefined). 5 | * @param promises Array of promises. 6 | * @returns Promise 7 | */ 8 | export async function firstValidData( 9 | promises: Promise>[] 10 | ): Promise { 11 | return new Promise((resolve) => { 12 | let pending = promises.length; 13 | let found = false; 14 | 15 | promises.forEach((promise) => { 16 | promise 17 | .then((result) => { 18 | if (!found && result !== undefined && result !== null) { 19 | found = true; 20 | resolve(result); // Return data pertama yang valid 21 | } else { 22 | pending--; 23 | if (pending === 0 && !found) { 24 | resolve(undefined); // Semua promise tidak ada data 25 | } 26 | } 27 | }) 28 | .catch(() => { 29 | pending--; 30 | if (pending === 0 && !found) { 31 | resolve(undefined); // Semua promise gagal 32 | } 33 | }); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/infrastructure/supports/regex.ts: -------------------------------------------------------------------------------- 1 | export function escapeRegex(str: string) { 2 | return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 3 | } 4 | 5 | export function isExactMatchPattern(pattern: RegExp) { 6 | return ( 7 | pattern.source.startsWith("^") && 8 | pattern.source.endsWith("$") && 9 | pattern.flags.includes("i") 10 | ); 11 | } 12 | 13 | export function normalizePattern(pattern: string | RegExp): RegExp { 14 | if (typeof pattern === "string") { 15 | return new RegExp(`^${escapeRegex(pattern)}$`, "i"); 16 | } 17 | return pattern; 18 | } 19 | 20 | export function getExactMatchKey(pattern: RegExp) { 21 | return pattern.source.replace(/^\^|\$|\/i$/g, "").toLowerCase(); 22 | } 23 | -------------------------------------------------------------------------------- /src/infrastructure/supports/string.support.ts: -------------------------------------------------------------------------------- 1 | export function replaceRandomSpacesToUnicode(input: string): string { 2 | let probability = Math.random() * (0.7 - 0.3) + 0.3; 3 | let unicode = `‎`; 4 | return input 5 | .split("") 6 | .map((char) => { 7 | if (char === " " && Math.random() < probability) { 8 | return unicode; 9 | } 10 | return char; 11 | }) 12 | .join(""); 13 | } 14 | -------------------------------------------------------------------------------- /src/infrastructure/supports/whatsapp.support.ts: -------------------------------------------------------------------------------- 1 | import { 2 | downloadContentFromMessage, 3 | getContentType, 4 | type DownloadableMessage, 5 | type MediaDownloadOptions, 6 | type MediaType, 7 | type MessageType, 8 | type proto, 9 | } from "@whiskeysockets/baileys"; 10 | 11 | export const getMessageCaption = (message: proto.IMessage) => { 12 | if (!message) return ""; 13 | 14 | const type = getContentType(message)!; 15 | const msg = 16 | type == "viewOnceMessage" 17 | ? message[type]!.message![getContentType(message[type]!.message!)!] 18 | : message[type]; 19 | 20 | return ( 21 | message?.conversation || 22 | (msg as proto.Message.IVideoMessage)?.caption || 23 | (msg as proto.Message.IExtendedTextMessage)?.text || 24 | message.ephemeralMessage?.message?.extendedTextMessage?.text || 25 | message.extendedTextMessage?.text || 26 | (type == "viewOnceMessage" && 27 | (msg as proto.Message.IVideoMessage)?.caption) || 28 | "" 29 | ); 30 | }; 31 | 32 | export const getMessageQutoedCaption = (message: proto.IMessage) => { 33 | const type = getContentType(message)!; 34 | const msg = 35 | type == "viewOnceMessage" 36 | ? message[type]!.message![getContentType(message[type]!.message!)!] 37 | : message[type]; 38 | 39 | return ( 40 | message?.ephemeralMessage?.message?.extendedTextMessage?.contextInfo 41 | ?.quotedMessage?.conversation || 42 | message?.extendedTextMessage?.contextInfo?.quotedMessage?.conversation || 43 | message?.extendedTextMessage?.contextInfo?.quotedMessage?.imageMessage 44 | ?.caption || 45 | message?.extendedTextMessage?.contextInfo?.quotedMessage 46 | ?.extendedTextMessage?.text || 47 | (msg as proto.Message.IVideoMessage)?.contextInfo?.quotedMessage 48 | ?.conversation || 49 | (msg as proto.IMessage)?.imageMessage?.caption || 50 | "" 51 | ); 52 | }; 53 | 54 | export const whatsappFormat = (text: string) => { 55 | // replace **text** to *text* 56 | text = text.replace(/\*\*(.*?)\*\*/g, "*$1*"); 57 | // replace __text__ to _text_ 58 | text = text.replace(/__(.*?)__/g, "_$1_"); 59 | // replace [text](url) to *text* (url) 60 | text = text.replace(/\[(.*?)\]\((.*?)\)/g, "*$1* ($2)"); 61 | // replace [text] to *text* 62 | text = text.replace(/\[(.*?)\]/g, "*$1*"); 63 | // remove all headings (#) 64 | text = text.replace(/^#+/gm, ""); 65 | return text; 66 | }; 67 | 68 | export const downloadContentBufferFromMessage = async ( 69 | { mediaKey, directPath, url }: DownloadableMessage, 70 | type: MediaType, 71 | opts?: MediaDownloadOptions 72 | ): Promise => { 73 | const stream = await downloadContentFromMessage( 74 | { mediaKey, directPath, url }, 75 | type, 76 | opts 77 | ); 78 | const bufferArray: Buffer[] = []; 79 | for await (const chunk of stream) { 80 | bufferArray.push(chunk); 81 | } 82 | 83 | return Buffer.concat(bufferArray); 84 | }; 85 | 86 | export const downloadQuotedMessageMedia = async ( 87 | message: proto.IMessage 88 | ): Promise => { 89 | const type = Object.keys(message)[0] as MessageType; 90 | const msg = message[type as keyof typeof message]; 91 | 92 | const stream = await downloadContentFromMessage( 93 | msg as DownloadableMessage, 94 | type.replace("Message", "") as MediaType 95 | ); 96 | let buffer = Buffer.from([]); 97 | for await (const chunk of stream) { 98 | buffer = Buffer.concat([buffer, chunk]); 99 | } 100 | 101 | return buffer; 102 | }; 103 | -------------------------------------------------------------------------------- /src/infrastructure/whatsapp/make-in-memory-store.ts: -------------------------------------------------------------------------------- 1 | import makeOrderedDictionary from '$infrastructure/whatsapp/make-ordered-dictionary' 2 | import type KeyedDB from '@adiwajshing/keyed-db' 3 | import type { Comparable } from '@adiwajshing/keyed-db/lib/Types' 4 | import type makeWASocket from '@whiskeysockets/baileys' 5 | import { 6 | DEFAULT_CONNECTION_CONFIG, 7 | jidDecode, 8 | jidNormalizedUser, 9 | md5, 10 | proto, 11 | toNumber, 12 | updateMessageWithReaction, 13 | updateMessageWithReceipt, 14 | type BaileysEventEmitter, 15 | type Chat, 16 | type ConnectionState, 17 | type Contact, 18 | type GroupMetadata, 19 | type PresenceData, 20 | type WAMessage, 21 | type WAMessageCursor, 22 | type WAMessageKey, 23 | } from '@whiskeysockets/baileys' 24 | import { ObjectRepository } from '@whiskeysockets/baileys/lib/Store/object-repository' 25 | import type { Label } from '@whiskeysockets/baileys/lib/Types/Label' 26 | import { 27 | LabelAssociationType, 28 | type LabelAssociation, 29 | type MessageLabelAssociation, 30 | } from '@whiskeysockets/baileys/lib/Types/LabelAssociation' 31 | import type { Logger } from 'pino' 32 | 33 | type WASocket = ReturnType; 34 | 35 | export const waChatKey = (pin: boolean) => ({ 36 | key: (c: Chat) => 37 | (pin ? (c.pinned ? '1' : '0') : '') + 38 | (c.archived ? '0' : '1') + 39 | (c.conversationTimestamp 40 | ? c.conversationTimestamp.toString(16).padStart(8, '0') 41 | : '') + 42 | c.id, 43 | compare: (k1: string, k2: string) => k2.localeCompare(k1), 44 | }); 45 | 46 | export const waMessageID = (m: WAMessage) => m.key.id || ''; 47 | 48 | export const waLabelAssociationKey: Comparable = { 49 | key: (la: LabelAssociation) => 50 | la.type === LabelAssociationType.Chat 51 | ? la.chatId + la.labelId 52 | : la.chatId + la.messageId + la.labelId, 53 | compare: (k1: string, k2: string) => k2.localeCompare(k1), 54 | }; 55 | 56 | export type BaileysInMemoryStoreConfig = { 57 | chatKey?: Comparable; 58 | labelAssociationKey?: Comparable; 59 | logger?: Logger; 60 | socket?: WASocket; 61 | }; 62 | 63 | const makeMessagesDictionary = () => makeOrderedDictionary(waMessageID); 64 | 65 | export default (config: BaileysInMemoryStoreConfig) => { 66 | const socket = config.socket; 67 | const chatKey = config.chatKey || waChatKey(true); 68 | const labelAssociationKey = 69 | config.labelAssociationKey || waLabelAssociationKey; 70 | const logger: Logger = 71 | config.logger || 72 | DEFAULT_CONNECTION_CONFIG.logger.child({ stream: 'in-mem-store' }); 73 | const KeyedDB = require('@adiwajshing/keyed-db').default; 74 | 75 | const chats = new KeyedDB(chatKey, (c: any) => c.id) as KeyedDB; 76 | const messages: { [_: string]: ReturnType } = 77 | {}; 78 | const deletedMessages: { 79 | [_: string]: { 80 | [_: string]: proto.IWebMessageInfo; 81 | }; 82 | } = {}; 83 | const editedMessages: { 84 | [_: string]: { 85 | [_: string]: proto.IWebMessageInfo; 86 | }; 87 | } = {}; 88 | const contacts: { [_: string]: Contact } = {}; 89 | const groupMetadata: { [_: string]: GroupMetadata } = {}; 90 | const presences: { [id: string]: { [participant: string]: PresenceData } } = 91 | {}; 92 | const state: ConnectionState = { connection: 'close' }; 93 | const labels = new ObjectRepository