From 8d96e74d191af4b06616de13346122390321facd Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 15:45:39 +0200 Subject: [PATCH 01/14] handle ALEXA.ResumeIntent --- functions/src/actions/resume-intent.js | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 functions/src/actions/resume-intent.js diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js new file mode 100644 index 00000000..4b41bd05 --- /dev/null +++ b/functions/src/actions/resume-intent.js @@ -0,0 +1,40 @@ +const dialog = require('../dialog'); +const playlist = require('../state/playlist'); +const query = require('../state/query'); +const {debug} = require('../utils/logger')('ia:actions:resume-intent'); + +const feederFromPlaylist = require('./high-order-handlers/middlewares/feeder-from-playlist'); +const fulfilResolvers = require('./high-order-handlers/middlewares/fulfil-resolvers'); +const playSong = require('./high-order-handlers/middlewares/play-song'); +const parepareSongData = require('./high-order-handlers/middlewares/song-data'); +const renderSpeech = require('./high-order-handlers/middlewares/render-speech'); + +/** + * handle ALEXA.ResumeIntent + * TODO: but maybe it would be useful for Actions of Google + * in case of new session for returned user + * + * @param app + */ +function handler (app) { + return feederFromPlaylist()({app, playlist, query}) + .then(ctx => + Object.assign({}, ctx, { + slots: Object.assign( + {}, ctx.slots, {platform: ctx.app.platform} + ) + }) + ) + .then(parepareSongData()) + .then(fulfilResolvers()) + .then(renderSpeech()) + .then(playSong()) + .catch(context => { + debug('It could be an error:', context); + return dialog.ask(app, {speech: `Please choose what do you want to play.`}); + }); +} + +module.exports = { + handler, +}; From 5b0dceb52e2cd9aa89d29fdcb32113cb076fe068 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 19:09:49 +0200 Subject: [PATCH 02/14] simplify passing platform to slots --- functions/src/actions/resume-intent.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js index 4b41bd05..d3b3e979 100644 --- a/functions/src/actions/resume-intent.js +++ b/functions/src/actions/resume-intent.js @@ -17,14 +17,12 @@ const renderSpeech = require('./high-order-handlers/middlewares/render-speech'); * @param app */ function handler (app) { - return feederFromPlaylist()({app, playlist, query}) - .then(ctx => - Object.assign({}, ctx, { - slots: Object.assign( - {}, ctx.slots, {platform: ctx.app.platform} - ) - }) - ) + return feederFromPlaylist()({ + app, + playlist, + query, + slots: {platform: app.platform} + }) .then(parepareSongData()) .then(fulfilResolvers()) .then(renderSpeech()) From 8f5377b15fdccd0514667dc56bf3e7c3cbb0b350 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 19:37:48 +0200 Subject: [PATCH 03/14] clean playlist when we land to welcome action --- functions/src/actions/welcome.js | 2 ++ functions/tests/actions/welcome.spec.js | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/functions/src/actions/welcome.js b/functions/src/actions/welcome.js index 33156ff3..8cc583de 100644 --- a/functions/src/actions/welcome.js +++ b/functions/src/actions/welcome.js @@ -1,6 +1,7 @@ const _ = require('lodash'); const dialog = require('../dialog'); +const playlist = require('../state/playlist'); const query = require('../state/query'); const welcomeStrings = require('../strings').intents.welcome; @@ -19,6 +20,7 @@ function handler (app) { speech = _.sample(welcomeStrings.acknowledges) + ' ' + welcomeStrings.speech; } + playlist.create(app, []); query.resetSlots(app); dialog.ask(app, Object.assign({}, welcomeStrings, {speech, reprompt})); } diff --git a/functions/tests/actions/welcome.spec.js b/functions/tests/actions/welcome.spec.js index 83246ddd..d1181a36 100644 --- a/functions/tests/actions/welcome.spec.js +++ b/functions/tests/actions/welcome.spec.js @@ -2,6 +2,7 @@ const {expect} = require('chai'); const rewire = require('rewire'); const welcome = rewire('../../src/actions/welcome'); +const playlist = require('../../src/state/playlist'); const query = require('../../src/state/query'); const mockApp = require('../_utils/mocking/platforms/assistant'); @@ -41,5 +42,12 @@ describe('actions', () => { expect(query.hasSlot(app, 'creator')).to.be.false; expect(query.hasSlot(app, 'collection')).to.be.false; }); + + it('should reset playlist', () => { + let app = mockApp(); + playlist.create(app, ['item-1', 'item-2']); + welcome.handler(app); + expect(playlist.isEmpty(app)).to.be.true; + }); }); }); From b6ef1a5740068cdb035ae424782a8f81de5dc7e2 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 19:45:27 +0200 Subject: [PATCH 04/14] explain how welcome action works --- functions/src/actions/welcome.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/functions/src/actions/welcome.js b/functions/src/actions/welcome.js index 8cc583de..6c520850 100644 --- a/functions/src/actions/welcome.js +++ b/functions/src/actions/welcome.js @@ -20,8 +20,15 @@ function handler (app) { speech = _.sample(welcomeStrings.acknowledges) + ' ' + welcomeStrings.speech; } + // TODO: it would be great to implement some sophisticated + // behaviour but for the moment we just clean state of the user's session + // when we return to welcome action + + // so "Resume" intent won't work after that + // we clean all that information playlist.create(app, []); query.resetSlots(app); + dialog.ask(app, Object.assign({}, welcomeStrings, {speech, reprompt})); } From 6ccee3d37f0d365ba4b347cde15a147493aafcc4 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 20:14:53 +0200 Subject: [PATCH 05/14] tell nothing to resume when we have nothing to resume --- functions/src/actions/resume-intent.js | 29 +++++++++++++++++++------- functions/src/state/dialog.js | 8 +++---- functions/src/state/helpers.js | 2 +- functions/src/strings.js | 10 +++++++++ 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js index d3b3e979..a8d9a8b9 100644 --- a/functions/src/actions/resume-intent.js +++ b/functions/src/actions/resume-intent.js @@ -1,6 +1,8 @@ const dialog = require('../dialog'); +const dialogState = require('../state/dialog'); const playlist = require('../state/playlist'); const query = require('../state/query'); +const strings = require('../strings'); const {debug} = require('../utils/logger')('ia:actions:resume-intent'); const feederFromPlaylist = require('./high-order-handlers/middlewares/feeder-from-playlist'); @@ -23,13 +25,26 @@ function handler (app) { query, slots: {platform: app.platform} }) - .then(parepareSongData()) - .then(fulfilResolvers()) - .then(renderSpeech()) - .then(playSong()) - .catch(context => { - debug('It could be an error:', context); - return dialog.ask(app, {speech: `Please choose what do you want to play.`}); + .then(() => { + return parepareSongData() + .then(fulfilResolvers()) + .then(renderSpeech()) + .then(playSong()) + .catch(context => { + debug('It could be an error:', context); + return dialog.ask(app, strings.intents.resume.fail); + }); + }, () => { + dialog.ask(app, { + speech: [ + strings.intents.resume.empty.speech, + dialogState.getLastReprompt(app), + ], + + reprompt: strings.intents.resume.empty.reprompt || dialogState.getLastReprompt(app), + + suggestions: [].concat(dialogState.getLastSuggestions(app)/*, strings.intents.resume.empty.suggestions*/), + }); }); } diff --git a/functions/src/state/dialog.js b/functions/src/state/dialog.js index 543a0ecc..f979e1c9 100644 --- a/functions/src/state/dialog.js +++ b/functions/src/state/dialog.js @@ -15,7 +15,7 @@ function savePhrase (app, phrase) { } function getLastSpeech (app) { - return _.at(getData(app), 'lastPhrase.speech')[0]; + return _.get(getData(app), 'lastPhrase.speech'); } /** @@ -25,7 +25,7 @@ function getLastSpeech (app) { * @returns {undefined|string} */ function getLastPhrase (app) { - return _.at(getData(app), 'lastPhrase')[0]; + return _.get(getData(app), 'lastPhrase'); } /** @@ -35,7 +35,7 @@ function getLastPhrase (app) { * @returns {undefined|string} */ function getLastReprompt (app) { - return _.at(getData(app), 'lastPhrase.reprompt')[0]; + return _.get(getData(app), 'lastPhrase.reprompt'); } /** @@ -45,7 +45,7 @@ function getLastReprompt (app) { * @returns {undefined|string} */ function getLastSuggestions (app) { - return _.at(getData(app), 'lastPhrase.suggestions')[0]; + return _.get(getData(app), 'lastPhrase.suggestions'); } module.exports = { diff --git a/functions/src/state/helpers.js b/functions/src/state/helpers.js index 271b42d9..7ca4c1e7 100644 --- a/functions/src/state/helpers.js +++ b/functions/src/state/helpers.js @@ -70,7 +70,7 @@ module.exports = { */ setData: (app, value) => { debug(`set user's state ${name} to ${JSON.stringify(value)}`); - if (typeof app === 'string') { + if (typeof app === 'string' || !app) { throw new Error(`Argument 'app' should be DialogflowApp object but we get ${app}`); } diff --git a/functions/src/strings.js b/functions/src/strings.js index d057623c..209f3f3c 100644 --- a/functions/src/strings.js +++ b/functions/src/strings.js @@ -392,6 +392,16 @@ module.exports = { speech: "I'm sorry I'm having trouble here. Maybe we should try this again later.", }], + resume: { + fail: { + speech: 'Fail to resume. Please choose what do you want to play.', + }, + + empty: { + speech: 'Nothing to resume. Please choose what do you want to play.', + }, + }, + titleOption: { false: { speech: `Ok, muting song titles.`, From 313961da8545c82dc4c05f81a41c882ad9f7f14e Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 20:48:32 +0200 Subject: [PATCH 06/14] add dialog mergre --- functions/src/actions/resume-intent.js | 2 +- functions/src/dialog/merge.js | 27 ++++++++++ functions/tests/dialog/merge.spec.js | 68 ++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 functions/src/dialog/merge.js create mode 100644 functions/tests/dialog/merge.spec.js diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js index a8d9a8b9..93002647 100644 --- a/functions/src/actions/resume-intent.js +++ b/functions/src/actions/resume-intent.js @@ -43,7 +43,7 @@ function handler (app) { reprompt: strings.intents.resume.empty.reprompt || dialogState.getLastReprompt(app), - suggestions: [].concat(dialogState.getLastSuggestions(app)/*, strings.intents.resume.empty.suggestions*/), + suggestions: [].concat(dialogState.getLastSuggestions(app)/*, strings.intents.resume.empty.suggestions */), }); }); } diff --git a/functions/src/dialog/merge.js b/functions/src/dialog/merge.js new file mode 100644 index 00000000..79dc2cf3 --- /dev/null +++ b/functions/src/dialog/merge.js @@ -0,0 +1,27 @@ +const _ = require('lodash'); + +/** + * Merge speech instances + * + * @param args + */ +module.exports = (...args) => args.reduce( + (acc, item) => { + if ('speech' in item) { + acc.speech = [] + .concat(acc.speech, item.speech) + .filter(i => i); + } + + if ('suggestions' in item) { + acc.suggestions = _.union(acc.suggestions, item.suggestions); + } + + if ('reprompt' in item) { + acc.reprompt = item.reprompt; + } + + return acc; + }, + {} +); diff --git a/functions/tests/dialog/merge.spec.js b/functions/tests/dialog/merge.spec.js new file mode 100644 index 00000000..2bdc05d6 --- /dev/null +++ b/functions/tests/dialog/merge.spec.js @@ -0,0 +1,68 @@ +const {expect} = require('chai'); + +const merge = require('../../src/dialog/merge'); + +describe('dialog', () => { + describe('merge', () => { + it('should concat speech together', () => { + const res = merge({ + speech: 'Picture a bright blue ball, just spinning, spinnin free', + }, { + speech: 'Dizzy with eternity.', + }); + + expect(res).to.be.deep.equal({ + speech: [ + 'Picture a bright blue ball, just spinning, spinnin free', + 'Dizzy with eternity.', + ], + }); + }); + + it('should concat suggestions together', () => { + const res = merge({ + suggestions: ['one', 'two'], + }, { + suggestions: ['three', 'four'], + }); + + expect(res).to.be.deep.equal({ + suggestions: [ + 'one', + 'two', + 'three', + 'four', + ], + }); + }); + + it('should drop suggestions duplications', () => { + const res = merge({ + suggestions: ['one', 'two', 'three'], + }, { + suggestions: ['two', 'three', 'four'], + }); + + expect(res).to.be.deep.equal({ + suggestions: [ + 'one', + 'two', + 'three', + 'four', + ], + }); + }); + + it('should left the last reprompt', () => { + const res = merge({ + reprompt: 'Which artist?' + }, { + reprompt: 'Which album?' + }); + + expect(res).to.be.deep.equal({ + reprompt: 'Which album?' + }); + }); + }); +}); From c20ef37b15f5e2c50c45f839ce6c37c690ffb94e Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 21:18:06 +0200 Subject: [PATCH 07/14] add test for resume empty playlist --- functions/tests/_utils/mocking/dialog/ask.js | 5 ++ .../tests/_utils/mocking/platforms/app.js | 2 +- functions/tests/actions/resume-intent.spec.js | 51 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 functions/tests/_utils/mocking/dialog/ask.js create mode 100644 functions/tests/actions/resume-intent.spec.js diff --git a/functions/tests/_utils/mocking/dialog/ask.js b/functions/tests/_utils/mocking/dialog/ask.js new file mode 100644 index 00000000..257c1b99 --- /dev/null +++ b/functions/tests/_utils/mocking/dialog/ask.js @@ -0,0 +1,5 @@ +const sinon = require('sinon'); + +module.exports = () => ({ + ask: sinon.spy(), +}); diff --git a/functions/tests/_utils/mocking/platforms/app.js b/functions/tests/_utils/mocking/platforms/app.js index 8a311af4..74044f75 100644 --- a/functions/tests/_utils/mocking/platforms/app.js +++ b/functions/tests/_utils/mocking/platforms/app.js @@ -1,6 +1,6 @@ const sinon = require('sinon'); -module.exports = ({getByName = {}, getData = {}}) => ({ +module.exports = ({getByName = {}, getData = {}} = {}) => ({ params: {getByName: sinon.stub().callsFake(name => getByName[name])}, persist: { getData: sinon.stub().callsFake(name => getData[name]), diff --git a/functions/tests/actions/resume-intent.spec.js b/functions/tests/actions/resume-intent.spec.js new file mode 100644 index 00000000..475db896 --- /dev/null +++ b/functions/tests/actions/resume-intent.spec.js @@ -0,0 +1,51 @@ +const {expect} = require('chai'); +const rewire = require('rewire'); + +const resumeIntent = rewire('../../src/actions/resume-intent'); +const strings = require('../../src/strings'); + +const mockDialog = require('../_utils/mocking/dialog/ask'); +const mockApp = require('../_utils/mocking/platforms/app'); + +describe('actions', () => { + describe('resume-intent', () => { + let app; + let dialog; + + beforeEach(() => { + app = mockApp({ + getData: { + dialog: { + lastPhrase: { + reprompt: 'What is the album?', + suggestions: ['Grateful Dead'], + } + } + } + }); + + dialog = mockDialog(); + + resumeIntent.__set__('dialog', dialog); + }); + + it('should reprompt when resume but nothing to resume', () => { + return resumeIntent.handler(app) + .then(() => { + expect(dialog.ask).to.have.been.called; + expect(dialog.ask.args[0][0]).to.equal(app); + expect(dialog.ask.args[0][1]).to.have.property('speech') + .with.members([ + strings.intents.resume.empty.speech, + 'What is the album?', + ]); + expect(dialog.ask.args[0][1]).to.have.property('suggestions') + .with.members([ + 'Grateful Dead', + ]); + expect(dialog.ask.args[0][1]).to.have.property('reprompt') + .be.equal('What is the album?'); + }); + }); + }); +}); From 9c3fa621ad2ecbc5bb1a3d4d738747c92dc7b55d Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 22:48:31 +0200 Subject: [PATCH 08/14] merge stored reprompt and strings of resume intent --- functions/src/actions/resume-intent.js | 15 +++++---------- functions/src/dialog/index.js | 1 + functions/tests/_utils/mocking/dialog/ask.js | 3 ++- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js index 93002647..02f2fac6 100644 --- a/functions/src/actions/resume-intent.js +++ b/functions/src/actions/resume-intent.js @@ -35,16 +35,11 @@ function handler (app) { return dialog.ask(app, strings.intents.resume.fail); }); }, () => { - dialog.ask(app, { - speech: [ - strings.intents.resume.empty.speech, - dialogState.getLastReprompt(app), - ], - - reprompt: strings.intents.resume.empty.reprompt || dialogState.getLastReprompt(app), - - suggestions: [].concat(dialogState.getLastSuggestions(app)/*, strings.intents.resume.empty.suggestions */), - }); + dialog.ask(app, dialog.merge({ + speech: dialogState.getLastReprompt(app), + reprompt: dialogState.getLastReprompt(app), + suggestions: dialogState.getLastSuggestions(app), + }, strings.intents.resume.empty)); }); } diff --git a/functions/src/dialog/index.js b/functions/src/dialog/index.js index d1d592f9..dcc26e16 100644 --- a/functions/src/dialog/index.js +++ b/functions/src/dialog/index.js @@ -3,5 +3,6 @@ const audio = require('./audio'); module.exports = { ask: require('./ask'), playSong: audio.playSong, + merge: require('./merge'), tell: require('./tell'), }; diff --git a/functions/tests/_utils/mocking/dialog/ask.js b/functions/tests/_utils/mocking/dialog/ask.js index 257c1b99..a527c7dd 100644 --- a/functions/tests/_utils/mocking/dialog/ask.js +++ b/functions/tests/_utils/mocking/dialog/ask.js @@ -1,5 +1,6 @@ const sinon = require('sinon'); +const dialog = require('../../../../src/dialog'); -module.exports = () => ({ +module.exports = () => Object.assign({}, dialog, { ask: sinon.spy(), }); From ec08f91004887498730bfab5e3bd1082b4e7ed09 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 23:27:40 +0200 Subject: [PATCH 09/14] contruct reprompt from stored dialog state --- functions/src/actions/resume-intent.js | 9 ++++----- functions/src/state/dialog.js | 13 +++++++++++++ functions/tests/state/dialog.spec.js | 19 ++++++++++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js index 02f2fac6..8e9a1f56 100644 --- a/functions/src/actions/resume-intent.js +++ b/functions/src/actions/resume-intent.js @@ -35,11 +35,10 @@ function handler (app) { return dialog.ask(app, strings.intents.resume.fail); }); }, () => { - dialog.ask(app, dialog.merge({ - speech: dialogState.getLastReprompt(app), - reprompt: dialogState.getLastReprompt(app), - suggestions: dialogState.getLastSuggestions(app), - }, strings.intents.resume.empty)); + dialog.ask(app, dialog.merge( + dialogState.getReprompt(app), + strings.intents.resume.empty + )); }); } diff --git a/functions/src/state/dialog.js b/functions/src/state/dialog.js index f979e1c9..b496d42c 100644 --- a/functions/src/state/dialog.js +++ b/functions/src/state/dialog.js @@ -48,10 +48,23 @@ function getLastSuggestions (app) { return _.get(getData(app), 'lastPhrase.suggestions'); } +/** + * get reprompt for speech + * + * @param app + * @returns {{reprompt: (undefined|string), speech: Array, suggestions: (undefined|string)}} + */ +const getReprompt = (app) => ({ + reprompt: getLastReprompt(app), + speech: getLastReprompt(app), + suggestions: getLastSuggestions(app), +}); + module.exports = { getLastPhrase, getLastReprompt, getLastSpeech, getLastSuggestions, + getReprompt, savePhrase, }; diff --git a/functions/tests/state/dialog.spec.js b/functions/tests/state/dialog.spec.js index 0a837eee..82ee96c4 100644 --- a/functions/tests/state/dialog.spec.js +++ b/functions/tests/state/dialog.spec.js @@ -1,6 +1,6 @@ const {expect} = require('chai'); -const {getLastPhrase, getLastReprompt, savePhrase} = require('../../src/state/dialog'); +const {getLastPhrase, getLastReprompt, getReprompt, savePhrase} = require('../../src/state/dialog'); describe('state', () => { let app; @@ -31,5 +31,22 @@ describe('state', () => { expect(getLastReprompt(app)).to.be.equal('Sorry! Repeat The number please!'); }); }); + + describe('getReprompt', () => { + it('should return reprompt object with speech, suggestions and upcoming reprompt speech', () => { + savePhrase(app, { + speech: 'What is your number?', + reprompt: 'Sorry! Repeat The number please!', + suggestions: ['1', '2', '3'], + }); + + expect(getReprompt(app)) + .to.be.deep.equal({ + speech: 'Sorry! Repeat The number please!', + reprompt: 'Sorry! Repeat The number please!', + suggestions: ['1', '2', '3'], + }); + }); + }); }); }); From 10215c6b613bf52bd4a42e1cc553004c225a4e4c Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 23:28:25 +0200 Subject: [PATCH 10/14] use reprompt for resume intent fail --- functions/src/actions/resume-intent.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js index 8e9a1f56..c9855e45 100644 --- a/functions/src/actions/resume-intent.js +++ b/functions/src/actions/resume-intent.js @@ -32,7 +32,10 @@ function handler (app) { .then(playSong()) .catch(context => { debug('It could be an error:', context); - return dialog.ask(app, strings.intents.resume.fail); + return dialog.ask(app, dialog.merge( + dialogState.getReprompt(app), + strings.intents.resume.fail + )); }); }, () => { dialog.ask(app, dialog.merge( From 10c06d5d9eb033d16802984d5e3dfc941e87504f Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 23:30:51 +0200 Subject: [PATCH 11/14] drop part of empty and fail resume speech --- functions/src/strings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/functions/src/strings.js b/functions/src/strings.js index 209f3f3c..13d7037b 100644 --- a/functions/src/strings.js +++ b/functions/src/strings.js @@ -394,11 +394,11 @@ module.exports = { resume: { fail: { - speech: 'Fail to resume. Please choose what do you want to play.', + speech: 'Fail to resume.', }, empty: { - speech: 'Nothing to resume. Please choose what do you want to play.', + speech: 'Nothing to resume.', }, }, From 89619e578960823d1b1c6043f5550c5d18e4b6fc Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Fri, 13 Apr 2018 23:33:43 +0200 Subject: [PATCH 12/14] resume speech should be before reprompt --- functions/src/actions/resume-intent.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js index c9855e45..01fb2803 100644 --- a/functions/src/actions/resume-intent.js +++ b/functions/src/actions/resume-intent.js @@ -33,14 +33,14 @@ function handler (app) { .catch(context => { debug('It could be an error:', context); return dialog.ask(app, dialog.merge( - dialogState.getReprompt(app), - strings.intents.resume.fail + strings.intents.resume.fail, + dialogState.getReprompt(app) )); }); }, () => { dialog.ask(app, dialog.merge( - dialogState.getReprompt(app), - strings.intents.resume.empty + strings.intents.resume.empty, + dialogState.getReprompt(app) )); }); } From 7ad17386ef0ddba63c4feb332adfd81cac63156e Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sat, 14 Apr 2018 00:22:49 +0200 Subject: [PATCH 13/14] store offset of stopped song --- functions/src/actions/playback-stopped.js | 7 ++++-- functions/src/platform/alexa/app.js | 9 ++++++++ functions/src/platform/assistant/app.js | 12 ++++++++++ functions/src/state/playback.js | 21 ++++++++++++++++++ .../tests/_utils/mocking/platforms/app.js | 5 ++++- .../tests/actions/playback-stopped.spec.js | 22 +++++++++++++++++++ 6 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 functions/tests/actions/playback-stopped.spec.js diff --git a/functions/src/actions/playback-stopped.js b/functions/src/actions/playback-stopped.js index ba716649..ed715a1a 100644 --- a/functions/src/actions/playback-stopped.js +++ b/functions/src/actions/playback-stopped.js @@ -1,8 +1,11 @@ const {debug} = require('../utils/logger')('ia:actions:playback-stopped'); +const playback = require('../state/playback'); + function handler (app) { - // TODO: log - debug('token', app.params.getByName('token')); + const offset = app.getOffset(); + debug('offset', offset); + playback.setOffset(app, offset); } /** diff --git a/functions/src/platform/alexa/app.js b/functions/src/platform/alexa/app.js index 889dbce4..db4b6dea 100644 --- a/functions/src/platform/alexa/app.js +++ b/functions/src/platform/alexa/app.js @@ -25,6 +25,15 @@ class App { isFirstTry () { return true; } + + /** + * Current track offset + * + * @returns {Number} + */ + getOffset () { + return this.ctx.event.request.offsetInMilliseconds; + } } module.exports = { diff --git a/functions/src/platform/assistant/app.js b/functions/src/platform/assistant/app.js index 49595ed8..5afa6271 100644 --- a/functions/src/platform/assistant/app.js +++ b/functions/src/platform/assistant/app.js @@ -25,6 +25,18 @@ class App { isFirstTry () { return this.ctx.getLastSeen(); } + + /** + * Current track offset + * + * for the moment Action of Google doesn't support offset + * so it's always zero + * + * @returns {Number} + */ + getOffset () { + return 0; + } } module.exports = { diff --git a/functions/src/state/playback.js b/functions/src/state/playback.js index dca4a4ac..499d0e8c 100644 --- a/functions/src/state/playback.js +++ b/functions/src/state/playback.js @@ -24,7 +24,28 @@ const setMuteSpeechBeforePlayback = (app, muteSpeech) => muteSpeech, })); +/** + * Get current played track offest + * + * @param app + */ +const getOffset = (app) => getData(app).offset || 0; + +/** + * Set current played track offset + * + * @param app + * @param offset + */ +const setOffset = (app, offset) => + setData(app, Object.assign({}, getData(app), { + offset, + })); + module.exports = { isMuteSpeechBeforePlayback, setMuteSpeechBeforePlayback, + + getOffset, + setOffset, }; diff --git a/functions/tests/_utils/mocking/platforms/app.js b/functions/tests/_utils/mocking/platforms/app.js index 74044f75..0e821620 100644 --- a/functions/tests/_utils/mocking/platforms/app.js +++ b/functions/tests/_utils/mocking/platforms/app.js @@ -1,7 +1,10 @@ const sinon = require('sinon'); -module.exports = ({getByName = {}, getData = {}} = {}) => ({ +module.exports = ({getByName = {}, getData = {}, offset = 0} = {}) => ({ + getOffset: sinon.stub().returns(offset), + params: {getByName: sinon.stub().callsFake(name => getByName[name])}, + persist: { getData: sinon.stub().callsFake(name => getData[name]), setData: sinon.spy(), diff --git a/functions/tests/actions/playback-stopped.spec.js b/functions/tests/actions/playback-stopped.spec.js new file mode 100644 index 00000000..44aa7fb8 --- /dev/null +++ b/functions/tests/actions/playback-stopped.spec.js @@ -0,0 +1,22 @@ +const {expect} = require('chai'); + +const mockApp = require('../_utils/mocking/platforms/app'); +const playbackStopped = require('../../src/actions/playback-stopped'); + +describe('actions', () => { + describe('playback stopped', () => { + let app; + + beforeEach(() => { + app = mockApp({ + offset: 12345, + }); + }); + + it('should store offset', () => { + playbackStopped.handler(app); + expect(app.persist.setData).to.have.been.called; + expect(app.persist.setData.args[0][1]).to.have.property('offset', 12345); + }); + }); +}); From 17f16cbcd32ae79446fe35c31ef8d9e7b7dd49b1 Mon Sep 17 00:00:00 2001 From: Eugene Krevenets Date: Sat, 14 Apr 2018 01:05:03 +0200 Subject: [PATCH 14/14] use offset for resume playback --- .../actions/high-order-handlers/middlewares/play-song.js | 3 ++- functions/src/actions/resume-intent.js | 7 ++++--- functions/src/dialog/audio.js | 1 + functions/src/platform/alexa/response/index.js | 3 ++- .../high-order-handlers/middlewares/play-song.spec.js | 3 ++- functions/tests/platform/alexa/response/index.spec.js | 5 ++++- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/functions/src/actions/high-order-handlers/middlewares/play-song.js b/functions/src/actions/high-order-handlers/middlewares/play-song.js index a0146481..5e48ae0c 100644 --- a/functions/src/actions/high-order-handlers/middlewares/play-song.js +++ b/functions/src/actions/high-order-handlers/middlewares/play-song.js @@ -8,12 +8,13 @@ const {debug} = require('../../../utils/logger')('ia:actions:middlewares:song-da * * @param mediaResponseOnly {boolean} we should return media response only */ -module.exports = ({mediaResponseOnly = false} = {}) => (context) => { +module.exports = ({mediaResponseOnly = false, offset = 0} = {}) => (context) => { debug('start'); const {app} = context; dialog.playSong(app, Object.assign( {}, context.slots, { mediaResponseOnly, + offset, speech: context.speech.join(' '), description: context.description, } diff --git a/functions/src/actions/resume-intent.js b/functions/src/actions/resume-intent.js index 01fb2803..baeae90f 100644 --- a/functions/src/actions/resume-intent.js +++ b/functions/src/actions/resume-intent.js @@ -1,6 +1,7 @@ const dialog = require('../dialog'); const dialogState = require('../state/dialog'); const playlist = require('../state/playlist'); +const playback = require('../state/playback'); const query = require('../state/query'); const strings = require('../strings'); const {debug} = require('../utils/logger')('ia:actions:resume-intent'); @@ -25,11 +26,11 @@ function handler (app) { query, slots: {platform: app.platform} }) - .then(() => { - return parepareSongData() + .then(ctx => { + return parepareSongData()(ctx) .then(fulfilResolvers()) .then(renderSpeech()) - .then(playSong()) + .then(playSong({offset: playback.getOffset(app)})) .catch(context => { debug('It could be an error:', context); return dialog.ask(app, dialog.merge( diff --git a/functions/src/dialog/audio.js b/functions/src/dialog/audio.js index 7e2e3fb9..0a28d25b 100644 --- a/functions/src/dialog/audio.js +++ b/functions/src/dialog/audio.js @@ -83,6 +83,7 @@ function playSong (app, options) { description, contentURL: options.audioURL, imageURL: options.imageURL || config.media.DEFAULT_SONG_IMAGE, + offset: options.offset, // if previous track was define we try to stitch to it // for the moment it only works for Alexa diff --git a/functions/src/platform/alexa/response/index.js b/functions/src/platform/alexa/response/index.js index 4ac0bd1f..f7170200 100644 --- a/functions/src/platform/alexa/response/index.js +++ b/functions/src/platform/alexa/response/index.js @@ -52,7 +52,8 @@ module.exports = (alexa) => m.contentURL, // expectedPreviousToken previousToken, - 0 + // offsetInMilliseconds + m.offset ); }); } else { diff --git a/functions/tests/actions/high-order-handlers/middlewares/play-song.spec.js b/functions/tests/actions/high-order-handlers/middlewares/play-song.spec.js index 2499ec74..e5875305 100644 --- a/functions/tests/actions/high-order-handlers/middlewares/play-song.spec.js +++ b/functions/tests/actions/high-order-handlers/middlewares/play-song.spec.js @@ -28,7 +28,7 @@ describe('actions', () => { const slots = { id: '123456', }; - return middleware()({app, description, speech, slots}) + return middleware({offset: 1234})({app, description, speech, slots}) .then(context => { expect(dialog.playSong).to.have.been.called; expect(dialog.playSong.args[0][0]).to.be.equal(app); @@ -36,6 +36,7 @@ describe('actions', () => { description, id: '123456', mediaResponseOnly: false, + offset: 1234, speech: speech[0], }); }); diff --git a/functions/tests/platform/alexa/response/index.spec.js b/functions/tests/platform/alexa/response/index.spec.js index 04efea7b..69bf3c70 100644 --- a/functions/tests/platform/alexa/response/index.spec.js +++ b/functions/tests/platform/alexa/response/index.spec.js @@ -56,6 +56,7 @@ describe('platform', () => { description: 'some description', contentURL: 'https://archive.org/download/song.mp3', imageURL: 'https://archive.org/download/image.jpg', + offset: 0, }], suggestions: [ 'next song', @@ -80,6 +81,7 @@ describe('platform', () => { description: 'some description', contentURL: 'https://archive.org/download/song.mp3', imageURL: 'https://archive.org/download/image.jpg', + offset: 0, }], suggestions: [ 'next song', @@ -123,6 +125,7 @@ describe('platform', () => { description: 'some description', contentURL: 'https://archive.org/download/new-track.mp3', imageURL: 'https://archive.org/download/image.jpg', + offset: 12345, previousTrack: { contentURL: 'https://archive.org/download/old-track.mp3', }, @@ -135,7 +138,7 @@ describe('platform', () => { 'https://archive.org/download/new-track.mp3', 'https://archive.org/download/new-track.mp3', 'https://archive.org/download/old-track.mp3', - 0 + 12345 ); }); });