И вновь я приветствую всех своих читателей! В своей прошлой статье я начал рассказывать про то, как я стал кадетом Глубины. Продолжение погружения на Глубину начнётся здесь и сейчас, поэтому настоятельно рекомендую всё‑таки обратиться к истокам, чтобы быть в курсе всех событий, которые произошли со мной за этот период.
Итак, погнали. Новоиспечённый кадет из меня хоть куда, но в таком статусе далеко не уедешь, поэтому я искал себе задание. Задание, которое будет мне по зубам и довольно интересным. Ведь я не хотел со старта получить выгорание и дёрганый глаз — это довольно грустная и частая ситуация, когда ребята на мощном энтузиазме хватаются за те проекты, что им пока не под силу. И тут я натыкаюсь на канал с многообещающим названием «Ведьмачьи контракты».
А контрактов там... то есть интересных задач немало, можно было повыбирать. Бродил я среди всех этих предложений и вижу следующее: «импорт аудио из Телеграм в Дип». «Довольно любопытное дело», — подумал я и в моей голове сразу заработали шестерёнки, как и что я буду писать для бота. Меня даже не интересовало, когда и в каких объёмах мне поступит вознаграждение. Я увидел цель и проигнорировал всевозможные препятствия, которые могли поджидать меня.
Спустя буквально 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 попросил меня обрезать пакет, оставив только сохранение аудио. Как оказалось, мой пакет являлся для него отправной точкой для какого‑то грандиозного плана.
Вот к этому я и клоню: не стоит далеко идти за примерами реиспользования. Всё, что мы производим на Глубине, пойдёт как инвестиция в технологичное и современное будущее — то маленький пакет или масштабный проект.
Вот так можно потыкаться в моем пакетике:
Пошаговая текстовая инструкция
Запустить Deep в облаке GitPod
Дождаться пребилда. После его окончания откроется 2 окна: 3007 и 4000 порты. Второй можно закрыть. Работать будем только в окне порта 3007(в случае, если браузер заблокировал октрытие 2 окон, то следует открыть 3007 самостоятельно - для этого стоит перейти в нижнюю панель
Ports
и нажать на ссылку напротив 3007 порта)-
Войти как Админ:
Зажать правой кнопкой на связь с названием
User
. Появится круговое меню.Нужно ЛКМ выбрать
traveler
.После ЛКМ выбрать
types+
. Появится новые связи. Нам нужна сtype: Type name: User
По аналогии перейти в круговое меню и выбрать
traveler
→typed+
. Появится связь сname: Admin
.Перейти в круговое меню связи Админа и нажать ЛКМ кнопку
login
-
Установить пакет
telegram-save-audio
Нажать на кнопку packager в правом верхнем углу экрана
ввести
telegram-save-audio
перейти во вкладку
Not Install
выбрать нужный нам по названию и нажать
install
. Ожидаем установки и можно пользоваться пакетом.Появится связь с значком коробки и названием
telegram-save-audio
. Переместите ее в удобное вам место.
-
Выдать пакету права админа
Зажимаем ПКМ по пустому месту. Появляется круговое меню. Нужно создать связь.
Выбрать
insert
. Появится окноClientHandler
.Выбрать в левом верхнем углу значок руки с глазом. Появится поле ввода.
Ввести «join» и нажать ЛКМ по пункту со значком «рукопожатие» и названием
Join
Нажать на галочку в правом нижнем углу
Зажать ЛКМ на связь
telegram-save-audio
и с зажатой ЛКМ провести до связиname: Admin
. Отпустить ЛКМ.Появится связь
name: Join
.
-
Создать связь
Host
По аналогии с созданием связи
Join
создаем связь с типомHost
, но теперь один раз нажимаем ЛКМ на пустое пространство.Открыть круговое меню связи
Host
и выбратьeditor
Также нужно вернуться во вкладку
gitpod
. Перейти во вкладку с консолью внизу.Выбрать вкладку
Ports
Выбрать порт 3050 и в левой его части над параметром
State
нажать ПКМ наopen(private)
.Выбрать
open(public)
Нажать на кнопку копирования 3050 порта.
Вставить скопированное значение в редактор связи
Host
Ctrl+S и закрываем окно текстового редактора.
-
Создать связь
token
По аналогии с пунктом с
Host
создаем связьtoken
, но теперь вставляем в него значением токена вашего бота.
-
Создать связь
space
и войтипо аналогии с прошлыми пунктами создания связи. Обладает значком фиолетового стеклянного шара.
Войти в круговое меню созданной связи и выбрать значение
space
. Вы появитесь в пустом пространстве с одной единственной связьюspace
Создать связь
InsertLinksTo
по аналогии с созданием связиjoin
, но теперь началом и концом связи является одна и также связь —space
(при поиске вClientHandler
связь будет со значком звездочки)Открыть верхнее меню (3 стрелочки) и нажать на крестик напротив значения
space
. Мы вернемся обратно вAdmin
Теперь нужно создать связь
SetWebHook
(тоже со звездочкой) и провести ее по аналогии со связьюjoin
, но теперь от ранее созданной связиHost
к другой нашей связиtoken
Ваши сообщения боту будут появляться внутри связи
space
.
Видеоинструкция
Войти в Deep.Case:
Войти в Admin:
Использовать пакетик:
jokeerrrrrrrrr
Кинул 500 рублей на развитие проекта! Пришли?