dola.jsJul 4, 202628.5 KB
raw
const axios = require('axios');
const crypto = require('crypto');
const qrcode = require('qrcode-terminal');
const fs = require('fs');

const sleep = ms => new Promise(r => setTimeout(r, ms));

class DolaAI {
    constructor() {
        this.cookies = null;
        
        this.inst = axios.create({
            baseURL: 'https://www.dola.com',
            headers: {
                'accept': '*/*',
                'agw-js-conv': 'str, str',
                'content-type': 'application/json',
                'origin': 'https://www.dola.com',
                'referer': 'https://www.dola.com/chat/',
                'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
            }
        });
        
        this.inst.interceptors.request.use(config => {
            if (this.cookies) config.headers['cookie'] = this.cookies.join('; ');
            return config;
        });
        
        this.inst.interceptors.response.use(res => {
            const setCookie = res.headers['set-cookie'];
            if (setCookie?.length) {
                this.cookies = this.cookies || [];
                for (const c of setCookie) {
                    const m = c.match(/^([^=]+)=([^;]+)/);
                    if (m && !this.cookies.some(ck => ck.startsWith(m[1] + '='))) {
                        this.cookies.push(`${m[1]}=${m[2]}`);
                    }
                }
            }
            
            return res;
        });
    }
    
    _uuid() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
            const r = crypto.randomInt(16);
            return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
    }
    
    _aws4Sign(method, host, urlPath, qs, body, accessKey, secretKey, sessionToken) {
        const now = new Date();
        const amzDate = now.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
        const dateStamp = amzDate.substring(0, 8);
        const payloadHash = crypto.createHash('sha256').update(body || '').digest('hex');
        const signedHeaders = body ? 'x-amz-content-sha256;x-amz-date;x-amz-security-token' : 'x-amz-date;x-amz-security-token';
        const canonicalHeaders = body ? `x-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\nx-amz-security-token:${sessionToken}\n` : `x-amz-date:${amzDate}\nx-amz-security-token:${sessionToken}\n`;
        const canonicalRequest = [method, urlPath, qs, canonicalHeaders, signedHeaders, payloadHash].join('\n');
        const credentialScope = `${dateStamp}/us-east-1/imagex/aws4_request`;
        const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, crypto.createHash('sha256').update(canonicalRequest).digest('hex')].join('\n');
        const kDate = crypto.createHmac('sha256', 'AWS4' + secretKey).update(dateStamp).digest();
        const kRegion = crypto.createHmac('sha256', kDate).update('us-east-1').digest();
        const kService = crypto.createHmac('sha256', kRegion).update('imagex').digest();
        const signingKey = crypto.createHmac('sha256', kService).update('aws4_request').digest();
        const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
        
        return {
            authorization: `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
            'x-amz-date': amzDate,
            'x-amz-content-sha256': payloadHash
        };
    }
    
    _crc32(buf) {
        let crc = 0xFFFFFFFF;
        const table = new Int32Array(256);
        for (let i = 0; i < 256; i++) {
            let c = i;
            for (let j = 0; j < 8; j++) {
                c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
            }
            table[i] = c;
        }
        
        for (let i = 0; i < buf.length; i++) {
            crc = table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
        }
        
        return ((crc ^ 0xFFFFFFFF) >>> 0).toString(16).padStart(8, '0');
    }
    
    _buildBlocks(message, attachmentUris = []) {
        const blocks = [];
        
        for (const uri of attachmentUris) {
            blocks.push({
                block_type: 10052,
                content: {
                    attachment_block: {
                        attachments: [{
                            type: 1,
                            identifier: this._uuid(),
                            image: {
                                name: 'image.png',
                                uri: uri,
                                image_ori: { url: '', width: 0, height: 0, format: '', url_formats: {} }
                            },
                            parse_state: 0,
                            review_state: 1,
                            upload_status: 1,
                            progress: 100,
                            src: ''
                        }]
                    },
                    pc_event_block: ''
                },
                block_id: this._uuid(),
                parent_id: '',
                meta_info: [],
                append_fields: []
            });
        }
        
        blocks.push({
            block_type: 10000,
            content: {
                text_block: { text: message, icon_url: '', icon_url_dark: '', summary: '' },
                pc_event_block: ''
            },
            block_id: this._uuid(),
            parent_id: '',
            meta_info: [],
            append_fields: []
        });
        
        return blocks;
    }
    
    _buildBody(blocks, conversationId, localConvId, deepThink = false, search = false) {
        return {
            client_meta: {
                local_conversation_id: localConvId,
                conversation_id: conversationId || '',
                bot_id: '7339470689562525703',
                last_section_id: '',
                last_message_index: null
            },
            messages: [{
                local_message_id: this._uuid(),
                content_block: blocks,
                message_status: 0
            }],
            option: {
                send_message_scene: search ? 'search' : '',
                create_time_ms: Date.now(),
                need_deep_think: deepThink ? 1 : 0,
                need_create_conversation: !conversationId,
                conversation_init_option: { need_ack_conversation: true },
                unique_key: this._uuid(),
                sse_recv_event_options: { support_chunk_delta: true },
                recovery_option: {
                    is_recovery: false,
                    req_create_time_sec: Math.floor(Date.now() / 1000),
                    append_sse_event_scene: 0
                },
                message_storage_type: 0
            },
            user_context: [],
            ext: {
                use_deep_think: deepThink ? '1' : '0',
                fp: 'verify_' + Array.from({ length: 12 }, () => crypto.randomInt(0, 10)).join(''),
                sub_conv_firstmet_type: '1',
                conversation_init_option: '{"need_ack_conversation":true}'
            }
        };
    }
    
    _parseSSE(data) {
        const result = {
            text: '',
            images: [],
            agentName: null,
            conversationId: null,
            localConversationId: null,
            messageId: null
        };
        
        const extract = block => {
            if (block.content?.text_block?.text) result.text += block.content.text_block.text;
            if (block.content?.creation_block?.creations) {
                for (const c of block.content.creation_block.creations) {
                    if (c.image?.image_thumb?.url) {
                        result.images.push({
                            url: c.image.image_thumb.url,
                            originalUrl: c.image.image_ori?.url || '',
                            key: c.image.key || '',
                            width: c.image.image_thumb.width || 0,
                            height: c.image.image_thumb.height || 0,
                            id: c.id
                        });
                    }
                }
            }
        };
        
        for (const event of data.split('\n\n')) {
            let eventType = '';
            let eventData = '';
            
            for (const line of event.split('\n')) {
                if (line.startsWith('event:')) eventType = line.slice(6).trim();
                if (line.startsWith('data:')) eventData = line.slice(5).trim();
            }
            
            if (!eventData) continue;
            try {
                const json = JSON.parse(eventData);
                
                if (json.ack_client_meta) {
                    result.conversationId = json.ack_client_meta.conversation_id;
                    result.localConversationId = json.ack_client_meta.local_conversation_id;
                }
                
                if (json.message_id) result.messageId = json.message_id;
                if (json.content?.ext?.agent_name) result.agentName = json.content.ext.agent_name;
                
                if (eventType === 'STREAM_MSG_NOTIFY') {
                    for (const block of (json.content?.content_block || [])) {
                        extract(block);
                    }
                }
                
                if (eventType === 'STREAM_CHUNK') {
                    for (const patch of (json.patch_op || [])) {
                        if (patch.patch_value?.ext?.agent_name) {
                            result.agentName = patch.patch_value.ext.agent_name;
                        }
                        
                        for (const block of (patch.patch_value?.content_block || [])) {
                            extract(block);
                        }
                    }
                }
            } catch (e) {}
        }
        
        return result;
    }
    
    _uploadAttachments = async function (buffers) {
        try {
            const uris = [];
            
            for (const buf of buffers) {
                const prepResp = await this.inst.post('/alice/resource/prepare_upload', {
                    tenant_id: '5',
                    scene_id: '4',
                    resource_type: 2
                }, {
                    params: {
                        aid: '495671',
                        device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                        device_platform: 'web',
                        language: 'en',
                        pc_version: '3.25.3',
                        pkg_type: 'release_version',
                        real_aid: '495671',
                        region: 'ID',
                        samantha_web: '1',
                        sys_region: 'ID',
                        tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
                        use_olympus_account: '1',
                        version_code: '20800',
                        web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                        web_platform: 'browser',
                        web_tab_id: this._uuid()
                    }
                });
                
                if (prepResp.data.code !== 0) throw new Error('prepare_upload failed');
                
                const { service_id: serviceId, upload_auth_token: authToken } = prepResp.data.data;
                const host = 'imagex-ap-southeast-1.bytevcloudapi.com';
                const applyParams = {
                    Action: 'ApplyImageUpload',
                    FileExtension: '.png',
                    FileSize: buf.length.toString(),
                    ServiceId: serviceId,
                    Version: '2018-08-01',
                    s: crypto.randomBytes(8).toString('hex')
                };
                
                const applyQs = Object.keys(applyParams).sort().map(k => `${k}=${encodeURIComponent(applyParams[k])}`).join('&');
                const applyAuth = this._aws4Sign('GET', host, '/', applyQs, '', authToken.access_key, authToken.secret_key, authToken.session_token);
                
                const applyResp = await axios.get(`https://${host}/?${applyQs}`, {
                    headers: {
                        'authorization': applyAuth.authorization,
                        'x-amz-date': applyAuth['x-amz-date'],
                        'x-amz-security-token': authToken.session_token,
                        'referer': 'https://www.dola.com/',
                        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
                    }
                });
                
                if (!applyResp.data.Result?.UploadAddress?.StoreInfos?.length) throw new Error('applyImageUpload failed');
                
                const storeInfo = applyResp.data.Result.UploadAddress.StoreInfos[0];
                const uploadHost = applyResp.data.Result.UploadAddress.UploadHosts?.[0] || 'tos-mya3-share.vodupload.com';
                
                await axios.post(`https://${uploadHost}/upload/v1/${storeInfo.StoreUri}`, buf, {
                    headers: {
                        'authorization': storeInfo.Auth,
                        'content-type': 'application/octet-stream',
                        'content-disposition': 'attachment; filename="image.png"',
                        'content-crc32': this._crc32(Buffer.from(buf)),
                        'referer': 'https://www.dola.com/',
                        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
                    },
                    timeout: 60000
                });
                
                const commitQs = Object.keys({ Action: 'CommitImageUpload', ServiceId: serviceId, Version: '2018-08-01' }).sort().map(k => `${k}=${encodeURIComponent({ Action: 'CommitImageUpload', ServiceId: serviceId, Version: '2018-08-01' }[k])}`).join('&');
                const commitBody = JSON.stringify({ SessionKey: applyResp.data.Result.UploadAddress.SessionKey });
                const commitAuth = this._aws4Sign('POST', host, '/', commitQs, commitBody, authToken.access_key, authToken.secret_key, authToken.session_token);
                
                const commitResp = await axios.post(`https://${host}/?${commitQs}`, commitBody, {
                    headers: {
                        'authorization': commitAuth.authorization,
                        'x-amz-content-sha256': commitAuth['x-amz-content-sha256'],
                        'x-amz-date': commitAuth['x-amz-date'],
                        'x-amz-security-token': authToken.session_token,
                        'referer': 'https://www.dola.com/',
                        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
                    }
                });
                
                if (!commitResp.data.Result?.Results?.length) throw new Error('commitUpload failed');
                
                const imageUri = commitResp.data.Result.Results[0].Uri;
                const localMsgId = this._uuid();
                
                await this.inst.post('/alice/message/pre_handle_v2_without_conv', {
                    uplink_entity: {
                        entity_type: 2,
                        entity_content: { image: { key: imageUri } },
                        identifier: localMsgId
                    },
                    bot_id: '7339470689562525703',
                    local_message_id: localMsgId
                }, {
                    params: {
                        aid: '495671',
                        device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                        device_platform: 'web',
                        language: 'en',
                        pc_version: '3.25.3',
                        pkg_type: 'release_version',
                        real_aid: '495671',
                        region: 'ID',
                        samantha_web: '1',
                        sys_region: 'ID',
                        tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
                        use_olympus_account: '1',
                        version_code: '20800',
                        web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                        web_platform: 'browser',
                        web_tab_id: this._uuid()
                    }
                });
                
                uris.push(imageUri);
            }
            
            return uris;
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    login = async function () {
        try {
            const initResp = await axios.get('https://www.dola.com/chat/', {
                headers: {
                    'accept': 'text/html',
                    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
                },
                maxRedirects: 5
            });
            
            this.cookies = [];
            if (initResp.headers['set-cookie']) {
                for (const c of initResp.headers['set-cookie']) {
                    const m = c.match(/^([^=]+)=([^;]+)/);
                    if (m) {
                        this.cookies.push(`${m[1]}=${m[2]}`);
                    }
                }
            }
            
            console.log('Requesting QR code...\n');
            const qrResp = await this.inst.get('/passport/web/get_qrcode/', {
                params: {
                    aid: '495671',
                    device_platform: 'web',
                    language: 'en',
                    account_sdk_source: 'web',
                    passport_jssdk_version: '2.0.1-verify-center.1',
                    next: 'https://www.dola.com',
                    verifyFp: `verify_${Array.from({ length: 12 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 8 }, () => crypto.randomInt(0, 10)).join('')}`,
                    sign: crypto.randomBytes(32).toString('hex'),
                    qs: Buffer.from(`flow_user_country=ID|flow_ssr_sidebar_expand=1|web_id=${Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join('')}`).toString('base64')
                }
            });
            
            if (qrResp.data.data?.error_code !== 0) throw new Error('Failed to get QR code');
            const qrToken = qrResp.data.data.token;
            
            qrcode.generate(qrResp.data.data.qrcode_index_url, { small: true }, qr => console.log(qr));
            await sleep(300);
            
            console.log('Waiting for scan... (Ctrl+C to cancel)');
            
            let attempts = 0;
            while (attempts < 120) {
                attempts++;
                
                try {
                    const statusResp = await this.inst.get('/passport/web/check_qrconnect/', {
                        params: {
                            aid: '495671',
                            device_platform: 'web',
                            language: 'en',
                            account_sdk_source: 'web',
                            passport_jssdk_version: '2.0.1-verify-center.1',
                            next: 'https://www.dola.com',
                            token: qrToken,
                            verifyFp: `verify_${Array.from({ length: 12 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 4 }, () => crypto.randomInt(0, 10)).join('')}_${Array.from({ length: 8 }, () => crypto.randomInt(0, 10)).join('')}`,
                            sign: crypto.randomBytes(32).toString('hex'),
                            qs: Buffer.from(`flow_user_country=ID|flow_ssr_sidebar_expand=1|web_id=${Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join('')}`).toString('base64')
                        }
                    });
                    
                    const data = statusResp.data.data;

                    if (data.status === 'confirmed') {
                        console.log('\n    Confirmed!');
                        if (data.redirect_url) {
                            await axios.get(data.redirect_url, {
                                headers: {
                                    'cookie': this.cookies.join('; '),
                                    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
                                },
                                maxRedirects: 5
                            });
                        }
                        
                        const cookieFile = `dola-cookies-${Date.now()}.json`;
                        fs.writeFileSync(cookieFile, JSON.stringify(this.cookies, null, 4));
                        console.log(`\nLogin Successful`);
                        console.log(`Cookies saved to: ${cookieFile}`);
                        
                        return cookieFile;
                    }
                    
                    if (data.status === 'expired') throw new Error('QR code expired');
                    process.stdout.write(`\r    Waiting... (${attempts}s)   `);
                } catch (e) {
                    if (e.message === 'QR code expired') {
                        throw e;
                    }
                }
                
                await sleep(1000);
            }
            
            throw new Error('Login timeout');
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    loadCookies = async function (cookiePath) {
        try {
            this.cookies = JSON.parse(fs.readFileSync(cookiePath, 'utf-8'));
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    chat = async function (message, { conversationId = null, attachments = [], deepThink = false, search = false } = {}) {
        try {
            const localConvId = 'local_' + Array.from({ length: 13 }, () => crypto.randomInt(0, 10)).join('');
            const attachmentUris = attachments.length > 0 ? await this._uploadAttachments(attachments) : [];
            const blocks = this._buildBlocks(message, attachmentUris);
            const body = this._buildBody(blocks, conversationId, localConvId, deepThink, search);
            
            const resp = await this.inst.post('/chat/completion', body, {
                params: {
                    aid: '495671',
                    device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                    device_platform: 'web',
                    language: 'en',
                    pc_version: '3.25.3',
                    pkg_type: 'release_version',
                    real_aid: '495671',
                    region: 'ID',
                    samantha_web: '1',
                    sys_region: 'ID',
                    tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
                    use_olympus_account: '1',
                    version_code: '20800',
                    web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                    web_platform: 'browser',
                    web_tab_id: this._uuid()
                },
                headers: {
                    'x-flow-trace': '04-' + crypto.randomBytes(16).toString('hex') + '-01'
                },
                timeout: 120000,
                responseType: 'text'
            });
            
            const result = this._parseSSE(resp.data);
            
            return {
                id: result.messageId,
                conversationId: result.conversationId || conversationId,
                agentName: result.agentName,
                content: {
                    text: result.text,
                    images: result.images
                }
            };
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    listConversation = async function (options = {}) {
        try {
            const { count = 50, offset = 0 } = options;
            const resp = await this.inst.post('/alice/conversation/list', {
                count,
                offset
            }, {
                params: {
                    aid: '495671',
                    device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                    device_platform: 'web',
                    language: 'en',
                    pc_version: '3.25.3',
                    pkg_type: 'release_version',
                    real_aid: '495671',
                    region: 'ID',
                    samantha_web: '1',
                    sys_region: 'ID',
                    tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
                    use_olympus_account: '1',
                    version_code: '20800',
                    web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                    web_platform: 'browser',
                    web_tab_id: this._uuid()
                }
            });
            
            return resp.data?.data?.conversation_list || [];
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    getConversation = async function (conversationId) {
        try {
            const resp = await this.inst.post('/alice/conversation/info', {
                conversation_id: conversationId
            }, {
                params: {
                    aid: '495671',
                    device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                    device_platform: 'web',
                    language: 'en',
                    pc_version: '3.25.3',
                    pkg_type: 'release_version',
                    real_aid: '495671',
                    region: 'ID',
                    samantha_web: '1',
                    sys_region: 'ID',
                    tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
                    use_olympus_account: '1',
                    version_code: '20800',
                    web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                    web_platform: 'browser',
                    web_tab_id: this._uuid()
                }
            });
            
            return resp.data?.data?.conversation || null;
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    deleteConversation = async function (conversationId) {
        try {
            const resp = await this.inst.post('/samantha/thread/delete', {
                thread_id: conversationId
            }, {
                params: {
                    aid: '495671',
                    device_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                    device_platform: 'web',
                    language: 'en',
                    pc_version: '3.25.3',
                    pkg_type: 'release_version',
                    real_aid: '495671',
                    region: 'ID',
                    samantha_web: '1',
                    sys_region: 'ID',
                    tea_uuid: Array.from({ length: 16 }, () => crypto.randomInt(0, 10)).join(''),
                    use_olympus_account: '1',
                    version_code: '20800',
                    web_id: Array.from({ length: 18 }, () => crypto.randomInt(0, 10)).join(''),
                    web_platform: 'browser',
                    web_tab_id: this._uuid()
                }
            });
            
            return resp.data;
        } catch (error) {
            throw new Error(error.message);
        }
    }
}

// caraa pemakaiannyaa:
(async () => {
    const dola = new DolaAI();
    
    // kloo belum punya cookies, loginn duluu
    // await dola.login();
    // kloo udaa punya cookies, tinggall di loadd
    await dola.loadCookies('./dola-cookies.json');
    
    // tinggall eksekusii dehh
    const chat1 = await dola.chat('haii! namaku riruu');
    console.log(JSON.stringify(chat1, null, 2));
    
    // kloo mauu follow up chatt
    const chat2 = await dola.chat('siapaa namaku??', { conversationId: chat1.conversationId });
    console.log(JSON.stringify(chat2, null, 2));
})();