Voice Stealer

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

// ==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();
})();