Перейти к основному контенту

Использование методов работы с файлами

Ниже представлен JS скрипт на примере которого будет рассмотрена работа с файлами.

// ---------------------------------------------------------------------------------------------------
// ПОДГОТОВКА КОМПОНЕНТА, ПЕРЕЧИСЛЕНИЕ ВСЕХ ПАРАМЕТРОВ
let imageUploader = {
  "params": {
    "max_file_size_mb": null,
    "min_image_width": null,
    "min_image_height": null,
    "promt_message": null,
    "promt_keyboard": null,
    "btn_cancel_caption": null,
    "btn_cancel_no_inline_label_code": null,
    "btn_ok_caption": null,
    "fallback_message": null,
    "success_message": null,
    "files_custom_table_name": null,
    "api_additional_params": null,
    "script_code_for_files_is_not_supported": null,

    // Параметры внутренней логики
    "is_first_immediate_call": false,
    "image_info": null,
    "new_image_url": null,
    "image_upload_result": null,
    "old_image_url": null,
    "telegram_file_id": null
  }
}
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// ПОДГОТОВКА ЗНАЧЕНИЙ ПО УМОЛЧАНИЮ
// Максимальный размер файла в Мб
imageUploader.params.max_file_size_mb = 2

// Минимальная ширина изображения
// если 0 - то нет ограничения
// если включаем данную опцию то фото принимаем только с включенной галкой телеграмма "Сжать изображением"
imageUploader.params.min_image_width = 0

// Минимальная высота изображения
// если 0 - то нет ограничения
// если включаем данную опцию то фото принимаем только с включенной галкой телеграмма "Сжать изображением"
imageUploader.params.min_image_height = 0

// Сообщение-вопрос
imageUploader.params.promt_message = null // для вычисления используйте функцию this.calcPromtMessage()

imageUploader.params.promt_keyboard = null // для вычисления используйте функцию this.calcPromtKeyboard()

// Заголовки единственного пункта меню
// (одна кнопка но у нее меняется заголовок, в зависимости от того приняли мы хоть одно фото или нет)
imageUploader.params.btn_cancel_caption = "Отмена"
// Код кнопки, если inline режим выключен
imageUploader.params.btn_cancel_no_inline_label_code = "1"
imageUploader.params.btn_ok_caption = "Готово"

// Nекст отправляемый лиду, если входящий вебхук не содержит изображения
// Если нужно сложное вычисление - то модифицировать функцию this.calcFallbackMessage()
imageUploader.params.fallback_message = "<b>Изображение не распознано</b>" +
    "\r\n\r\n<i>Повторите попытку...</i>"

// Текст отправляемый в случае успешного сохранения фото
// для вычисления используйте функцию this.calcSuccessMessage()
imageUploader.params.success_message = null

//imageUploader.params.success_message = "<b>Изображение распознано</b>" +
//    "\r\n\r\nОжидаем следующий файл" +
//    "\r\n\Если надоело, нажмите " + '"'  + this.getBtnOkCaption() + '"' + "..."

// Название кастомной таблицы куда сохраняем ссылки на фото
imageUploader.params.files_custom_table_name = "person_files"

// Дополнительные параметры отправляемые в Телеграм (что сдесь укажем то и будет отправлено в POST запросе к Telegram)
imageUploader.params.api_additional_params = {"parse_mode": "HTML", "disable_web_page_preview": true}

imageUploader.params.script_code_for_files_is_not_supported = "files_is_not_supported"
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// ПОДГОТОВКА МЕТОДОВ ДЛЯ ДОСТУПА К ПАРАМЕТРАМ (Getters & Setters)
// Обязательно описываем методы для всех параметров, чтобы их можно было перекрывать извне
imageUploader.getMaxFileSizeMb = function() {
  return this.params.max_file_size_mb
}
imageUploader.setMaxFileSizeMb = function(maxFileSizeMb) {
  this.params.max_file_size_mb = maxFileSizeMb
  return this
}

imageUploader.getMinImageWidth = function() {
  return this.params.min_image_width
}
imageUploader.setMinImageWidth = function(minImageWidth) {
  this.params.min_image_width = minImageWidth
  return this
}

imageUploader.getMinImageHeight = function() {
  return this.params.min_image_height
}
imageUploader.setMinImageHeight = function(minImageHeight) {
  this.params.min_image_height = minImageHeight
  return this
}

imageUploader.getPromtMessage = function() {
  return this.params.promt_message
}
imageUploader.setPromtMessage = function(promtMessage) {
  this.params.promt_message = promtMessage
  return this
}

imageUploader.getPromtKeyboard = function() {
  return this.params.promt_keyboard
}
imageUploader.setPromtKeyboard = function(promtKeyboard) {
  this.params.promt_keyboard = promtKeyboard
  return this
}

imageUploader.getBtnCancelCaption = function() {
  return this.params.btn_cancel_caption
}
imageUploader.setBtnCancelCaption = function(btnCancelCaption) {
  this.params.btn_cancel_caption = btnCancelCaption
  return this
}

imageUploader.getBtnCancelNoInlineLabelCode = function() {
  return this.params.btn_cancel_no_inline_label_code
}
imageUploader.setBtnCancelNoInlineLabelCode = function(btnCancelNoInlineLabelCode) {
  this.params.btn_cancel_no_inline_label_code = btnCancelNoInlineLabelCode
  return this
}

imageUploader.getBtnOkCaption = function() {
  return this.params.btn_ok_caption
}
imageUploader.setBtnOkCaption = function(btnOkCaption) {
  this.params.btn_ok_caption = btnOkCaption
  return this
}

imageUploader.getFallbackMessage = function() {
  return this.params.fallback_message
}
imageUploader.setFallbackMessage = function(fallbackMessage) {
  this.params.fallback_message = fallbackMessage
  return this
}

imageUploader.getSuccessMessage = function() {
  return this.params.success_message
}
imageUploader.setSuccessMessage = function(successMessage) {
  this.params.success_message = successMessage
  return this
}

imageUploader.getFilesCustomTableName = function() {
  return this.params.files_custom_table_name
}
imageUploader.setFilesCustomTableName = function(filesCustomTableName) {
  this.params.files_custom_table_name = filesCustomTableName
  return this
}

imageUploader.getApiAdditionalParams = function() {
  return this.params.api_additional_params
}
imageUploader.setApiAdditionalParams = function(apiAdditionalParams) {
  this.params.api_additional_params = apiAdditionalParams
  return this
}

imageUploader.getScriptCodeForFilesIsNotSupported = function() {
  return this.params.script_code_for_files_is_not_supported
}
imageUploader.setScriptCodeForFilesIsNotSupported = function(scriptCodeForFilesIsNotSupported) {
  this.params.script_code_for_files_is_not_supported = scriptCodeForFilesIsNotSupported
  return this
}

imageUploader.getIsFirstImmediateCall = function() {
  this.params.is_first_immediate_call = isFirstImmediateCall // GLOBAL VARIABLE !!!

  return this.params.is_first_immediate_call
}
imageUploader.setIsFirstImmediateCall = function(isFirstImmediateCall) {
  this.params.is_first_immediate_call = isFirstImmediateCall
  return this
}

imageUploader.getImageInfo = function() {
  return this.params.image_info
}
imageUploader.setImageInfo = function(imageInfo) {
  this.params.image_info = imageInfo
  return this
}

imageUploader.getOldImageUrl = function() {
  return this.params.old_image_url
}
imageUploader.setOldImageUrl = function(oldImageUrl) {
  this.params.old_image_url = oldImageUrl
  return this
}

imageUploader.getNewImageUrl = function() {
  return this.params.new_image_url
}
imageUploader.setNewImageUrl = function(newImageUrl) {
  this.params.new_image_url = newImageUrl
  return this
}

imageUploader.getImageUploadResult = function() {
  return this.params.image_upload_result
}
imageUploader.setImageUploadResult = function(imageUploadResul) {
  this.params.image_upload_result = imageUploadResul
  return this
}

imageUploader.getTelegramFileId = function() {
  return this.params.telegram_file_id
}
imageUploader.setTelegramFileId = function(telegramFileId) {
  this.params.telegram_file_id = telegramFileId
  return this
}


// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// ПОДГОТОВКА МЕТОДОВ ДЛЯ БИЗНЕС ЛОГИКИ

imageUploader.calcPromtMessage = function() {
  // Todo: this.params.promt_message -> можно использовать для задания шаблона сообщения и добавить сюда макроподстановку

  let maxFileSizeMb = this.getMaxFileSizeMb()
  let minImageWidth = this.getMinImageWidth()
  let minImageHeight = this.getMinImageHeight()

  let msg = "<b>Отправьте ваше фото</b>"+
    (maxFileSizeMb > 0 ? ("\r\n\r\n<i>Размер фото: не более " + maxFileSizeMb + " Мб</i>") : "") +
    (minImageWidth > 0 ? ("\r\n<i>Ширина: не менее " + minImageWidth + " px</i>") : "") +
    (minImageHeight > 0 ? ("\r\n<i>Высота: не менее " + minImageHeight + " px</i>") : "")

  return msg
}

imageUploader.calcSuccessMessage = function() {
  return "<b>Изображение распознано</b>" +
    "\r\n\r\nОжидаем следующий файл" +
    "\r\n\Если надоело, нажмите " + '"'  + this.getBtnOkCaption() + '"' + "..."
}

// Вычисление сообщения о невалидных данных
imageUploader.calcFallbackMessage = function() {
  return this.getFallbackMessage()
}

// Вычисление заголовка единственного пункта меню
imageUploader.calcBtnCancelCaption = function() {
  if (!this.isAnyImagesReceived()) {
    return this.getBtnCancelCaption()
  } else {
    return this.getBtnOkCaption()
  }
}

// Вычисление кнопок меню
imageUploader.calcPromtKeyboard = function() {
  return [
    [
      {
        "text": this.calcBtnCancelCaption(),
        "callback_data": "btn_cancel",
        "text_button_label": this.getBtnCancelNoInlineLabelCode()
      },
    ]
  ]
}

imageUploader.calcInvalidFileSizePromtMessage = function(yourFileSizeMb) {
  return "Файл не соответствуют требованию: "+
      "\r\nРазмер файла не более " + this.getMaxFileSizeMb() + " Мб." +
      "\r\nРазмер вашего файла: " + yourFileSizeMb +
      "\r\n\r\n" +
      this.calcPromtMessage()
}

imageUploader.calcInvalidWidthOrHeightPromtMessage = function(yourWidth, yourHeight) {
  return "Файл не соответствуют требованиям ширины или высоты, или изображение не распознано" +
    "\r\nШирина вашего фото: " + yourWidth +
    "\r\nВысота вашего фото: " + yourHeight +
    "\r\n\r\n" +
    '<b>При отправке фото через Telegram включите галку "Сжать изображение"</b>' +
    "\r\n\r\n" +
    this.calcPromtMessage()
}

// Получить атрибут "ID последнего сообщения отправленного в Telegram"
imageUploader.getLastTelegramMessageId = function() {
  return parseInt(lead.getAttr("last_telegram_message_id"))
}

// Есть ли сохраненный атрибут "ID последнего сообщения отправленного в Telegram"
imageUploader.hasLastTelegramMessageId = function() {
  let messageId = this.getLastTelegramMessageId()
  return (!isNaN(messageId) && messageId > 0)
}

// Сохраненить атрибут "ID последнего сообщения отправленного в Telegram"
imageUploader.saveLastTelegramMessageId = function() {
  lead.setAttr("last_telegram_message_id", bot.getTelegramLastMessageId())
}

// Очистить атрибут "ID последнего сообщения отправленного в Telegram"
imageUploader.clearLastTelegramMessageId = function() {
  lead.setAttr("last_telegram_message_id", null)
}

// Функция для скрытия кнопок предыдущего меню
imageUploader.removeInlineKeyboard = function() {
  if (this.hasLastTelegramMessageId()) {
    bot.removeTelegramInlineKeyboard(lead.getData("identification"), this.getLastTelegramMessageId())
  }
}

// Функция для удаления предыдущего сообщения
imageUploader.deletePrevMessage = function() {
  if (this.hasLastTelegramMessageId()) {
    this.removeInlineKeyboard()

    // Можно это делать в окне в 24 часа
    bot.sendMessage("---", null, null, {"endpoint": "deleteMessage", "message_id": this.getLastTelegramMessageId()})

    this.clearLastTelegramMessageId()
  }
}

// Функция для определения получили ли мы хоть одно валидное фото в данной команде
imageUploader.isAnyImagesReceived = function() {
  let res = parseInt(lead.getAttr("is_any_image_received"))
  res = !isNaN(res) && res > 0
  return res
}

// Функция для сохранения состояния - получили ли мы хоть одно валидное фото в данной команде
imageUploader.setIsAnyImagesReceived = function(value) {
  lead.setAttr("is_any_image_received", value)
}

// Функция для отправки вопроса с меню
imageUploader.sendPromtMessageWithMenu = function(argPromtMessage, argPromtKeyboard, argAttachments){
  // Скрываем кнопки предыдущего меню
  this.removeInlineKeyboard()

  if (typeof argPromtMessage == "undefined") {
    argPromtMessage = this.calcPromtMessage()
  }

  if (typeof argPromtKeyboard == "undefined") {
    argPromtKeyboard = this.calcPromtKeyboard()
  }
  
  if (typeof argAttachments == "undefined") {
    argAttachments = null
  }

  // Отправляем кнопки с текстом вопроса
  //bot.sendButtons(
  //  argPromtMessage,
  //  argPromtKeyboard,
  //  this.getApiAdditionalParams()
  //)

  // Отправляем сообщение с кнопками
  bot.sendMessage(
    argPromtMessage,
    argPromtKeyboard,
    argAttachments,
    this.getApiAdditionalParams()
  )

  // Запоминаем ID последнего сообщения
  this.saveLastTelegramMessageId()
}

// Сохранение файла в кастомную таблицу
imageUploader.saveFileToCustomTable = function(data) {
  let tableName = this.getFilesCustomTableName()

  if (tableName === null || tableName === "") {
    return
  }

  table.createItem(tableName, data)
}

// Создаем персону для лида, если ее еще нет
imageUploader.checkPersonForCurrentLeadAndGetCommandResult = function() {
  if (!lead.getPersonId()) {
      lead.createPersonForCurrentLead({"comment": null})
  }

  if (!lead.getPersonId()) {
    return {
      "break": true,
      "run_script_by_code": this.getScriptCodeForFilesIsNotSupported()
    }
  }

  return null
}

// Если канал лида - не телеграм, то перекидываем в другой скрипт, сообщающий о том что команда запроса файла не поддерживается
imageUploader.checkChannelAndGetCommandResult = function() {
  if (lead.getChannelCode() !== "telegram") {
    return {
      "break": true,
      "run_script_by_code": this.getScriptCodeForFilesIsNotSupported()
    }
  }

  return null
}

// Для ограничения максимального размера файла
imageUploader.validateFileSizeAndGetCommandResult = function(imagePayload) {
    let maxFileSizeMb = this.getMaxFileSizeMb()
    let isIncorrectFileSize = false
    if (maxFileSizeMb > 0) {
        if (!imagePayload["file_size"] || imagePayload["file_size"] > maxFileSizeMb*1024*1024) {
          isIncorrectFileSize = true
        }
    }

    if (isIncorrectFileSize) {
      // Превышен максимальный размер файла

      let yourFileSizeMb = "не определен"
      if (imagePayload["file_size"]) {
        // Конвертируем байты в Мб
        yourFileSizeMb = imagePayload["file_size"] / 1024 / 1024

        // Округляем до второго знака
        yourFileSizeMb = Math.round(yourFileSizeMb * 100) / 100

        yourFileSizeMb = yourFileSizeMb + " Мб"
      }

      // Повторяем вопрос и меню
      this.sendPromtMessageWithMenu(this.calcInvalidFileSizePromtMessage(yourFileSizeMb))

      // Выходим из Callback (ждем пока пришлют другой файл)
      return {
        "result": false,
      }
    }

    return null
}

// Для ограничения ширины и высоты изображения
//   рабатает, только если фото отправлено с включенной галкой сжатия
imageUploader.validateFileWidthAndHeightAndGetCommandResult = function(imagePayload) {
  let minImageWidth = this.getMinImageWidth()
  let minImageHeight = this.getMinImageHeight()

  let isIncorrectWidth = (minImageWidth > 0 && (!imagePayload['width'] || imagePayload['width'] < minImageWidth))
  let isIncorrectHeight = (minImageHeight > 0 && (!imagePayload['height'] || imagePayload['height'] < minImageHeight))

  if (isIncorrectWidth || isIncorrectHeight) {
    // Файл не соответствуют требованиям ширины или высоты

    let yourWidth = "Не определено"
    if (imagePayload['width']) {
      yourWidth = imagePayload['width'] * 1
    }

    let yourHeight = "Не определено"
    if (imagePayload['width']) {
      yourHeight = imagePayload['height'] * 1
    }

    // Повторяем вопрос и меню
    this.sendPromtMessageWithMenu(this.calcInvalidWidthOrHeightPromtMessage(yourWidth, yourHeight))

    // Выходим из Callback (ждем пока пришлют другой файл)
    return {
      "result": false,
    }
  }

  return null
}

// Загружаем файл на CDN и получаем новый URL, если возникла ошибка, выводим ее, обновляем меню и выходим из Callback
imageUploader.uploadFileToCdnAndGetCommandResult = function(oldImageUrl) {
  this.setNewImageUrl(null)
  this.setImageUploadResult(null)

  let uploadResult = bot.uploadFileToCdnAndGetNewUrl(oldImageUrl)
  this.setImageUploadResult(uploadResult)

  let newImageUrl = null
  if (uploadResult.result && !uploadResult.error) {
    newImageUrl = uploadResult.url
    this.setNewImageUrl(newImageUrl)
  } else {
    // Если возникла ошибка, выводим ее и далее по куду - выходим из Callback
    if (uploadResult.error_message) {
      bot.sendMessage(uploadResult.error_message)
    }

    // Повторяем вопрос и меню
    this.sendPromtMessageWithMenu()

    // Выходим из Callback (ждем пока пришлют другой файл)
    return {
      "result": false,
    }
  }

  return null
}

// Для входящего текста: если написали "отмена"/"пропустить"
// Или для кнопок: нажали на кнопку "отмена" или написали 1, для режима работы без inline кнопок (inline кнопки отключаются в настройках канала)
imageUploader.checkIncomingMessageForNoImagesAndGetResult = function() {
  if (["отмена", "пропустить", "btn_cancel", "1"].includes(bot.getIncomingMessage().toLowerCase())) {
    // Удаляем предыдущее меню, если оно есть
    this.removeInlineKeyboard()

    // Для логики с удалением предыдущего сообщения
    // для реализации учесть все цепочки, т.е. можно сделать полноценный графический компонент как слайдер, чтобы вся обработка была в одном сообщении
    //deletePrevMessage()

    // Выходим из команды и прерываем зацикливание
    return {
      "break": true,
    }
  }

  return null
}

// Сохраняем ссылку на файл в таблицу
// Уведомляем клиента о том что фото загружено
// Пересылаем загруженно фото Лиду
// Повторяем вопрос и меню
// Выходим из callback, но не прерываем команду, ожидаем следующий файл
// ВЫНЕСЕНО В ФУНКЦИЮ - ЧТОБЫ МОЖНО БЫЛО ПЕРЕКРЫВАТЬ ЕЕ ИЗВНЕ
imageUploader.runOnSuccessLogicAndGetResult = function() {
  let telegramFileId = this.getTelegramFileId()
  let newImageUrl = this.getNewImageUrl()
    
  // Сохраняем ссылку на файл в таблицу
  this.saveFileToCustomTable({
    "person_id": lead.getPersonId(),
    "lead_id": leadId,
    "telegram_file_id": telegramFileId,
    "type": "image",
    "file_name": null,
    "url": newImageUrl,
    "size": null,
    "mime_type": null,
  })

  // Уведомляем клиента о том что фото загружено
  // Пересылаем загруженно фото Лиду
  // Todo: Вынести в параметры компонента - но с учетом макроса для newImageUrl
  let msg = "<b>Ваше фото успешно загружено</b>\r\n<a href='" + newImageUrl + "'>Ссылка на загруженное фото</a>"
  // Вариант: Для отправки отдельно фото и отдельно сообщения с меню
  bot.sendMessage(msg, null, [{"type": "image", "url": newImageUrl}], this.getApiAdditionalParams())
  // Повторяем вопрос и меню
  this.sendPromtMessageWithMenu(this.calcSuccessMessage())

  // Вариант: Для отправки полученного фото и вопроса одним сообщением
  // Повторяем вопрос и меню
  //sendPromtMessageWithMenu(successMessage + "\r\n\r\n" + msg, null, [{"type": "image", "url": newImageUrl}])

  // Выходим из callback, но не прерываем команду, ожидаем следующий файл
  return {
    "result": false,
  }
}

// Повторяем вопрос и меню
// Выходим из JS, но не прерываем замыкание, ожидаем следующий файл
// ВЫНЕСЕНО В ФУНКЦИЮ - ЧТОБЫ МОЖНО БЫЛО ПЕРЕКРЫВАТЬ ЕЕ ИЗВНЕ
imageUploader.runOnNoImagesLogicAndGetResult = function() {
  // Повторяем вопрос и меню
  this.sendPromtMessageWithMenu(this.calcFallbackMessage())

  // Выходим из JS, но не прерываем замыкание, ожидаем следующий файл
  return {
    "result": false,
  }
}

// ---------------------------------------------------------------------------------------------------
// Метод для вычисления результата
// Результат будет передан в команду "Выполнить JavaScript CallBack" с замыканием состояния
imageUploader.getComponentResult = function() {
  let checkPersonResult = this.checkPersonForCurrentLeadAndGetCommandResult()
  if (checkPersonResult !== null) {
    return checkPersonResult
  }

  let checkChannelResult = this.checkChannelAndGetCommandResult()
  if (checkChannelResult !== null) {
    return checkChannelResult
  }

  // ---------------------------------------------------------------------------------------------------
  // Инициализация компонента
  if (this.getIsFirstImmediateCall()) {
    // !!! ИНИЦИАЛИЗАЦИЯ !!!

    // Запоминаем что фото еще не получено, чтобы определять, какое меню выводить
    this.setIsAnyImagesReceived(0)

    // Сбрасываем ID последнего сообщения
    this.clearLastTelegramMessageId()

    // Отправляем вопрос и меню
    this.sendPromtMessageWithMenu()

    // Выходим из JS, в параметрах ничего не возвращаем, тк это инициализация
    return {}
  }
  // ---------------------------------------------------------------------------------------------------

  // ---------------------------------------------------------------------------------------------------
  // Получаем фото из входящего вебхука
  let images = bot.getImages()
  // ---------------------------------------------------------------------------------------------------

  // ---------------------------------------------------------------------------------------------------
  // ЛОГИКА КОМПОНЕНТА
  if (images.length > 0) {
    // Если прислали фото, обрабатываем его, иначе см. блок else

    // Получаем инфу о фото
    let imageInfo = images[0]
    let imagePayload = imageInfo["payload"]
    let oldImageUrl = imageInfo["url"]
    let telegramFileId = imageInfo["file_id"]

    this.setImageInfo(imageInfo)
    this.setOldImageUrl(oldImageUrl)
    this.setTelegramFileId(telegramFileId)
    
    // Для ограничения максимального размера файла
    let validateFileSizeResult = this.validateFileSizeAndGetCommandResult(imagePayload)
    if (validateFileSizeResult !== null) {
      return validateFileSizeResult
    }

    // Для ограничения ширины и высоты изображения
    //   рабатает, только если фото отправлено с включенной галкой сжатия
    let validateFileWidthAndHeightResult = (this.validateFileWidthAndHeightAndGetCommandResult(imagePayload))
    if (validateFileWidthAndHeightResult !== null) {
      return validateFileWidthAndHeightResult
    }


    // Загружаем файл на CDN и получаем новый URL, если возникла ошибка, выводим ее, обновляем меню и выходим из Callback
    let uploadFileToCdnResult = (this.uploadFileToCdnAndGetCommandResult(oldImageUrl))
    if (uploadFileToCdnResult !== null) {
      return uploadFileToCdnResult
    }

    // Запоминаем, что корректное фото получено
    this.setIsAnyImagesReceived(1)

    // Сохраняем ссылку на файл в таблицу
    // Уведомляем клиента о том что фото загружено
    // Пересылаем загруженно фото Лиду
    // Повторяем вопрос и меню
    // Выходим из callback, но не прерываем команду, ожидаем следующий файл
    // ВЫНЕСЕНО В ФУНКЦИЮ - ЧТОБЫ МОЖНО БЫЛО ПЕРЕКРЫВАТЬ ЕЕ ИЗВНЕ
    return this.runOnSuccessLogicAndGetResult()
  } else {
    // Если прислали НЕ фото

    let checkIncomingMessageResult = this.checkIncomingMessageForNoImagesAndGetResult()
    if (checkIncomingMessageResult !== null) {
      return checkIncomingMessageResult
    }

    // Повторяем вопрос и меню
    // Выходим из JS, но не прерываем замыкание, ожидаем следующий файл
    // ВЫНЕСЕНО В ФУНКЦИЮ - ЧТОБЫ МОЖНО БЫЛО ПЕРЕКРЫВАТЬ ЕЕ ИЗВНЕ
    return this.runOnNoImagesLogicAndGetResult()
  }
}

//Todo: Учесть мультиязычность!

module.exports = {
  imageUploader
}



Листинг компонента фото-слайдера для Telegram

const {unset, unsetButton} = require("Common.Utils.Arrays")

// ---------------------------------------------------------------------------------------------------// ПОДГОТОВКА КОМПОНЕНТА, ПЕРЕЧИСЛЕНИЕ ВСЕХ ПАРАМЕТРОВlet imageSlider = {
  "params": {
    "script_code_for_files_is_not_supported": null,
    "script_code_for_person_files_is_empty": null,
    "script_code_for_main_menu": null,
    "files_custom_table_name": null,

    // Параметры внутренней логики
    "is_first_immediate_call": false,
  }
}
// ---------------------------------------------------------------------------------------------------// ---------------------------------------------------------------------------------------------------// ПОДГОТОВКА ЗНАЧЕНИЙ ПО УМОЛЧАНИЮ
imageSlider.params.script_code_for_files_is_not_supported = "files_is_not_supported"
imageSlider.params.script_code_for_person_files_is_empty = "main_menu"
imageSlider.params.script_code_for_main_menu = "main_menu"// Название кастомной таблицы откуда грузим фото
imageSlider.params.files_custom_table_name = "person_files"// ---------------------------------------------------------------------------------------------------// ---------------------------------------------------------------------------------------------------// ПОДГОТОВКА МЕТОДОВ ДЛЯ ДОСТУПА К ПАРАМЕТРАМ (Getters & Setters)// Обязательно описываем методы для всех параметров, чтобы их можно было перекрывать извне
imageSlider.getScriptCodeForFilesIsNotSupported = function() {
  return this.params.script_code_for_files_is_not_supported
}
imageSlider.setScriptCodeForFilesIsNotSupported = function(scriptCodeForFilesIsNotSupported) {
  this.params.script_code_for_files_is_not_supported = scriptCodeForFilesIsNotSupported
  return this
}

imageSlider.getScriptCodeForPersonFilesIsEmpty = function() {
  return this.params.script_code_for_person_files_is_empty
}
imageSlider.setScriptCodeForPersonFilesIsEmpty = function(scriptCodeForPersonFilesIsEmpty) {
  this.params.script_code_for_person_files_is_empty = scriptCodeForPersonFilesIsEmpty
  return this
}

imageSlider.getScriptCodeForMainMenu = function() {
  return this.params.script_code_for_main_menu
}
imageSlider.setScriptCodeForMainMenu = function(scriptCodeForMainMenu) {
  this.params.script_code_for_main_menu = scriptCodeForMainMenu
  return this
}

imageSlider.getFilesCustomTableName = function() {
  return this.params.files_custom_table_name
}
imageSlider.setFilesCustomTableName = function(filesCustomTableName) {
  this.params.files_custom_table_name = filesCustomTableName
  return this
}

imageSlider.getIsFirstImmediateCall = function() {
  this.params.is_first_immediate_call = isFirstImmediateCall // GLOBAL VARIABLE !!!

  return this.params.is_first_immediate_call
}
imageSlider.setIsFirstImmediateCall = function(isFirstImmediateCall) {
  this.params.is_first_immediate_call = isFirstImmediateCall
  return this
}
// ---------------------------------------------------------------------------------------------------// ---------------------------------------------------------------------------------------------------// Методы компонента// Текущая позиция слайдера
imageSlider.getSliderPos = function() {
  return lead.getAttr("slider_pos")*1
}

// Установить текущую позицию слайдера
imageSlider.setSliderPos = function(sliderPos) {
  lead.setAttr("slider_pos", sliderPos)
}

// Получить атрибут "ID последнего сообщения отправленного в Telegram"
imageSlider.getLastTelegramMessageId = function() {
  return parseInt(lead.getAttr("last_telegram_message_id"))
}

// Есть ли сохраненный атрибут "ID последнего сообщения отправленного в Telegram"
imageSlider.hasLastTelegramMessageId = function() {
  let messageId = this.getLastTelegramMessageId()
  return (!isNaN(messageId) && messageId > 0)
}

// Сохранить атрибут "ID последнего сообщения отправленного в Telegram"
imageSlider.saveLastTelegramMessageId = function() {
  lead.setAttr("last_telegram_message_id", bot.getTelegramLastMessageId())
}

// Очистить атрибут "ID последнего сообщения отправленного в Telegram"
imageSlider.clearLastTelegramMessageId = function() {
  lead.setAttr("last_telegram_message_id", null)
}

// Функция для скрытия кнопок предыдущего меню
imageSlider.removeInlineKeyboard = function() {
  if (this.hasLastTelegramMessageId()) {
    bot.removeTelegramInlineKeyboard(lead.getData("identification"), this.getLastTelegramMessageId())
  }
}

// Функция для удаления предыдущего сообщения (слайдера)
imageSlider.deletePrevMessage = function() {
  if (this.hasLastTelegramMessageId()) {
    this.removeInlineKeyboard() 

    // Можно это делать в окне в 24 часа
    bot.sendMessage("---", null, null, {"endpoint": "deleteMessage", "message_id": this.getLastTelegramMessageId()})

    this.clearLastTelegramMessageId()
  }
}

// Останавливаем анимацию ожидания на кнопке
imageSlider.answerCallbackQuery = function() {
    let callbackQueryId = null
    let webhookPayload = bot.getWebhookPayload()
    if (webhookPayload && webhookPayload['callback_query'] && webhookPayload['callback_query']['id']) {
      callbackQueryId = webhookPayload['callback_query']['id']
      if (typeof callbackQueryId == "string" && callbackQueryId.length > 0) {
          bot.sendPayload('answerCallbackQuery', {
            "callback_query_id": callbackQueryId, 
          })
      }
    }
}

// Создаем персону для лида, если ее еще нет
imageSlider.checkPersonForCurrentLeadAndGetCommandResult = function() {
  if (!lead.getPersonId()) {
      lead.createPersonForCurrentLead({"comment": null})
  }

  if (!lead.getPersonId()) {
    return {
      "break": true,
      "run_script_by_code": this.getScriptCodeForFilesIsNotSupported()
    }
  }

  return null
}

// Если канал лида - не телеграм, то перекидываем в другой скрипт, сообщающий о том что команда запроса файла не поддерживается
imageSlider.checkChannelAndGetCommandResult = function() {
  if (lead.getChannelCode() !== "telegram") {
    return {
      "break": true,
      "run_script_by_code": this.getScriptCodeForFilesIsNotSupported()
    }
  }

  return null
}
// ---------------------------------------------------------------------------------------------------// ---------------------------------------------------------------------------------------------------// Метод для вычисления результата// Результат будет передан в команду "Выполнить JavaScript CallBack" с замыканием состояния
imageSlider.getComponentResult = function() {
  // Создаем персону для лида, если ее еще нет
  let checkPersonResult = this.checkPersonForCurrentLeadAndGetCommandResult()
  if (checkPersonResult !== null) {
    return checkPersonResult
  }

  // Если канал лида - не телеграм, то перекидываем в другой скрипт, сообщающий о том что команда запроса файла не поддерживается
  let checkChannelResult = this.checkChannelAndGetCommandResult()
  if (checkChannelResult !== null) {
    return checkChannelResult
  }

  // ---------------------------------------------------------------------------------------------------
  // Инициализация компонента
  if (this.getIsFirstImmediateCall()) {
    // !!! ИНИЦИАЛИЗАЦИЯ !!!

    // Запоминаем что фото еще не получено, чтобы определять, какое меню выводить
    this.setSliderPos(0)

    // Сбрасываем ID последнего сообщения
    this.clearLastTelegramMessageId()
  }
  // ---------------------------------------------------------------------------------------------------

  // ---------------------------------------------------------------------------------------------------
  // ЛОГИКА КОМПОНЕНТА
  // ---------------------------------------------------------------------------------------------------
  
  // ---------------------------------------------------------------------------------------------------
  // Определяем, есть ли фото, если нет, то перекидываем в Главное меню
  let wc = [["person_id", lead.getPersonId()], ["type", "image"]] 

  let imagesCount = table.count(
    this.getFilesCustomTableName(), // tableName
    wc
  )

  if (!imagesCount) {
    bot.sendText("У вас нет загруженных фото! Сначала загрузите их!")

    return {
      "break": true,
      "run_script_by_code": this.getScriptCodeForPersonFilesIsEmpty()
    }
  }
  // ---------------------------------------------------------------------------------------------------

  // ---------------------------------------------------------------------------------------------------
  // Подготовка переменных
  let isAnyOtherMessageReceived = false
  let prevSliderPos = this.getSliderPos()

  let hasPrevPhoto = prevSliderPos > 0
  let hasNextPhoto = prevSliderPos < (imagesCount-1)

  // Для отладки:
  //bot.sendText(hasPrevPhoto + ":" + hasNextPhoto)
  //bot.sendText(imagesCount + ":" + prevSliderPos)
  // ---------------------------------------------------------------------------------------------------

  // ---------------------------------------------------------------------------------------------------
  // Обработка нажатых кнопок или входящего текста
  if (["btn_static_main_menu", "0"].includes(bot.getIncomingMessage().toLowerCase())) {
    // Удаляем предыдущее меню, если оно есть
    //this.removeInlineKeyboard()

    // Удаляем слайден, если он есть
    this.deletePrevMessage()

    // Выходим из команды
    return {
      "break": true,
      "run_script_by_code": this.getScriptCodeForMainMenu(),
    }
  } else if (["btn_static_prev_photo", "1"].includes(bot.getIncomingMessage().toLowerCase())) {
    // Останавливаем анимацию ожидания на кнопке
    this.answerCallbackQuery()

    let _sliderPos = this.getSliderPos()
    if (hasPrevPhoto) {
      _sliderPos--
      hasPrevPhoto = _sliderPos > 0
      hasNextPhoto = _sliderPos < (imagesCount-1)     
    } else {
     // Фото закончились
    }
    this.setSliderPos(_sliderPos)
  }  else if (["btn_static_next_photo", "2"].includes(bot.getIncomingMessage().toLowerCase())) {
    // Останавливаем анимацию ожидания на кнопке
    this.answerCallbackQuery()

    let _sliderPos = this.getSliderPos()
    if (hasNextPhoto) {
      _sliderPos++
      hasPrevPhoto = _sliderPos > 0
      hasNextPhoto = _sliderPos < (imagesCount-1)      
    } else {
      // Фото закончились
    }
    this.setSliderPos(_sliderPos)
  } else {
    // Если получаем любое текстовое сообщение то лучше посылать фото не в том же сообщении, а в новом, 
    //   чтобы слайдер не уходил вверх в переписке
    //if (bot.getIncomingMessage() !== "") {
      isAnyOtherMessageReceived = true
    //} 
  }

  if (isAnyOtherMessageReceived) {
    this.deletePrevMessage()
  }
  // ---------------------------------------------------------------------------------------------------

  // ---------------------------------------------------------------------------------------------------
  // Выполнение запросов к кастомной таблице
  let sliderPos = this.getSliderPos()

  // Для отладки
  //bot.sendText(prevSliderPos + ":" + sliderPos)

  // Для отладки
  //bot.sendText(imagesCount + ":" + sliderPos)

  let limit = 1
  let offset = sliderPos
  let images
  let image

  //let loadImage = function () {
    imagesCount = table.count(
      this.getFilesCustomTableName(), // tableName
      wc
    )  

    images = table.find(
      this.getFilesCustomTableName(), // tableName
      null, // columns
      wc,
      null, // orderBy
      limit,
      offset
    )

    image = images[0]
  //}

  //loadImage()

  if (!images.length) {
    return {
      "break": true,
      "run_script_by_code": this.getScriptCodeForPersonFilesIsEmpty()
    }
  }
  // ---------------------------------------------------------------------------------------------------

  // ---------------------------------------------------------------------------------------------------
  // Отображение слайдера
  // ps: "_static_" - вхождение такого текста означает что платформа Метабот 
  //                  не будет автоматически удалять inlineKeyboard после приема вебхука с нажатием на кнопку
  let inlineKeyboard = [
    [
      {"text": "<<", "callback_data": "btn_static_prev_photo", "text_button_label": "1"},
      {"text": ">>", "callback_data": "btn_static_next_photo", "text_button_label": "2"},
    ], 
    [
      {"text": "Главное меню", "callback_data": "btn_static_main_menu", "text_button_label": "0"},
    ]
  ]

  // Удаление кнопок меню, если действие недоступно
  if (!hasPrevPhoto) {
    unsetButton(inlineKeyboard, "btn_static_prev_photo")
  }
  if (!hasNextPhoto) {
    unsetButton(inlineKeyboard, "btn_static_next_photo")
  }

  // Для отладки
  //bot.sendText(this.hasLastTelegramMessageId() + ":" + this.getLastTelegramMessageId())

  if (this.hasLastTelegramMessageId()) {
    if (prevSliderPos !== sliderPos) {
      newMedia = {"type": "photo", "media": image.url, "caption": "Фото #" + image.id}
      bot.sendMessage("---", inlineKeyboard, null, {"endpoint": "editMessageMedia", "message_id": this.getLastTelegramMessageId(), "media": newMedia})
      //this.saveLastTelegramMessageId()  // Тут этого делать не нужно! Тк тут мы обновляем существующее сообщение !
    }
  } else {
    bot.sendMessage("Фото #" + image.id, inlineKeyboard, [{"type": "image", "url": image.url}])
    this.saveLastTelegramMessageId()
  }

  return {
    "result": false,
  }
  // ---------------------------------------------------------------------------------------------------
}

//Todo: Учесть мультиязычность!module.exports = {
  imageSlider
}