app.js 13.4 KB
const TelegramBot = require('node-telegram-bot-api');

const config = require('./config');

// Create a bot that uses 'polling' to fetch new updates
const bot = new TelegramBot(config.telegram.token, { polling: true });

var request = require('request');

// Translation api url
const translate_api_url = 'https://openapi.naver.com/v1/papago/n2mt';

// Language detection api url
const languagedetect_api_url = 'https://openapi.naver.com/v1/papago/detectLangs'

// Initialize database
const db = require('./db');
db.init();

// Import Google Cloud client library and create a client
const vision = require('@google-cloud/vision');
const visionClient = new vision.ImageAnnotatorClient(config.gcloud);

// fs module for saving/removing image file upon/after recognition
const fs = require('fs');

// /echo [whatever]
bot.onText(/\/echo (.+)/, (msg, match) => {
  // 'msg' is the received Message from Telegram
  // 'match' is the result of executing the regexp above on the text content
  // of the message

  const chatId = msg.chat.id;
  const resp = match[1]; // the captured "whatever"

  // send back the matched "whatever" to the chat
  bot.sendMessage(chatId, resp);
});

/**
 * Translate given message and return the result as a parameter of Promise.
 *
 * @param {*} user    Info of user that sent the message (msg.from)
 * @param {*} message Message to translate
 */
function translate(user, message) {
  return new Promise(function (resolve, reject) {
    const userId = user.id;
    const userLang = user.language_code;

    // Language detection options
    var lang_detect_options = {
      url: languagedetect_api_url,
      form: { 'query': message },
      headers: {
        'X-Naver-Client-Id': config.papago.client_id,
        'X-Naver-Client-Secret': config.papago.client_secret
      }
    };

    // Papago language detection
    request.post(lang_detect_options, function (error, response, body) {
      console.log(response.statusCode);
      if (!error && response.statusCode == 200) {
        var detect_body = JSON.parse(response.body);
        var source = '';
        var target = '';
        var result = { type: 'text', text: '' };

        // Check if detection was successful
        console.log('Language detected: ' + detect_body.langCode);

        // Translate using papago
        // Try to grab source lang and target lang from user's preference first.
        // If preference is not found, source default to detected language, and
        // target defaults to English for Korean source, or Korean for all other langs.
        if (detect_body.langCode != 'unk') {
          db.findPref(userId, "pref", (dbResult) => {
            source = detect_body.langCode;

            if (source == userLang) {
              // User wants to translate his language into another
              // Leave target untouched, which is his desired destination language

              // Check if user target language exists in db, if not default to English
              console.log('Translating to target');
              target = (dbResult != undefined && dbResult.target != undefined)
                ? dbResult.target : 'en';
            } else {
              // source != userLang
              // User wants to translate another language into his
              // Then destination should be his language
              console.log('Translating to userLang');
              target = userLang;
            }

            console.log(source + ' => ' + target);

            // Papago translation options
            var translate_options = {
              url: translate_api_url,
              form: {
                'source': source, // Before translation
                'target': target, // After translation
                'text': message // Message to translate
              },
              headers: {
                'X-Naver-Client-Id': config.papago.client_id,
                'X-Naver-Client-Secret': config.papago.client_secret
              }
            };

            // Send translatation request
            request.post(translate_options, function (error, response, body) {
              var objBody = JSON.parse(response.body);
              if (!error && response.statusCode == 200) {
                // On success
                result.text = objBody.message.result.translatedText;

                console.log('Before: ' + message);
                console.log('After: ' + result.text);
              } else {
                // On failure
                result.text = '[' + objBody.errorCode + '] ' + objBody.errorMessage;
              }
              resolve(result.text);
            });
          });
        }
        // Language not detected
        else {
          result.text = '언어를 감지할 수 없습니다.';
          reject(result.text);
        }
      }
    });
  });
}

/**
 * Detect and read text from an image file using Google Cloud Vision API.
 *
 * @param {*} fileId Telegram-provided file id of image to read text from
 */
function detectText(fileId) {
  return new Promise(function (resolve, reject) {
    // Download the image, which will later be deleted to avoid git detection
    bot.downloadFile(fileId, '.').then(function (filePath) {
      console.log('Image downloaded: ', filePath);

      visionClient.documentTextDetection(filePath)
        .then(function (result) {
          const fullTextAnnotation = result[0].fullTextAnnotation;
          const text = fullTextAnnotation.text;

          console.log('Text detection result: ', text);

          resolve(text);
          // Delete the image
          fs.unlink(filePath, function (error) {
            if (error) {
              console.log('Error deleting image: ', error);
            } else {
              console.log('Successfully deleted file ', filePath);
            }
          });
        }).catch(function (error) {
          console.log(error);

          reject(error);
          // Delete the image
          fs.unlink(filePath, function (error) {
            if (error) {
              console.log('Error deleting image: ', error);
            } else {
              console.log('Successfully deleted file ', filePath);
            }
          });
        });
    });
  });
}

// [Any normal message which is not a command (not starting with '/')]
bot.onText(/^(?!\/)((.|\n)+)/, (msg, match) => {
  const user = msg.from;
  const chatId = msg.chat.id;
  const chatType = msg.chat.type;
  const received_msg = match[1];

  // Ignore if we are not on a private chat,
  // since direct translation is to be used only on private chats.
  if (chatType != 'private') {
    return;
  }

  translate(user, received_msg).then(function (result) {
    bot.sendMessage(chatId, result);
  }).catch(function (error) {
    console.log(error);
  });
});

// /t(ranslate) [Whatever]
// Translate the 'whatever' and show the result in any kind of chatroom.
// Also, if the given '/t' message is a reply to another message,
// translate the reply target message as well.
bot.onText(/^\/(t|translate)($| ((.|\n)+))/, (msg, match) => {
  const user = msg.from;
  const chatId = msg.chat.id;
  const chatType = msg.chat.type;
  const received_msg = match[3];
  // Whether the given '/t' message is a reply to another message
  const isReply = msg.reply_to_message != undefined;
  // Whether a message has been given after '/t' command
  const msgExists = received_msg != undefined;

  if (isReply) {
    // Translate the reply's target message
    const replyMsg = msg.reply_to_message.text;
    translate(user, replyMsg).then(function (result) {
      bot.sendMessage(chatId, result);
    }).catch(function (error) {
      console.log(error);
    });

    // If the reply contains image, translate it as well
    const photoExists = msg.reply_to_message.photo != undefined;
    if (photoExists) {
      // Choose largest image possible
      const photo = msg.reply_to_message.photo[msg.reply_to_message.photo.length - 1];
      const photoId = photo.file_id;
      const caption = msg.reply_to_message.caption;

      // Detect text from given image
      detectText(photoId).then(function (text) {
        // Translate the result
        translate(user, text).then(function (result) {
          // Send recognized text to user
          bot.sendMessage(chatId, result);
        });
      }).catch(function (error) {
        // Text detection failed
        console.log('Text detection error: ', error);
      });

      // Translate caption if exists
      const captionExists = caption != undefined;
      if (captionExists) {
        translate(user, caption).then(function (result) {
          bot.sendMessage(chatId, result);
        }).catch(function (error) {
          console.log(error);
        });
      }
    }
  }

  // Translate the message after '/t' if exists
  if (msgExists) {
    translate(user, received_msg).then(function (result) {
      bot.sendMessage(chatId, result);
    }).catch(function (error) {
      console.log(error);
    });
  }
});

// When an image file is received, temporarily save it in current directory
// and use tesseract to recognize its text. The texts are then translated
// and sent to the user.
bot.on('photo', (msg) => {
  const user = msg.from;
  const chatId = msg.chat.id;
  const chatType = msg.chat.type;
  const photo = msg.photo[msg.photo.length - 1]; // Choose largest image possible
  const photoId = photo.file_id;
  const caption = msg.caption;

  // Ignore if we are not on a private chat
  if (chatType != 'private') {
    return;
  }

  // Detect text from given image
  detectText(photoId).then(function (text) {
    // Translate the result
    translate(user, text).then(function (result) {
      // Send recognized text to user
      bot.sendMessage(chatId, result);
    });
  }).catch(function (error) {
    // Text detection failed
    console.log('Text detection error: ', error);
  });

  // Translate caption if exists
  const captionExists = caption != undefined;
  if (captionExists) {
    translate(user, caption).then(function (result) {
      bot.sendMessage(chatId, result);
    }).catch(function (error) {
      console.log(error);
    });
  }
});

// /l(anguage)
// Let user select the language he wants his message to translate to.
// When triggered, bot will send an inline keyboard message with a list
// of available langauges. For an example of an inline keyboard message,
// see https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating.
bot.onText(/^\/(l|anguage)/, (msg, match) => {
  const chatId = msg.chat.id;
  const msgId = msg.message_id;

  const inlineKeyboard = {
    inline_keyboard: [
      // Languages supported by papago language detection
      // One array per line
      [
        { text: '한국어', callback_data: 'ko' },
        { text: '영어', callback_data: 'en' },
        { text: '일본어', callback_data: 'ja' }
      ],
      [
        { text: '중국어 간체', callback_data: 'zh-CN' },
        { text: '중국어 번체', callback_data: 'zh-TW' },
        { text: '스페인어', callback_data: 'es' }
      ],
      [
        { text: '프랑스어', callback_data: 'fr' },
        { text: '독일어', callback_data: 'de' },
        { text: '베트남어', callback_data: 'vi' }
      ],
      [
        { text: '인도네시아어', callback_data: 'id' },
        { text: '태국어', callback_data: 'th' }
      ],
      [
        { text: '러시아어', callback_data: 'ru' },
        { text: '이탈리아어', callback_data: 'it' }
      ],
    ]
  }
  const options = {
    reply_to_message_id: msgId,
    reply_markup: inlineKeyboard
  }

  bot.sendMessage(chatId, '무슨 언어로 번역할까요? 선택은 기억됩니다.', options);
});

bot.on('callback_query', (query) => {
  const queryId = query.id;
  const userId = query.from.id;
  const langCode = query.data;

  db.updatePref(userId, { "pref": { "target": langCode } }, (result) => {
    const options = {
      text: 'From now on, your messages will be translated into ' + langCode
    }

    bot.answerCallbackQuery(queryId, options);
  })
});

bot.onText(/^\/(h|help)/, (msg, match) => {
  const chatId = msg.chat.id;
  const helpText = "봇 사용법\n\n\
/h 또는 /help 커맨드를 입력해 이 도움말을 보실 수 있습니다.\n\n\
메시지 또는 사진 바로 번역하기\n\
- 봇과의 1:1 대화에서 메시지나 이미지를 보내면 번역된 결과를 받아보실 수 있습니다.\n\
- 번역은 유저의 기본언어->타겟 언어, 외국어->유저의 기본언어 로 진행됩니다.\n\
- /l 커맨드를 이용해 번역할 타겟 언어를 설정하면 해당 언어로 번역됩니다. 기본값은 영어입니다.\n\n\
/t 또는 /translate : [msg] 또는 /t [msg] 또는 [reply] + /t\n\
- 1:1 대화나 그룹챗에서 번역하고 싶은 메시지가 있다면 '/t 번역하고 싶은 메시지'를 입력하거나, \
번역하고 싶은 메시지에 '답장하기'를 선택하고 '/t'를 입력하면 해당 메시지가 번역됩니다.\n\n\
/l 또는 /language\n\
- 번역할 타겟 언어를 설정할 수 있습니다. 기본값은 영어입니다.\n"
  const langCodes = "언어 코드:\n\
ko = 한국어\nen = 영어\nja = 일본어\nzh-CN = 중국어 간체\nzh-TW = 중국어 번체\n\
vi = 베트남어\nid = 인도네시아어\nth = 태국어\nde = 독일어\n\
ru = 러시아어\nes = 스페인어\nit = 이탈리아어\nfr = 프랑스어"
  const available = "지원되는 번역:\n\
ko <-> en/ja/zh-CN/zh-TW/vi/id/th/de/ru/es/it/fr\n\
en <-> ja/zh-CN/zh-TW/fr\n\
ja <-> zh-CN/zh-TW\nzh-CN <-> zh-TW"

  bot.sendMessage(chatId, helpText);
  bot.sendMessage(chatId, langCodes);
  bot.sendMessage(chatId, available);
});