И вновь я приветствую всех своих читателей! В своей прошлой статье я начал рассказывать про то, как я стал кадетом Глубины. Продолжение погружения на Глубину начнётся здесь и сейчас, поэтому настоятельно рекомендую всё‑таки обратиться к истокам, чтобы быть в курсе всех событий, которые произошли со мной за этот период.

Итак, погнали. Новоиспечённый кадет из меня хоть куда, но в таком статусе далеко не уедешь, поэтому я искал себе задание. Задание, которое будет мне по зубам и довольно интересным. Ведь я не хотел со старта получить выгорание и дёрганый глаз — это довольно грустная и частая ситуация, когда ребята на мощном энтузиазме хватаются за те проекты, что им пока не под силу. И тут я натыкаюсь на канал с многообещающим названием «Ведьмачьи контракты».

А задачек там… в общем есть из чего выбратьВыбираем задачку тут
А задачек там… в общем есть из чего выбрать
Выбираем задачку тут

А контрактов там... то есть интересных задач немало, можно было повыбирать. Бродил я среди всех этих предложений и вижу следующее: «импорт аудио из Телеграм в Дип». «Довольно любопытное дело», — подумал я и в моей голове сразу заработали шестерёнки, как и что я буду писать для бота. Меня даже не интересовало, когда и в каких объёмах мне поступит вознаграждение. Я увидел цель и проигнорировал всевозможные препятствия, которые могли поджидать меня.

Спустя буквально 5 минут, не успев моргнуть глазом, я уже получаю консультацию от Сонирута о том, что должен представлять из себя этот пакет и какие функции должен выполнять. Да, как я и думал, бот в Telegram... выкачка аудио... Звучит вполне логично и ожидаемо. Можно браться за работу. Но только тогда пришло осознание, что не всё так просто...

О самом ДипКейсе можно долго говорить и восхищаться красотой связейВосхищаемся тут
О самом ДипКейсе можно долго говорить и восхищаться красотой связей
Восхищаемся тут

Принцип работы пакета

Принцип работы пакета — баланс простоты и сложности. В моём понимании, проблем было не избежать. Если свести объяснения к минимуму, то сначала отправляется файл, Дип его качает и добавляет в пространство. Но глаза боятся, а руки делают, поэтому расскажу про всю последовательность своих действий.

В своей первой статье я упоминал о том, какую ценность несут в себе консультации. Так что без преувеличений отмечу, что Фокша оказал мне невероятную помощь на старте. Точнее сказать, он предоставил мне готовый пакет для Telegram.

Вот кстати этот пакет.Ознакамливаемся тут
Вот кстати этот пакет.
Ознакамливаемся тут

Конечно на тот момент мне не до конца было понятно, что я должен был сделать по итогу. Что за токены, причём тут 3050 порт и как ЭТО связать с ботом? Фокша не дремал и оперативно выдал все необходимые данные, которые мне очень помогли. Нам нужно было создать webhook, который будет реагировать на получение ботом сообщения, а затем собирал всю информацию о сообщении и создавал связи в пространстве.

Вот тогда пазл начал складываться, и мне стало гораздо проще и понятнее разобраться, что к чему. Из плюсов — я взялся за этот проект уже с базой необходимых знаний, полученных благодаря консультациям. Поэтому с определённой базой знаний не так уж и страшно в коде поковыряться, чтобы понять, как всё устроено.

А вот так выглядит мой пакетик во время принятия сообщений.
А вот так выглядит мой пакетик во время принятия сообщений.

А принцип работы следующий: после того, как пользователь отправляет боту сообщение, в пространстве начинают появляться связи: кто отправил, когда, содержание отправленного, на каком языке и так далее. Это основа, которая когда‑нибудь может кому‑нибудь понадобиться. Уж лучше данных будет больше, чем меньше. Лишнее всегда можно будет вырезать.

Тем не менее, этот пакет пока что не принимал аудио. С этим придётся повозиться. Если все данные о сообщении приходили просто как массив информации, то аудио мы получаем через метод Telegram API «getFile». При всём при этом, перед получением файла необходимо узнать его имя на сервере. А потом еще и скачать… А файл может быть и голосовым, и музыкой, и записью — чем угодно.

Вот оно, противостояние ogg против mp3. И тут на сцене появляется if. Простейшее, но вместе с тем гениальное решение. Либо аудио, либо голосовое.

Вы скорее всего догадались: я просто сделал две вариации с идентичным кодом. Только в одном случае для обычных записей, а второе для голосовых.

Код обработчика, который за это отвечает прикладываю сюда. Вдруг кому пригодится.

async (req, res, next, { deep, require, gql }) => {
    console.log(req.body);  // Выводит тело запроса в консоль
    const body = req.body;  // Сохраняет тело запроса в переменную body

    // Получает идентификаторы типов для различных элементов данных
    const typeIds = {
        contain: await deep.id("@deep-foundation/core", 'Contain'),
        updateId: await deep.id("@deep-foundation/telegram_save_audio", 'update_id'),
        message: await deep.id("@deep-foundation/telegram_save_audio", 'message'),
        messageId: await deep.id("@deep-foundation/telegram_save_audio", 'message_id'),
        from: await deep.id("@deep-foundation/telegram_save_audio", 'from'),
        chat: await deep.id("@deep-foundation/telegram_save_audio", 'chat'),
        id: await deep.id("@deep-foundation/telegram_save_audio", 'id'),
        isBot: await deep.id("@deep-foundation/telegram_save_audio", 'is_bot'),
        firstName: await deep.id("@deep-foundation/telegram_save_audio", 'first_name'),
        lastName: await deep.id("@deep-foundation/telegram_save_audio", 'last_name'),
        username: await deep.id("@deep-foundation/telegram_save_audio", 'username'),
        languageCode: await deep.id("@deep-foundation/telegram_save_audio", 'language_code'),
        type: await deep.id("@deep-foundation/telegram_save_audio", 'type'),
        date: await deep.id("@deep-foundation/telegram_save_audio", 'date'),
        text: await deep.id("@deep-foundation/telegram_save_audio", 'text'),
        audio: await deep.id("@deep-foundation/telegram_save_audio", 'Audio'),
        voice: await deep.id("@deep-foundation/telegram_save_audio", 'Voice'),
        duration: await deep.id("@deep-foundation/telegram_save_audio", 'Duration'),
        fileName: await deep.id("@deep-foundation/telegram_save_audio", 'File_name'),
        mimeType: await deep.id("@deep-foundation/telegram_save_audio", 'Mime_type'),
        title: await deep.id("@deep-foundation/telegram_save_audio", 'Title'),
        performer: await deep.id("@deep-foundation/telegram_save_audio", 'Performer'),
        fileId: await deep.id("@deep-foundation/telegram_save_audio", 'File_id'),
        fileUniqueId: await deep.id("@deep-foundation/telegram_save_audio", 'File_unique_id'),
        fileSize: await deep.id("@deep-foundation/telegram_save_audio", 'File_size'),
        telegram: await deep.id("@deep-foundation/telegram_save_audio", "Token"),
        insertLinksTo: await deep.id("@deep-foundation/telegram_save_audio", 'InsertLinksTo')
    };

    // Получает токен для связи с Telegram
    const { data: telegramLinkToken } = await deep.select({ type_id: typeIds.telegram });
    const [{ value: { value: tokenValue } }] = telegramLinkToken;

    // Выбирает ссылку для вставки
    const selectInsertLinksToLinkId = await deep.select({ type_id: typeIds.insertLinksTo });
    console.log(selectInsertLinksToLinkId);

    // Если ссылка для вставки не найдена, выбрасывает ошибку
    if (!selectInsertLinksToLinkId.data.length) {
        throw new Error(`${typeIds.insertLinksTo} link type not found`);
    }

    const { data: [{ id: insertLinksToLinkId, to_id: insertLinksToObjectLinkId }] } = selectInsertLinksToLinkId;

    // Вставляет сообщение в базу данных
    const { data: [{ id: messageLinkId }] } = await deep.insert({
        type_id: typeIds.message,
        in: {
            data: {
                type_id: typeIds.contain,
                from_id: insertLinksToObjectLinkId,
            },
        },
    });

    // Вставляет ID обновления в базу данных
    const { data: [{ id: updateIdLinkId }] } = await deep.insert({
        type_id: typeIds.updateId,
        number: { data: { value: body.update_id } },
        in: {
            data: {
                type_id: typeIds.contain,
                from_id: messageLinkId,
            },
        },
    });

    // Вставляет ID сообщения в базу данных
    await deep.insert({
        type_id: typeIds.messageId,
        number: { data: { value: body.message.message_id } },
        in: {
            data: {
                type_id: typeIds.contain,
                from_id: messageLinkId,
            },
        },
    });

    // Вставляет отправителя сообщения в базу данных
    const { data: [{ id: fromLinkId }] } = await deep.insert({
        type_id: typeIds.from,
        in: {
            data: {
                type_id: typeIds.contain,
                from_id: messageLinkId,
            },
        },
    });

    // Функция для вставки данных отправителя в базу данных
    async function insertData_from(items_from) {
        for (let i = 0; i < items_from.length; i++) {
            const { type_id, value, value_type } = items_from[i];

            await deep.insert({
                type_id,
                [value_type]: { data: { value } },
                in: {
                    data: {
                        type_id: typeIds.contain,
                        from_id: fromLinkId,
                    },
                },
            });
        }
    }

    // Данные отправителя
    const items_from = [
        { type_id: typeIds.id, value: body.message.from.id, value_type: 'number' },
        { type_id: typeIds.firstName, value: body.message.from.first_name, value_type: 'string' },
        { type_id: typeIds.lastName, value: body.message.from.last_name, value_type: 'string' },
        { type_id: typeIds.username, value: body.message.from.username, value_type: 'string' },
        { type_id: typeIds.languageCode, value: body.message.from.language_code, value_type: 'string' },
        { type_id: typeIds.isBot, value: +body.message.from.is_bot, value_type: 'number' },
    ];

    // Вставка данных отправителя в базу данных
    await insertData_from(items_from);

    // Вставка чата в базу данных
    const { data: [{ id: chatLinkId }] } = await deep.insert({
        type_id: typeIds.chat,
        in: {
            data: {
                type_id: typeIds.contain,
                from_id: messageLinkId,
            },
        },
    });

    // Функция для вставки данных чата в базу данных
    async function insertData_chat(items_chat) {
        for (let i = 0; i < items_chat.length; i++) {
            const { type_id, value, value_type } = items_chat[i];

            await deep.insert({
                type_id,
                [value_type]: { data: { value } },
                in: {
                    data: {
                        type_id: typeIds.contain,
                        from_id: chatLinkId,
                    },
                },
            });
        }
    }

    // Данные чата
    const items_chat = [
        { type_id: typeIds.id, value: body.message.chat.id, value_type: 'number' },
        { type_id: typeIds.firstName, value: body.message.chat.first_name, value_type: 'string' },
        { type_id: typeIds.lastName, value: body.message.chat.last_name, value_type: 'string' },
        { type_id: typeIds.username, value: body.message.chat.username, value_type: 'string' },
        { type_id: typeIds.type, value: body.message.chat.type, value_type: 'string' },
        { type_id: typeIds.date, value: body.message.date, value_type: 'number' },
        { type_id: typeIds.text, value: body.message.text, value_type: 'string' },
    ];

    // Вставка данных чата в базу данных
    await insertData_chat(items_chat);

    // Если в сообщении есть аудио
    if (body.message.audio) {
        // Вставка аудио в базу данных
        const { data: [{ id: audioLinkId }] } = await deep.insert({
            type_id: typeIds.audio,
            in: {
                data: {
                    type_id: typeIds.contain,
                    from_id: messageLinkId,
                },
            },
        });

        // Функция для вставки данных аудио в базу данных
        async function insertData(items) {
            for (let i = 0; i < items.length; i++) {
                const { type_id, value, value_type } = items[i];

                await deep.insert({
                    type_id,
                    [value_type]: { data: { value } },
                    in: {
                        data: {
                            type_id: typeIds.contain,
                            from_id: audioLinkId,
                        },
                    },
                });
            }
        }

        // Данные аудио
        const items = [
            { type_id: typeIds.duration, value: body.message.audio.duration, value_type: 'number' },
            { type_id: typeIds.mimeType, value: body.message.audio.mime_type, value_type: 'string' },
            { type_id: typeIds.fileId, value: body.message.audio.file_id, value_type: 'string'
            },
            { type_id: typeIds.fileUniqueId, value: body.message.audio.file_unique_id, value_type: 'string' },
            { type_id: typeIds.fileSize, value: body.message.audio.file_size, value_type: 'number' },
            { type_id: typeIds.fileName, value: body.message.audio.file_name, value_type: 'string' },
            { type_id: typeIds.title, value: body.message.audio.title, value_type: 'string' },
            { type_id: typeIds.performer, value: body.message.audio.performer, value_type: 'string' },
        ];

        // Вставка данных аудио в базу данных
        await insertData(items);

        // Получение файла аудио из Telegram
        const axios = require('axios');
        const FormData = require('form-data');
        const axiosInstance = axios.create({ maxBodyLength: Infinity });
        const getinfo_file = await axiosInstance.get("https://api.telegram.org/bot" + tokenValue + "/getFile?file_id=" + body.message.audio.file_id);
        const file = await axiosInstance.get("https://api.telegram.org/file/bot" + tokenValue + "/" + getinfo_file.data.result.file_path, {
            responseType: 'arraybuffer'
        });

        let buffer = Buffer.from(file.data);

        // Создание формы для отправки файла
        const formData = new FormData();
        formData.append('file', buffer, { filename: 'example.mp3', contentType: 'audio/mpeg' });
        const { data: [{ id }] } = await deep.insert({
            type_id: deep.idLocal('@deep-foundation/core', 'AsyncFile'),
            in: {
                data: [
                    {
                        type_id: deep.idLocal('@deep-foundation/core', 'Contain'),
                        from_id: audioLinkId,
                    },
                ]
            },
        });

        // Отправка файла на сервер
        const ssl = deep.apolloClient.ssl;
        const path = deep.apolloClient.path.slice(0, -4);
        const url = `${ssl ? "https://" : "http://"}${path}/file`;

        await axiosInstance.post(url, formData, {
            headers: {
                'linkId': id,
                "Authorization": `Bearer ${deep.token}`,
            },
        });
    } else if (body.message.voice) {
        // Если в сообщении есть голосовое сообщение
        const { data: [{ id: voiceLinkId }] } = await deep.insert({
            type_id: typeIds.voice,
            in: {
                data: {
                    type_id: typeIds.contain,
                    from_id: messageLinkId,
                },
            },
        });

        // Функция для вставки данных голосового сообщения в базу данных
        async function insertData(items) {
            for (let i = 0; i < items.length; i++) {
                const { type_id, value, value_type } = items[i];

                await deep.insert({
                    type_id,
                    [value_type]: { data: { value } },
                    in: {
                        data: {
                            type_id: typeIds.contain,
                            from_id: voiceLinkId,
                        },
                    },
                });
            }
        }

        // Данные голосового сообщения
        const items = [
            { type_id: typeIds.duration, value: body.message.voice.duration, value_type: 'number' },
            { type_id: typeIds.mimeType, value: body.message.voice.mime_type, value_type: 'string' },
            { type_id: typeIds.fileId, value: body.message.voice.file_id, value_type: 'string' },
            { type_id: typeIds.fileUniqueId, value: body.message.voice.file_unique_id, value_type: 'string' },
            { type_id: typeIds.fileSize, value: body.message.voice.file_size, value_type: 'number' },
        ];

        // Вставка данных голосового сообщения в базу данных
        await insertData(items);

        // Получение файла голосового сообщения из Telegram
        const axios = require('axios');
        const FormData = require('form-data');

        const axiosInstance = axios.create({ maxBodyLength: Infinity });
        const getinfo_file = await axiosInstance.get("https://api.telegram.org/bot" + tokenValue + "/getFile?file_id=" + body.message.voice.file_id);
        console.log(getinfo_file);
        const file = await axiosInstance.get("https://api.telegram.org/file/bot" + tokenValue + "/" + getinfo_file.data.result.file_path, {
            responseType: 'arraybuffer'
        });

        console.log(file);

        let buffer = Buffer.from(file.data);

        // Создание формы для отправки файла
        const formData = new FormData();
        formData.append('file', buffer, { filename: 'example.ogg', contentType: 'audio/ogg' });
        const { data: [{ id }] } = await deep.insert({
            type_id: deep.idLocal('@deep-foundation/core', 'AsyncFile'),
            in: {
                data: [
                    {
                        type_id: deep.idLocal('@deep-foundation/core', 'Contain'),
                        from_id: voiceLinkId,
                    },
                ]
            },
        });

        console.log('drop-zone id file', id, file);
        console.log('drop-zone formData', formData);

        // Отправка файла на сервер
        const ssl = deep.apolloClient.ssl;
        const path = deep.apolloClient.path.slice(0, -4);
        const url = `${ssl ? "https://" : "http://"}${path}/file`;

        await axiosInstance.post(url, formData, {
            headers: {
                'linkId': id,
                "Authorization": `Bearer ${deep.token}`,
            },
        });
    }

    res.send('ok');  // Отправляет ответ 'ok' на запрос
}

После получения аудио, Дип вытягивает всю информацию: вес, название, длительность, исполнитель, тип и имя файла в системе. Эта ссылка на файл в дальнейшем нам пригодится, поскольку она подставляется для получения файла, как говорилось ранее. И если мы воспользуемся обычным браузером, то по этой ссылке просто загрузится файл.

Стало быть, мы уже увидели финишную прямую, но вот только есть одно «но»: в Дип приходят файлы побитово. Вернее, приходят биты в виде текста, и всё это необходимо перезаписать как файл. Один бессознательный вечер, ещё одна консультация и да, теперь пакет создаёт файл. Вся система работает и радует глаз. Но у вас, читателей, может возникнуть резонный вопрос: а зачем так заморачиваться? Есть же много возможностей и более простых вариантов.

К примеру, можно использовать пакет в связке с другим пакетом, который преобразовывает речь в текст и пакетом ChatGPT. Или же использовать отправленные аудио в связке с собственной системой умного дома. На самом деле вариантов уйма, я согласен. И естественно, всё зависит от пользователя и поставленной задачи. Но мне не хотелось бы полагаться просто на теории и предположения, когда можно быть частью чего‑то большего и воплотить задумки в реальность. Совсем недавно SenchaPencha попросил меня обрезать пакет, оставив только сохранение аудио. Как оказалось, мой пакет являлся для него отправной точкой для какого‑то грандиозного плана.

Вот к этому я и клоню: не стоит далеко идти за примерами реиспользования. Всё, что мы производим на Глубине, пойдёт как инвестиция в технологичное и современное будущее — то маленький пакет или масштабный проект.

Вот так можно потыкаться в моем пакетике:

Пошаговая текстовая инструкция

  1. Запустить Deep в облаке GitPod

  2. Дождаться пребилда. После его окончания откроется 2 окна: 3007 и 4000 порты. Второй можно закрыть. Работать будем только в окне порта 3007(в случае, если браузер заблокировал октрытие 2 окон, то следует открыть 3007 самостоятельно - для этого стоит перейти в нижнюю панель Ports и нажать на ссылку напротив 3007 порта)

  3. Войти как Админ:

    1. Зажать правой кнопкой на связь с названием User. Появится круговое меню.

    2. Нужно ЛКМ выбрать traveler.

    3. После ЛКМ выбрать types+. Появится новые связи. Нам нужна с type: Type name: User

    4. По аналогии перейти в круговое меню и выбрать travelertyped+. Появится связь с name: Admin.

    5. Перейти в круговое меню связи Админа и нажать ЛКМ кнопку login

  4. Установить пакет telegram-save-audio

    1. Нажать на кнопку packager в правом верхнем углу экрана

    2. ввести telegram-save-audio

    3. перейти во вкладку Not Install

    4. выбрать нужный нам по названию и нажать install. Ожидаем установки и можно пользоваться пакетом.

    5. Появится связь с значком коробки и названием telegram-save-audio. Переместите ее в удобное вам место.

  5. Выдать пакету права админа

    1. Зажимаем ПКМ по пустому месту. Появляется круговое меню. Нужно создать связь.

    2. Выбрать insert. Появится окно ClientHandler.

    3. Выбрать в левом верхнем углу значок руки с глазом. Появится поле ввода.

    4. Ввести «join» и нажать ЛКМ по пункту со значком «рукопожатие» и названием Join

    5. Нажать на галочку в правом нижнем углу

    6. Зажать ЛКМ на связь telegram-save-audio и с зажатой ЛКМ провести до связи name: Admin. Отпустить ЛКМ.

    7. Появится связь name: Join.

  6. Создать связь Host

    1. По аналогии с созданием связи Join создаем связь с типом Host, но теперь один раз нажимаем ЛКМ на пустое пространство.

    2. Открыть круговое меню связи Host и выбрать editor

    3. Также нужно вернуться во вкладку gitpod. Перейти во вкладку с консолью внизу.

    4. Выбрать вкладку Ports

    5. Выбрать порт 3050 и в левой его части над параметром State нажать ПКМ на open(private).

    6. Выбрать open(public)

    7. Нажать на кнопку копирования 3050 порта.

    8. Вставить скопированное значение в редактор связи Host

    9. Ctrl+S и закрываем окно текстового редактора.

  7. Создать связь token

    1. По аналогии с пунктом с Host создаем связь token, но теперь вставляем в него значением токена вашего бота.

  8. Создать связь space и войти

    1. по аналогии с прошлыми пунктами создания связи. Обладает значком фиолетового стеклянного шара.

    2. Войти в круговое меню созданной связи и выбрать значение space. Вы появитесь в пустом пространстве с одной единственной связью space

  9. Создать связь InsertLinksTo по аналогии с созданием связи join, но теперь началом и концом связи является одна и также связь — space(при поиске в ClientHandler связь будет со значком звездочки)

  10. Открыть верхнее меню (3 стрелочки) и нажать на крестик напротив значения space. Мы вернемся обратно в Admin

  11. Теперь нужно создать связь SetWebHook (тоже со звездочкой) и провести ее по аналогии со связью join, но теперь от ранее созданной связи Host к другой нашей связи token

  12. Ваши сообщения боту будут появляться внутри связи space.

Видеоинструкция

  1. Войти в Deep.Case:

  1. Войти в Admin:

  1. Использовать пакетик:

Комментарии (2)


  1. jokeerrrrrrrrr
    10.04.2024 17:36
    +1

    Кинул 500 рублей на развитие проекта! Пришли?


  1. ris58h
    10.04.2024 17:36
    +2

    При чём здесь хаб Ненормальное программирование? Хорош спамить уже.