Voice Stealer

Добавляет возможность сохранения чужих голосовых сообщений и отправки их от своего имени.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Voice Stealer
// @namespace    https://vk.com/
// @version      1.6.0
// @description  Добавляет возможность сохранения чужих голосовых сообщений и отправки их от своего имени.
// @author       FallenAstaroth
// @match        https://vk.com/*
// @icon         https://img.icons8.com/color/512/vk-circled.png
// @grant        GM.xmlHttpRequest
// @run-at       document-end
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==

(async function() {
    "use strict";

    const db = await initDb();
    const myId = await getMyId();
    let replyMessageId = null;

    insertCss(`
        :root {
            --color-back-grey: #222222;
            --color-border: #424242;
            --color-grey: #656565;
            --color-hover-grey: #828282;
            --color-scrollbar: #888;
            --color-hover-scrollbar: #555;
            --color-border-green: #6abd71;
            --transition-time: .3s;
        }
        .voice-popup {
            display: none;
            background: rgba(0, 0, 0, .6);
            width: 100%;
            height: 100%;
            position: fixed;
            top: 0;
            left: 0;
            z-index: 1000;
        }
        .voice-stealer-save-audio,
        .voice-popup button {
            border: none;
            background: transparent;
            padding: 0;
            cursor: pointer;
        }
        .voice-popup h2,
        .voice-popup p {
            margin: 0;
        }
        .voice-popup .items .item .delete {
            position: relative;
            width: 18px;
            height: 18px;
            opacity: 1;
            transition: var(--transition-time);
        }
        .voice-popup .close {
            position: absolute;
            right: 16px;
            top: 16px;
            width: 18px;
            height: 18px;
            opacity: 1;
            transition: var(--transition-time);
        }
        .voice-popup .items .item .delete:hover,
        .voice-popup .close:hover {
          opacity: .7;
        }
        .voice-popup .items .item .delete:before, .voice-popup .items .item .delete:after,
        .voice-popup .close:before, .close:after {
            position: absolute;
            left: 8px;
            top: 1px;
            content: ' ';
            height: 16px;
            width: 2px;
            background-color: var(--color-grey);
        }
        .voice-popup .items .item .delete:before,
        .voice-popup .close:before {
            transform: rotate(45deg);
        }
        .voice-popup .items .item .delete:after,
        .voice-popup .close:after {
            transform: rotate(-45deg);
        }
        .voice-popup .content {
            width: 100%;
            max-width: 300px;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            padding: 20px;
            background-color: var(--color-back-grey);
            border-radius: 10px;
            border: 1px solid var(--color-border);
            z-index: 1001;
        }
        .voice-popup .items {
            overflow-x: auto;
            height: 100%;
            max-height: 305px;
            margin-top: 20px;
            background: var(--color-back-grey);
            border: 1px solid var(--color-border);
        }
        .voice-popup .tooltips::-webkit-scrollbar,
        .voice-popup .items::-webkit-scrollbar {
            width: 5px;
        }
        .voice-popup .tooltips::-webkit-scrollbar-track,
        .voice-popup .items::-webkit-scrollbar-track {
            background: transparent;
        }
        .voice-popup .tooltips::-webkit-scrollbar-thumb,
        .voice-popup .items::-webkit-scrollbar-thumb {
            background: var(--color-scrollbar);
        }
        .voice-popup .tooltips::-webkit-scrollbar-thumb:hover,
        .voice-popup .items::-webkit-scrollbar-thumb:hover {
            background: ver(--color-hover-scrollbar);
        }
        .voice-popup .items .item:not(:first-child) {
            border-top: 1px solid var(--color-border);
        }
        .voice-popup .items .item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 5px 10px;
            transition: var(--transition-time);
        }
        .voice-popup .items .item.searched {
            border: 1px solid var(--color-border-green);
        }
        .voice-popup .items .item p {
            width: 100%;
            margin-left: 10px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        .voice-popup .items .item p svg {
            width: 20px;
            height: 20px;
        }
        .voice-popup .tools {
            margin-top: 20px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        .voice-popup .upload-audio {
            display: flex;
            align-items: center;
            margin-top: 20px;
        }
        .voice-popup .upload-audio .audio {
            display: none;
        }
        .voice-popup .upload-audio .save,
        .voice-popup .upload-audio .upload {
            padding: 8px;
            margin-left: 10px;
            background-color: var(--color-grey);
            display: block;
            border-radius: 5px;
            cursor: pointer;
            display: block;
            transition: var(--transition-time);
        }
        .voice-popup .upload-audio.multi-upload .upload {
            margin-left: 0;
        }
        .voice-popup .upload-audio.multi-upload .save {
            width: 100%;
        }
        .voice-popup .upload-audio .save:hover,
        .voice-popup .upload-audio .upload:hover {
            background-color: var(--color-hover-grey);
        }
        .voice-popup .upload-audio .save {
            padding: 11px 12px;
        }
        .voice-popup .form {
            width: 100%;
            display: flex;
            justify-content: space-between;
            margin-top: 10px;
        }
        .voice-popup .upload-audio .name,
        .voice-popup .search input,
        .voice-popup .form input {
            width: 100%;
            background: transparent;
            border: 1px solid var(--color-border);
            border-radius: 5px;
            padding: 10px 12px;
        }
        .voice-popup .form button {
            background-color: var(--color-grey);
            border-radius: 5px;
            padding: 10px 12px;
            margin-left: 10px;
            transition: var(--transition-time);
        }
        .voice-popup .form button:hover {
            background-color: var(--color-hover-grey);
        }
        .voice-popup .search.form {
            margin-top: 0;
        }
        .voice-popup .search .tooltips {
            position: absolute;
            overflow-x: auto;
            max-height: 120px;
            top: 45px;
            left: 0;
            display: none;
            padding: 10px 0;
            background: var(--color-back-grey);
            border: 1px solid var(--color-border);
            border-radius: 5px;
        }
        .voice-popup .search .tooltips .tooltip {
            padding: 5px 12px;
            cursor: pointer;
        }
        .voice-popup .search,
        .im_msg_audiomsg {
            position: relative;
        }
        .voice-stealer-save-audio {
            position: absolute;
            padding: 0 5px;
            bottom: 30px;
            right: -25px;
        }
        .voice-stealer-save-audio svg {
            width: 16px;
            height: 16px;
        }
    `);

    function insertCss(css) {
        var head = document.getElementsByTagName("head")[0];
        if (!head) {
            return;
        }
        var style = document.createElement("style");
        style.type = "text/css";
        style.innerHTML = css;
        head.appendChild(style);
    }

    function insertElements() {
        $("body").append(`
            <div class="voice-popup voice-messages-list">
                <div class="content">
                    <button class="close"></button>
                    <h2>Список сохранённых ГС</h2>
                    <div class="items"></div>
                    <div class="tools">
                        <div class="search form">
                            <input type="text" placeholder="Поиск">
                            <div class="tooltips"></div>
                        </div>
                    </div>
                    <div class="upload-audio single-upload">
                        <input type="text" class="name" placeholder="Название">
                        <input class="audio" id="stealer-audio" type="file" accept=".mp3">
                        <label for="stealer-audio" class="upload">
                            <svg fill="none" height="18" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg">
                                <g fill="currentColor">
                                    <path d="M19 19a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2zm-7-2a1 1 0 0 1-1-1V5.41l-4.3 4.3a1 1 0 0 1-1.31.08l-.1-.08a1 1 0 0 1 0-1.42l6-6a1 1 0 0 1 1.42 0l6 6a1 1 0 0 1-1.42 1.42L13 5.4V16a1 1 0 0 1-1 1z"></path>
                                </g>
                            </svg>
                        </label>
                        <button class="save">Добавить</button>
                    </div>
                    <div class="upload-audio multi-upload">
                        <input class="audio" id="stealer-audios" type="file" accept=".mp3" multiple>
                        <label for="stealer-audios" class="upload">
                            <svg fill="none" height="18" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg">
                                <g fill="currentColor">
                                    <path d="M19 19a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2zm-7-2a1 1 0 0 1-1-1V5.41l-4.3 4.3a1 1 0 0 1-1.31.08l-.1-.08a1 1 0 0 1 0-1.42l6-6a1 1 0 0 1 1.42 0l6 6a1 1 0 0 1-1.42 1.42L13 5.4V16a1 1 0 0 1-1 1z"></path>
                                </g>
                            </svg>
                        </label>
                        <button class="save">Добавить все</button>
                    </div>
                </div>
            </div>
        `);
        $("body").append(`
            <div class="voice-popup voice-messages-save">
                <div class="content">
                    <button class="close"></button>
                    <h2>Сохранить новое ГС</h2>
                    <div class="form">
                        <input type="text" placeholder="Название"/>
                        <button class="save">Сохранить</button>
                    </div>
                </div>
            </div>
        `);
        $(".im_chat-input--buttons").prepend(`
            <div class="im-chat-input--attach voice-stealer">
                <label onmouseover="showTooltip(this, { text: 'Отправить сохранённое ГС', black: true, shift: [4, 5] });" class="im-chat-input--attach-label">
                    <svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
                        <g id="music_outline_20__Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
                            <g id="music_outline_20__Icons-20/music_outline_20">
                                <g id="music_outline_20__music_outline_20">
                                    <path d="M0 0h20v20H0z"></path>
                                    <path d="M14.73 2.05a2.28 2.28 0 0 1 2.75 2.23v7.99c0 3.57-3.5 5.4-5.39 3.51-1.9-1.9-.06-5.38 3.52-5.38h.37V6.76L8 8.43v5.82c0 3.5-3.35 5.34-5.27 3.62l-.11-.1c-1.9-1.9-.06-5.4 3.51-5.4h.37V6.24c0-.64.05-1 .19-1.36l.05-.13c.17-.38.43-.7.76-.93.36-.26.7-.4 1.41-.54ZM6.5 13.88h-.37c-2.32 0-3.34 1.94-2.45 2.82.88.89 2.82-.13 2.82-2.45v-.37Zm9.48-1.98h-.37c-2.32 0-3.34 1.94-2.46 2.82.89.89 2.83-.13 2.83-2.45v-.37Zm-.02-7.78a.78.78 0 0 0-.92-.6L9.06 4.77c-.4.09-.54.15-.68.25a.8.8 0 0 0-.27.33c-.08.18-.1.35-.1.88v.67l7.97-1.67V4.2Z" id="music_outline_20__Icon-Color" fill="currentColor" fill-rule="nonzero"></path>
                                </g>
                            </g>
                        </g>
                    </svg>
                </label>
            </div>
        `);
    }

    function insertSaveButtonOnLoad() {
        $(".im_msg_audiomsg").append(`
            <button class="voice-stealer-save-audio">
                <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
                    <path fill-rule="evenodd" d="M10.18 1.5H9c-.8 0-1.47 0-2.01.05-.63.05-1.17.16-1.67.41a4.25 4.25 0 0 0-1.86 1.86c-.25.5-.36 1.04-.41 1.67C3 6.1 3 6.86 3 7.82v4.36c0 .95 0 1.71.05 2.33.05.63.16 1.17.41 1.67a4.25 4.25 0 0 0 1.86 1.86c.5.25 1.04.36 1.67.4.61.06 1.37.06 2.33.06h1.36c.96 0 1.72 0 2.33-.05a4.39 4.39 0 0 0 1.67-.41 4.25 4.25 0 0 0 1.86-1.86c.25-.5.36-1.04.41-1.67.05-.62.05-1.38.05-2.33V8.32c0-.48 0-.73-.06-.96-.05-.2-.13-.4-.24-.58-.12-.2-.3-.37-.64-.72l-3.62-3.62a4.27 4.27 0 0 0-.72-.65 2 2 0 0 0-.58-.24c-.23-.05-.48-.05-.96-.05Zm5.32 10.65c0 1 0 1.7-.04 2.24a2.9 2.9 0 0 1-.26 1.1A2.75 2.75 0 0 1 14 16.7c-.25.13-.57.21-1.11.26-.55.04-1.25.04-2.24.04h-1.3c-1 0-1.7 0-2.24-.04a2.9 2.9 0 0 1-1.1-.26 2.75 2.75 0 0 1-1.21-1.2 2.94 2.94 0 0 1-.26-1.11c-.04-.55-.04-1.25-.04-2.24v-4.3c0-1 0-1.7.04-2.24.05-.53.13-.86.26-1.1A2.75 2.75 0 0 1 6 3.3c.25-.13.57-.21 1.11-.26C7.66 3 8.36 3 9.35 3H10v2.35c0 .4 0 .76.02 1.05.03.3.09.61.24.9.21.4.54.73.94.94.29.15.6.21.9.24.29.02.64.02 1.05.02h2.35v3.65ZM14.88 7 11.5 3.62v1.7c0 .45 0 .74.02.95.02.22.05.3.07.33a.75.75 0 0 0 .3.31c.05.02.12.05.33.07.22.02.51.02.96.02h1.7Z" clip-rule="evenodd"></path>
                </svg>
            </button>
        `);
    }

    function insertSaveButtonOnUpdate() {
        $(".im-page-chat-contain").on("DOMSubtreeModified", function(event) {
            if ($(event.target).find(".im_msg_audiomsg .voice-stealer-save-audio").length > 0) {
                return;
            }
            $(event.target).find(".im_msg_audiomsg").append(`
                <button class="voice-stealer-save-audio">
                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 20 20">
                        <path fill-rule="evenodd" d="M10.18 1.5H9c-.8 0-1.47 0-2.01.05-.63.05-1.17.16-1.67.41a4.25 4.25 0 0 0-1.86 1.86c-.25.5-.36 1.04-.41 1.67C3 6.1 3 6.86 3 7.82v4.36c0 .95 0 1.71.05 2.33.05.63.16 1.17.41 1.67a4.25 4.25 0 0 0 1.86 1.86c.5.25 1.04.36 1.67.4.61.06 1.37.06 2.33.06h1.36c.96 0 1.72 0 2.33-.05a4.39 4.39 0 0 0 1.67-.41 4.25 4.25 0 0 0 1.86-1.86c.25-.5.36-1.04.41-1.67.05-.62.05-1.38.05-2.33V8.32c0-.48 0-.73-.06-.96-.05-.2-.13-.4-.24-.58-.12-.2-.3-.37-.64-.72l-3.62-3.62a4.27 4.27 0 0 0-.72-.65 2 2 0 0 0-.58-.24c-.23-.05-.48-.05-.96-.05Zm5.32 10.65c0 1 0 1.7-.04 2.24a2.9 2.9 0 0 1-.26 1.1A2.75 2.75 0 0 1 14 16.7c-.25.13-.57.21-1.11.26-.55.04-1.25.04-2.24.04h-1.3c-1 0-1.7 0-2.24-.04a2.9 2.9 0 0 1-1.1-.26 2.75 2.75 0 0 1-1.21-1.2 2.94 2.94 0 0 1-.26-1.11c-.04-.55-.04-1.25-.04-2.24v-4.3c0-1 0-1.7.04-2.24.05-.53.13-.86.26-1.1A2.75 2.75 0 0 1 6 3.3c.25-.13.57-.21 1.11-.26C7.66 3 8.36 3 9.35 3H10v2.35c0 .4 0 .76.02 1.05.03.3.09.61.24.9.21.4.54.73.94.94.29.15.6.21.9.24.29.02.64.02 1.05.02h2.35v3.65ZM14.88 7 11.5 3.62v1.7c0 .45 0 .74.02.95.02.22.05.3.07.33a.75.75 0 0 0 .3.31c.05.02.12.05.33.07.22.02.51.02.96.02h1.7Z" clip-rule="evenodd"></path>
                    </svg>
                </button>
            `);
        });
    }

    async function insertAudioList() {
        let audios = await dbGetAudios();

        if (audios.length > 0) {
            let elements, tooltips;
            elements = tooltips = "";

            audios.forEach((element) => {
                elements += formatAudio(element.id, element.audio, element.attachment);
                tooltips += formatTooltip(element.id, element.audio);
            });

            $(".voice-messages-list .items").append(elements);
            $(".voice-messages-list .tools .tooltips").append(tooltips);
        } else {
            $(".voice-messages-list .items").append(formatError("Вы ещё не сохраняли ГС"));
        }
    }

    function formatTooltip(record, title) {
        return `<div class="tooltip" data-record-id="${record}"><p>${title}</p></div>`;
    }

    function formatAudio(record, title, attachment) {
        return `
            <div class="item">
                <button data-record-id="${record}" class="delete"></button>
                <p> ${title}
                    <button data-audio-id="${attachment}" class="send">
                        <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
                            <g id="send_24__Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
                                <g id="send_24__send_24">
                                    <path id="send_24__Rectangle-76" d="M0 0h24v24H0z"></path>
                                    <path d="M5.74 15.75a39.14 39.14 0 0 0-1.3 3.91c-.55 2.37-.95 2.9 1.11 1.78 2.07-1.13 12.05-6.69 14.28-7.92 2.9-1.61 2.94-1.49-.16-3.2C17.31 9.02 7.44 3.6 5.55 2.54c-1.89-1.07-1.66-.6-1.1 1.77.17.76.61 2.08 1.3 3.94a4 4 0 0 0 3 2.54l5.76 1.11a.1.1 0 0 1 0 .2L8.73 13.2a4 4 0 0 0-3 2.54Z" id="send_24__Mask" fill="currentColor"></path>
                                </g>
                            </g>
                        </svg>
                    </button>
                </p>
            </div>
        `;
    }

    function formatError(text) {
        return `
            <div class="error">
                <p>${text}</p>
            </div>
        `;
    }

    async function initDb() {
        return new Promise((resolve, reject) => {
            let request = indexedDB.open("audios", 1);

            request.onerror = event => {
                console.error(event);
            }

            request.onupgradeneeded = event => {
                let db = event.target.result;
                let objectStore = db.createObjectStore("audios", { keyPath: "id", autoIncrement: true });
                objectStore.createIndex("audio", "audio", { unique: false });
            };

            request.onsuccess = event => {
                resolve(event.target.result);
            };
        });
    }

    async function dbGetAudios() {
        return new Promise((resolve, reject) => {
            let transaction = db.transaction(["audios"], "readonly");

            transaction.onerror = event => {
                reject(event);
            };

            let store = transaction.objectStore("audios");

            store.getAll().onsuccess = event => {
                resolve(event.target.result);
            };
        });
    }

    async function dbAddAudio(audio) {
        return new Promise((resolve, reject) => {
            let transaction = db.transaction(["audios"], "readwrite");

            transaction.onerror = event => {
                reject(event);
            };

            let store = transaction.objectStore("audios");

            store.put(audio).onsuccess = event => {
                resolve(event.target.result);
            };
        });
    }

    async function dbDelAudio(key) {
        return new Promise((resolve, reject) => {
            let transaction = db.transaction(["audios"], "readwrite");

            transaction.oncomplete = event => {
                resolve();
            };

            transaction.onerror = event => {
                reject(event);
            };

            let store = transaction.objectStore("audios");
            store.delete(key);
        });
    }

    async function callApi(method, data) {
        return await vkApi.api(method, data);
    }

    async function getPeerId() {
        let link = $(".im-page--aside-photo ._im_header_link").attr("href");

        if (link.includes("sel=")) {
            return 2000000000 + parseInt(link.split("=c")[1]);
        } else {
            return (await callApi("users.get", {
                user_ids: link.slice(1)
            }))[0].id;
        }
    }

    async function sendAudio(object) {
        let data = {
            peer_id: (await getPeerId()),
            attachment: $(object).attr("data-audio-id"),
            random_id: 0
        }

        if (replyMessageId) {
            data.reply_to = replyMessageId;
        }

        await callApi("messages.send", data);
        $(".voice-messages-list").fadeToggle(150);

        if (replyMessageId) {
            $(".im-replied-container--remove").click();
        }

        replyMessageId = null;
    }

    async function saveAudio(peerId, messageId, audioIndex) {
        let attachment, message;

        let data = await callApi("messages.getByConversationMessageId", {
            peer_id: peerId,
            conversation_message_ids: messageId
        });

        if (data.items[0].fwd_messages.length > 0) {
            message = data.items[0].fwd_messages[audioIndex].attachments[0].audio_message;
        } else {
            message = data.items[0].attachments[audioIndex].audio_message;
        }

        if (message.owner_id === myId) {
            attachment = `doc${message.owner_id}_${message.id}`;
        } else {
            let formData = new FormData();

            let data = await fetch(message.link_mp3, {
                method: "GET"
            });

            let blob = await data.blob();
            formData.append("file", blob);

            attachment = await uploadAudio(formData);
        }

        return attachment;
    }

    async function uploadAudio(formData) {
        let url = (await callApi("docs.getUploadServer", {
            type: "audio_message"
        })).upload_url;

        let file = await fetch(url, {
            method: "POST",
            body: formData
        });

        file = JSON.parse(await file.text()).file;

        let result = (await callApi("docs.save", {
            file: file
        })).audio_message;

        return `doc${result.owner_id}_${result.id}`;
    }

    async function addAudioFromSave(object) {
        let audio = $(object).parent().find("input").val();
        let peerId = $(object).attr("data-message-peer");
        let messageId = $(object).attr("data-message-id");
        let audioIndex = $(object).attr("data-message-index");

        let attachment = await saveAudio(peerId, messageId, audioIndex);

        let record = await dbAddAudio({
            audio: audio,
            attachment: attachment
        });

        $(".voice-messages-list .items").append(formatAudio(record, audio, attachment));
        $(".voice-messages-list .tools .tooltips").append(formatTooltip(record, audio));
        $(".voice-messages-list .items .error").remove();
    }

    async function addAudioFromUpload() {
        try {
            let formData = new FormData();

            $(".voice-popup .single-upload .save").prop("disabled", true);
            $(".voice-popup .single-upload .save").html("Загрузка...");
            formData.append("file", document.getElementById("stealer-audio").files[0]);

            let audio = $(".voice-popup .single-upload .name").val();
            let attachment = await uploadAudio(formData);

            let record = await dbAddAudio({
                audio: audio,
                attachment: attachment
            });

            $(".voice-messages-list .items").append(formatAudio(record, audio, attachment));
            $(".voice-messages-list .tools .tooltips").append(formatTooltip(record, audio));
            $(".voice-messages-list .items .error").remove();
        } catch(error) {
            console.error(error.message);
        } finally {
            $(".voice-popup .single-upload .save").prop("disabled", false);
            $(".voice-popup .single-upload .save").html("Добавить");
        }
    }

    async function addAudioFromMultiUpload() {
        try {
            let audios = [];

            $(".voice-popup .multi-upload .save").prop("disabled", true);
            $(".voice-popup .multi-upload .save").html("Загрузка...");

            Array.from(document.getElementById("stealer-audios").files).forEach(function(file) {
                let formData = new FormData();
                formData.append("file", file);
                audios.push(promiseUpload(formData));
            });

            let results = await Promise.allSettled(audios);
        } catch(error) {
            console.error(error.message);
        } finally {
            $(".voice-popup .multi-upload .save").prop("disabled", false);
            $(".voice-popup .multi-upload .save").html("Добавить все");
        }
    }

    function promiseUpload(formData) {
        return new Promise(async (resolve, reject) => {
            try {
                let attachment = await uploadAudio(formData);
                let name = formData.get("file").name;
                let audio = name.slice(0, -4);

                let record = await dbAddAudio({
                    audio: audio,
                    attachment: attachment
                });

                $(".voice-messages-list .items").append(formatAudio(record, audio, attachment));
                $(".voice-messages-list .tools .tooltips").append(formatTooltip(record, audio));
                $(".voice-messages-list .items .error").remove();

                resolve(200);
            } catch(error) {
                reject(500);
            }
        })
    }

    async function deleteAudio(object) {
        let record = $(object).parent().find("button.delete").attr("data-record-id");

        await dbDelAudio(parseInt(record));

        $(object).parent().remove();
        $(`.voice-messages-list .tooltips .tooltip`).remove(`[data-record-id="${record}"]`);

        if ($(".voice-messages-list .items .item").length <= 0) {
            $(".voice-messages-list .items").append(formatError("Нет сохраенённых ГС"));
        }
    }

    async function showAudio(object) {
        $(".voice-messages-list .tooltips .tooltip").hide();

        if ($(object).val() === "") {
            $(".voice-messages-list .items .item").removeClass("searched");
            $(`.voice-messages-list .tooltips`).fadeOut(150);
        } else {
            let elements = $(`.voice-messages-list .tooltips .tooltip p:contains("${$(object).val()}")`);

            if (elements.length <= 0) {
                $(`.voice-messages-list .tooltips`).fadeOut(150);
                return;
            }
            elements.parent().show();

            $(`.voice-messages-list .tooltips`).fadeIn(150);
        }
    }

    async function searchAudio(object) {
        let selectorList = ".voice-messages-list .items";
        let selectorItem = $(`.voice-messages-list .items .item`).removeClass("searched").find(`[data-record-id="${$(object).attr("data-record-id")}"]`);

        if (selectorItem.length <= 0) {
            return;
        }

        $(selectorList).stop().animate( {
            scrollTop: selectorItem[0].offsetTop - $(selectorList)[0].offsetTop - 10
        }, 150);

        selectorItem.parent().addClass("searched");
    }

    async function getMyId() {
        return (await callApi("users.get", {}))[0].id;
    }

    async function replyMessage(message) {
        replyMessageId = $(message).closest(".im-mess").attr("data-msgid");
    }

    function observeSendButton() {
        $(".im_chat-input--buttons .voice-stealer").unbind("click").on("click", function() {
            $(".voice-messages-list").fadeToggle(150);
        });
    }

    function observeSaveButton() {
        $(".im-page--chat-body").unbind("click", ".im_msg_audiomsg .voice-stealer-save-audio").on("click", ".im_msg_audiomsg .voice-stealer-save-audio", function() {
            event.preventDefault();
            event.stopPropagation();

            let index = $(this).closest(".im-mess-stack_fwd").index();
            index = (index === -1) ? 0 : index;

            $(".voice-messages-save button.save")
                .attr("data-message-id", $(this).closest(".im-mess:not(.im-mess_fwd)").attr("data-cmid"))
                .attr("data-message-peer", $(this).closest(".im-mess:not(.im-mess_fwd)").attr("data-peer"))
                .attr("data-message-index", index);
            $(".voice-messages-save input").val("");
            $(".voice-messages-save").fadeToggle(150);
            $(".voice-messages-save input").focus();
        });
    }

    function observeCloseButton() {
        $(".voice-popup .close").unbind("click").on("click", function() {
            $(this).parent().parent().fadeToggle(150);
        });
    }

    function observeAudioSend() {
        $(".voice-messages-list .items").unbind("click", ".item button.send").on("click", ".item button.send", function() {
            sendAudio(this);
        });
    }

    function observeAudioSave() {
        $(".voice-messages-save button.save").unbind("click").on("click", function() {
            addAudioFromSave(this);
            $(".voice-messages-save").fadeToggle(150);
        });
    }

    function observeAudioDelete() {
        $(".voice-messages-list .items").unbind("click", ".item button.delete").on("click", ".item button.delete", function() {
            deleteAudio(this);
        });
    }

    function observeAudioTooltips() {
        $(".voice-messages-list .search input").unbind("input").on("input", function() {
            showAudio(this);
        });
    }

    function observeAudioSearch() {
        $(".voice-messages-list .search").unbind("click", ".tooltip").on("click", ".tooltip", function() {
            searchAudio(this);
        });
    }

    function observeAudioUpload() {
        $(".voice-popup .single-upload .save").unbind("click").on("click", function() {
            addAudioFromUpload();
        });
    }

    function observeAudioMultiUpload() {
        $(".voice-popup .multi-upload .save").unbind("click").on("click", function() {
            addAudioFromMultiUpload();
        });
    }

    function observeMessageReply() {
        $(".im-page--chat-body").unbind("click", ".im-mess--reply").on("click", ".im-mess--reply", function() {
            replyMessage(this);
        });
    }

    async function run() {
        insertElements();
        await insertAudioList();
        insertSaveButtonOnLoad();
        insertSaveButtonOnUpdate();
        observeSendButton();
        observeSaveButton();
        observeCloseButton();
        observeAudioSend();
        observeAudioSave();
        observeAudioDelete();
        observeAudioTooltips();
        observeAudioSearch();
        observeAudioUpload();
        observeAudioMultiUpload();
        observeMessageReply();
    }

    run();
})();