Voice Stealer

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();