# Akinator Plugin dan Python Bridge Dokumen ini berisi dua file untuk menjalankan game Akinator di bot WhatsApp: - `akinator.js` — menangani perintah, sesi pemain, jawaban, tombol, dan hasil tebakan. - `akipy_bridge.py` — menghubungkan plugin dengan library Akinator melalui Python. ## Instalasi 1. Simpan bagian **Plugin** sebagai `cmd/plugins/game/akinator.js`. 2. Simpan bagian **Bridge** sebagai `system/lib/akipy_bridge.py`. 3. Install Python 3 dan library yang dibutuhkan: ```bash python3 -m pip install akipy ``` 4. Pastikan perintah berikut bisa dijalankan: ```bash python3 --version ``` 5. Jika bridge disimpan di lokasi berbeda, atur path berikut sebelum menjalankan bot: ```bash export AKINATOR_BRIDGE_PATH="/path/ke/akipy_bridge.py" ``` ## Perintah - `.aki` atau `.akinator` — mulai permainan. - `.aki 0` — Ya. - `.aki 1` — Tidak. - `.aki 2` — Tidak tahu. - `.aki 3` — Mungkin. - `.aki 4` — Mungkin tidak. - `.aki back` — kembali ke pertanyaan sebelumnya. - `.aki stop` — hentikan sesi. ## Catatan penggunaan - Satu pemain hanya memiliki satu sesi aktif berdasarkan JID WhatsApp. - Sesi yang masih berjalan akan hilang ketika proses bot dimulai ulang. - Tombol jawaban hanya diproses untuk pemain yang memulai sesi. - Jika tombol WhatsApp gagal dikirim, plugin menampilkan pilihan dalam bentuk teks. - Foto karakter memakai foto dari Akinator jika tersedia; pencarian gambar hanya digunakan sebagai cadangan. - Perintah membutuhkan akses internet saat memulai sesi, menjawab, atau kembali ke pertanyaan sebelumnya. --- ## Plugin: `akinator.js` ```js import { execFile } from 'node:child_process' import { promisify } from 'node:util' import path from 'node:path' import { getVerifiedQuoted } from '#system/function.js' import { Button } from '#lib/rich_elements.js' import { googleImageSearch } from '#scraper/google_image.js' /** * Mithra EX Akinator game plugin. * * @requires Python 3 and the `akipy` package. * @requires system/lib/akipy_bridge.py. */ const exec = promisify(execFile) const BRIDGE_PATH = process.env.AKINATOR_BRIDGE_PATH || path.join(process.cwd(), 'system', 'lib', 'akipy_bridge.py') const sessions = new Map() const ANSWERS = [ { key: '0', label: 'Ya', aliases: ['0', 'ya', 'yes', 'y'] }, { key: '1', label: 'Tidak', aliases: ['1', 'tidak', 'no', 'ga', 'gk', 't'] }, { key: '2', label: 'Tidak Tahu', aliases: ['2', 'tidak tahu', 'gatau', 'dont know', 'idk', 'ragu'] }, { key: '3', label: 'Mungkin', aliases: ['3', 'mungkin', 'probably', 'bisa jadi'] }, { key: '4', label: 'Mungkin Tidak', aliases: ['4', 'mungkin tidak', 'probably not'] } ] /** * Executes one operation against the Python bridge. * @param {string} action Bridge operation name. * @param {...string} args Serialized bridge arguments. * @returns {Promise} Current Akinator state. */ async function bridge(action, ...args) { const { stdout } = await exec('python3', [BRIDGE_PATH, action, ...args], { timeout: 25000, maxBuffer: 1024 * 1024 }) const result = JSON.parse(stdout.trim()) if (result?.error) throw new Error(result.error) return result } const startSession = (lang = 'id') => bridge('start', lang) const answerSession = (state, answer) => bridge('answer', JSON.stringify(state), String(answer)) const backSession = (state) => bridge('back', JSON.stringify(state)) /** * Resolves the character image returned by Akinator or an image-search fallback. * @param {string} name Character name. * @param {string|null} fallback Image URL returned by Akinator. * @returns {Promise} Resolved image URL. */ async function resolveCharacterImage(name, fallback) { if (typeof fallback === 'string' && /^https?:\/\//i.test(fallback)) return fallback try { const images = await googleImageSearch(name) return Array.isArray(images) ? images[0] || null : null } catch (error) { console.warn('[Akinator] image lookup failed:', error?.message || error) return null } } /** * Sends the current question with quick replies and a text fallback. * @param {object} client WhatsApp socket. * @param {object} m Message wrapper. * @param {object} state Akinator state. * @param {string} prefix Command prefix. * @param {string} command Command name. * @returns {Promise} Sent message result. */ async function sendQuestion(client, m, state, prefix, command) { const number = Number.parseInt(state.step || 0, 10) + 1 const text = [ `╭─〔 🎯 *AKINATOR* · Pertanyaan #${number} 〕─⬣`, '', `│ *${state.question || 'Pertanyaan'}*`, '', `│ Keyakinan: *${Math.round(state.progression || 0)}%*`, '╰─────────────────────────⬣' ].join('\n') try { const button = new Button(client) .setTitle('AKINATOR') .setBody(text) .addReply('Ya', `${prefix + command} 0 ${m.sender}`) .addReply('Tidak', `${prefix + command} 1 ${m.sender}`) .addReply('Tidak Tahu', `${prefix + command} 2 ${m.sender}`) .addReply('Mungkin', `${prefix + command} 3 ${m.sender}`) .addReply('Kembali', `${prefix + command} back ${m.sender}`) return button.send(m.chat, { quoted: getVerifiedQuoted(m.pushName) || m }) } catch (error) { console.error('[Akinator] button fallback:', error?.message || error) return client.sendMessage(m.chat, { text: `${text}\n\n• 0: Ya\n• 1: Tidak\n• 2: Tidak Tahu\n• 3: Mungkin\n• 4: Mungkin Tidak\n\n_Ketik \`${prefix + command} <0-4>\`_` }, { quoted: getVerifiedQuoted(m.pushName) || m }) } } /** * Sends the final guess with image and action buttons. * @param {object} client WhatsApp socket. * @param {object} m Message wrapper. * @param {object} state Akinator state. * @param {string} prefix Command prefix. * @param {string} command Command name. * @returns {Promise} Sent message result. */ async function sendGuess(client, m, state, prefix, command) { const name = state.name_proposition || 'Karakter Anda' const caption = [ '╭─〔 🎯 *HASIL TEBAKAN AKINATOR* 〕─⬣', '', `│ • *Karakter* : ${name}`, `│ • *Deskripsi* : ${state.description_proposition || '-'}`, `│ • *Keyakinan* : ${Math.round(state.progression || 100)}%`, `│ • *Langkah* : ${state.step || 0} Pertanyaan`, '', '╰─────────────────────────⬣' ].join('\n') const image = await resolveCharacterImage(name, state.photo) try { const button = new Button(client).setTitle('AKINATOR RESULT').setBody(caption) if (image) button.setImage(image) button.addReply('Main Lagi', `${prefix + command}`) button.addReply('Selesai', `${prefix + command} stop ${m.sender}`) return button.send(m.chat, { quoted: getVerifiedQuoted(m.pushName) || m }) } catch { const content = image ? { image: { url: image }, caption } : { text: caption } return client.sendMessage(m.chat, content, { quoted: getVerifiedQuoted(m.pushName) || m }) } } /** * Handles starting, answering, rewinding, and stopping Akinator sessions. * @param {object} m Message wrapper. * @param {object} context Command context. * @returns {Promise<*>} Command response. */ const handler = async (m, { conn, xp, args, usedPrefix, command }) => { const client = conn || xp const id = m.sender const session = sessions.get(id) const sub = String(args[0] || '').toLowerCase() const target = args[1] if (target && target !== id) { const number = target.split('@')[0] return m.reply(`⚠️ *Tombol ini khusus untuk @${number} yang sedang bermain!*\n\nKetik \`${usedPrefix + command}\` untuk memulai sesi sendiri.`, { mentions: [target] }) } if (['stop', 'end', 'keluar', 'selesai', 'done'].includes(sub)) { sessions.delete(id) return m.reply(session ? '🛑 *Sesi Akinator telah dihentikan.*' : '✅ *Terima kasih telah bermain Akinator!*') } if (!session) { if (args.length && ['0', '1', '2', '3', '4', 'back', 'kembali'].includes(sub)) { return m.reply(`❌ *Kamu belum memiliki sesi game Akinator aktif.*\n\nKetik \`${usedPrefix + command}\` untuk mulai bermain!`) } await m.react('🧞') try { const state = await startSession('id') sessions.set(id, state) return sendQuestion(client, m, state, usedPrefix, command) } catch (error) { console.error('[Akinator Start Error]', error) return m.reply(`❌ Gagal terhubung ke server Akinator: ${error?.message || error}`) } } if (!args.length) return sendQuestion(client, m, session, usedPrefix, command) await m.react('⏳') try { if (sub === 'back' || sub === 'kembali') { if (!session.step) return m.reply('❌ Belum ada pertanyaan sebelumnya.') const previous = await backSession(session) sessions.set(id, previous) return sendQuestion(client, m, previous, usedPrefix, command) } const answer = ANSWERS.find(item => item.aliases.includes(sub)) if (!answer) return m.reply('Pilihan respon: 0 Ya, 1 Tidak, 2 Tidak Tahu, 3 Mungkin, 4 Mungkin Tidak.') const next = await answerSession(session, answer.key) if (next.win || next.name_proposition) { sessions.delete(id) await m.react('🧞') return sendGuess(client, m, next, usedPrefix, command) } sessions.set(id, next) return sendQuestion(client, m, next, usedPrefix, command) } catch (error) { console.error('[Akinator Answer Error]', error) return m.reply('❌ Gagal memproses jawaban ke server Akinator. Coba lagi.') } } handler.help = ['akinator', 'aki <0-4>', 'aki stop', 'aki back'] handler.tags = ['game'] handler.command = ['akinator', 'aki', 'tebakkarakter', 'jin'] handler.limit = true export default handler ``` --- ## Bridge: `akipy_bridge.py` ```python #!/usr/bin/env python3 """Akinator web bridge for the Mithra EX JavaScript plugin.""" import sys import json import asyncio from akipy.async_akinator import Akinator def state_from_akinator(aki): """Serialize the current Akinator object into JSON-compatible state.""" return { "uri": aki.uri, "lang": aki.lang, "theme": aki.theme, "session": aki.session, "signature": aki.signature, "identifiant": aki.identifiant, "step": aki.step, "progression": aki.progression, "step_last_proposition": aki.step_last_proposition, "question": aki.question, "win": aki.win, "name_proposition": aki.name_proposition, "description_proposition": aki.description_proposition, "photo": aki.photo, } def akinator_from_state(state): """Restore an Akinator object from plugin session state.""" aki = Akinator() aki.uri = state.get("uri", "https://id.akinator.com") aki.lang = state.get("lang", "id") aki.theme = state.get("theme", 1) aki.session = state.get("session") aki.signature = state.get("signature") aki.identifiant = state.get("identifiant") aki.step = int(state.get("step", 0)) aki.progression = float(state.get("progression", 0.0)) aki.step_last_proposition = state.get("step_last_proposition", "") aki.win = state.get("win", False) aki.name_proposition = state.get("name_proposition") aki.description_proposition = state.get("description_proposition") aki.photo = state.get("photo") return aki async def start_session(lang="id"): """Start a new Akinator session.""" aki = Akinator() await aki.start_game(language=lang, child_mode=False) return state_from_akinator(aki) async def answer_session(state, answer_id): """Submit an answer and return the updated session state.""" aki = akinator_from_state(state) await aki.answer(str(answer_id)) return state_from_akinator(aki) async def back_session(state): """Move the session back by one question.""" aki = akinator_from_state(state) await aki.back() return state_from_akinator(aki) async def main(): """Parse the CLI request and print one JSON response.""" if len(sys.argv) < 2: print(json.dumps({"error": "No action specified"})) return action = sys.argv[1] if action == "start": result = await start_session(sys.argv[2] if len(sys.argv) > 2 else "id") elif action == "answer": result = await answer_session(json.loads(sys.argv[2]), sys.argv[3]) elif action == "back": result = await back_session(json.loads(sys.argv[2])) else: result = {"error": f"Unknown action: {action}"} print(json.dumps(result, ensure_ascii=False)) if __name__ == "__main__": asyncio.run(main()) ```