functions/index.js (210 lines of code) (raw):

const fs = require('fs'); const uuid = require('uuid'); const cors = require('cors')({origin: true}); const vision = require('@google-cloud/vision'); const {Datastore} = require('@google-cloud/datastore'); const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path; const ffmpeg = require('fluent-ffmpeg'); const {google} = require('googleapis'); const validation = require('./validation'); ffmpeg.setFfmpegPath(ffmpegPath); const visionClient = new vision.v1p3beta1.ImageAnnotatorClient(); const datastore = new Datastore(); async function getGoogleAPIAuthentication() { const auth = new google.auth.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/drive'] }); return await auth.getClient(); } // Makes an ffmpeg command return a promise. function promisifyCommand(command) { return new Promise((resolve, reject) => { command.on('end', resolve).on('error', reject).run(); }); } async function writeFileAsync(path, buffer) { return new Promise((resolve, reject) => { fs.writeFile(path, buffer, (err) => { if(err) { reject(err); } else { resolve(); } }); }); } function addSecurityHeaders(res) { res.set('X-Frame-Options', 'SAMEORIGIN'); } exports.saveAudioSuggestions = async (req, res) => { addSecurityHeaders(res); return cors(req, res, async () => { // convert base64 body to blob of webm const nodeBuffer = Buffer.from(req.body, 'base64'); // convert to mp3 const fileName = uuid.v1(); const tempLocalPath = `/tmp/${fileName}.webm`; const targetTempFilePath = `/tmp/${fileName}.mp3`; await writeFileAsync(tempLocalPath, nodeBuffer); const command = new ffmpeg(tempLocalPath).toFormat('mp3').save(targetTempFilePath); await promisifyCommand(command); // upload to drive const drive = google.drive({version: 'v3', auth: await getGoogleAPIAuthentication()}); const createResponse = (await drive.files.create({ requestBody: { parents: [ process.env['AUDIO_FOLDER_ID'] ], mimeType: 'audio/mp3', name: `${fileName}.mp3` }, media: { mimeType: 'audio/mp3', body: fs.createReadStream(targetTempFilePath), } })); const fileId = createResponse.data.id; const file = (await drive.files.get({ fileId: fileId, fields: 'webContentLink' })).data; console.log(`Audio saved to ${file.webContentLink}.`); res.status(200).send(file.webContentLink); }); }; async function saveFeedback(spreadsheetId, sheetTitle, data) { const sheets = google.sheets({version: 'v4', auth: await getGoogleAPIAuthentication()}); // find spreadsheet const spreadsheet = (await sheets.spreadsheets.get({spreadsheetId: spreadsheetId, fields: 'sheets(properties.title)'})).data; if(!spreadsheet) { throw new Error("Spreadsheet not found"); } // find sheet const sheet = spreadsheet.sheets.find(sh => sh.properties.title === sheetTitle); if(!sheet) { // sheet does not exist - create it await sheets.spreadsheets.batchUpdate({ spreadsheetId: spreadsheetId, requestBody: { "requests": [ { "addSheet": { "properties": { "title": sheetTitle, "sheetType": "GRID" } } } ] } }) } // append data to sheet await sheets.spreadsheets.values.append({ spreadsheetId: spreadsheetId, range: sheetTitle + '!A1', valueInputOption: 'RAW', requestBody: { values: [data] } }); } exports.addSuggestions = async (req, res) => { addSecurityHeaders(res); return cors(req, res, async () => { if (!validation.isTargetLanguage(req.body.native_language)) { res.status(400).send("Invalid target language"); return; } else if(!validation.isPrimaryLanguage(req.body.language)) { res.status(400).send("Invalid primary language"); return; } await saveFeedback(process.env['SUGGESTIONS_SPREADSHEET'], req.body.native_language, [ req.body.language || '', req.body.native_language || '', req.body.english_word || '', req.body.primary_word || '', req.body.translation || '', req.body.transliteration || '', req.body.sound_link || '', new Date() ]); res.status(200).send("Translation suggestions saved."); }); }; exports.addFeedback = async (req, res) => { addSecurityHeaders(res); return cors(req, res, async () => { if (!validation.isTargetLanguage(req.body.native_language)) { res.status(400).send("Invalid target language"); return; } else if(!validation.isPrimaryLanguage(req.body.language)) { res.status(400).send("Invalid primary language"); return; } await saveFeedback(process.env['FEEDBACK_SPREADSHEET'], req.body.native_language, [ req.body.language || '', req.body.native_language || '', req.body.english_word || '', req.body.primary_word || '', req.body.translation || '', req.body.transliteration || '', req.body.sound_link || '', req.body.types ? req.body.types.join(', ') : '', req.body.content || '', new Date() ]); res.status(200).send("Feedback saved."); }); }; exports.getTranslations = async (req, res) => { addSecurityHeaders(res); return cors(req, res, async () => { const english_words = req.body.english_words; const primary_language = req.body.primary_language; const target_language = req.body.target_language; if (!validation.isTargetLanguage(target_language)) { res.status(400).send("Invalid target language"); return; } else if(!validation.isPrimaryLanguage(primary_language)) { res.status(400).send("Invalid primary language"); return; } else if(!english_words) { res.status(400).send("No words found"); return; } for(const w of english_words) { if(validation.containsHTML(w)) { res.status(400).send("Words cannot contain HTML tags"); return; } } const promises = english_words.map(async english_word => { const wordKey = datastore.key(['Translation', english_word]); const translations = await datastore.get(wordKey); return { word: english_word, translations: translations && translations.length > 0 ? translations[0] : null }; }); Promise.all(promises).then(docs => { const translations = docs.map(x => createTranslationResponseForApp(x, primary_language, target_language)); res.status(200).send(translations); }).catch(function(error) { console.log("Internal server error", error); res.status(500).send(error) }); }); }; function createTranslationResponseForApp(data, primary_language, target_language) { const primaryTranslation = data && data.translations ? data.translations[primary_language] : null; const targetTranslation = data && data.translations ? data.translations[target_language] : null; return { english_word: data.word, primary_word: primaryTranslation ? primaryTranslation.translation || '' : '', translation: targetTranslation ? targetTranslation.translation || '' : '', transliteration: targetTranslation ? targetTranslation.transliteration || '' : '', sound_link: targetTranslation ? targetTranslation.sound_link || '' : '' }; } exports.visionAPI = async (req, res) => { addSecurityHeaders(res); return cors(req, res, async () => { try { const requestVision = { image: {content: Buffer.from(req.body, 'base64')}, features: [{type: 'LABEL_DETECTION', maxResults: 10}, {type: 'SAFE_SEARCH_DETECTION'}] }; visionClient.annotateImage(requestVision) .then(response => { res.status(200).send(response && response.length > 0 ? response[0] : null); return "200" }) .catch(err => { console.error(err); res.status(500).send(err); }); } catch (err) { console.log(`Unable to detect objects: ${err}`) res.status(500).send(err); } }); };