Back to Home
Gistify Snippet
kyiov
1d ago·1 file(s)
Public

Akinator Plugin dan Python Bridge

Akinator Plugin dan Python Bridge

#github-gist#markdown
0
Raw
akinator.mdmarkdown
361 lines

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:

    python3 -m pip install akipy
    
  4. Pastikan perintah berikut bisa dijalankan:

    python3 --version
    
  5. Jika bridge disimpan di lokasi berbeda, atur path berikut sebelum menjalankan bot:

    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

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.