Back to Home
Gistify Snippet
akinator.mdmarkdown
361 linesAkinator 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
Simpan bagian Plugin sebagai
cmd/plugins/game/akinator.js.Simpan bagian Bridge sebagai
system/lib/akipy_bridge.py.Install Python 3 dan library yang dibutuhkan:
python3 -m pip install akipyPastikan perintah berikut bisa dijalankan:
python3 --versionJika bridge disimpan di lokasi berbeda, atur path berikut sebelum menjalankan bot:
export AKINATOR_BRIDGE_PATH="/path/ke/akipy_bridge.py"
Perintah
.akiatau.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
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<object>} 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<string|null>} 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<object>} 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<object>} 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
#!/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())
Discussion & Comments0
No comments yet — be the first to leave feedback or start a code discussion.
Sign in with your GitHub account to write comments and join the discussion.