From d3a7b32ced5b099fbcfa9e78cf87c24ec347c520 Mon Sep 17 00:00:00 2001 From: Young Rok Kim Date: Sun, 24 Sep 2017 01:57:04 +0900 Subject: [PATCH 01/79] joi: add missing 'alt' method --- types/joi/index.d.ts | 10 ++++++++-- types/joi/joi-tests.ts | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/types/joi/index.d.ts b/types/joi/index.d.ts index 1b4c3fcbaf..68eb27ca93 100644 --- a/types/joi/index.d.ts +++ b/types/joi/index.d.ts @@ -551,7 +551,7 @@ export interface StringSchema extends AnySchema { * Requires the string value to be a valid GUID. */ guid(options?: GuidOptions): this; - + /** * Alias for `guid` -- Requires the string value to be a valid GUID */ @@ -962,10 +962,15 @@ export function string(): StringSchema; /** * Generates a type that will match one of the provided alternative schemas */ -export function alternatives(): AlternativesSchema; export function alternatives(types: SchemaLike[]): AlternativesSchema; export function alternatives(...types: SchemaLike[]): AlternativesSchema; +/** + * Alias for `alternatives` + */ +export function alt(types: SchemaLike[]): AlternativesSchema; +export function alt(...types: SchemaLike[]): AlternativesSchema; + /** * Generates a placeholder schema for a schema that you would provide with the fn. * Supports the same methods of the any() type. @@ -1031,3 +1036,4 @@ export function extend(extention: Extension): any; * Returns a plain object representing the schema's rules and properties */ export function describe(schema: Schema): Description; + diff --git a/types/joi/joi-tests.ts b/types/joi/joi-tests.ts index 4f49a69b3f..5f44e6ad01 100644 --- a/types/joi/joi-tests.ts +++ b/types/joi/joi-tests.ts @@ -830,6 +830,13 @@ schema = Joi.alternatives().try(schema, schema); schema = Joi.alternatives(schemaArr); schema = Joi.alternatives(schema, anySchema, boolSchema); +schema = Joi.alt(); +schema = Joi.alt().try(schemaArr); +schema = Joi.alt().try(schema, schema); + +schema = Joi.alt(schemaArr); +schema = Joi.alt(schema, anySchema, boolSchema); + // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- schema = Joi.lazy(() => schema) From d90627cfa9bd47a2f3dbbc5ccaf23472a4472909 Mon Sep 17 00:00:00 2001 From: Young Rok Kim Date: Sun, 24 Sep 2017 02:04:43 +0900 Subject: [PATCH 02/79] joi: revert f59617 --- types/joi/index.d.ts | 222 +++++++++++++++++++++++++++++++---------- types/joi/joi-tests.ts | 53 ++++++++++ 2 files changed, 222 insertions(+), 53 deletions(-) diff --git a/types/joi/index.d.ts b/types/joi/index.d.ts index 68eb27ca93..af07b0bb2a 100644 --- a/types/joi/index.d.ts +++ b/types/joi/index.d.ts @@ -37,10 +37,10 @@ export interface ValidationOptions { skipFunctions?: boolean; /** * remove unknown elements from objects and arrays. Defaults to false - * - when true, all unknown elements will be removed - * - when an object: - * - arrays - set to true to remove unknown items from arrays. - * - objects - set to true to remove unknown keys from objects + * when true, all unknown elements will be removed + * when an object: + * arrays set to true to remove unknown items from arrays. + * objects set to true to remove unknown keys from objects */ stripUnknown?: boolean | { arrays?: boolean; objects?: boolean }; /** @@ -291,7 +291,7 @@ export interface AnySchema extends JoiObject { /** * Sets a default value if the original value is undefined. - * @param value - the value. + * @param value the value. * value supports references. * value may also be a function which returns the default value. * If value is specified as a function that accepts a single parameter, that parameter will be a context @@ -333,22 +333,22 @@ export interface AnySchema extends JoiObject { /** * Considers anything that matches the schema to be empty (undefined). - * @param schema - any object or joi schema to match. An undefined schema unsets that rule. + * @param schema any object or joi schema to match. An undefined schema unsets that rule. */ empty(schema?: SchemaLike): this; /** * Overrides the default joi error with a custom error if the rule fails where: - * @param err - can be: - * an instance of `Error` - the override error. + * @param err can be: + * an instance of `Error` the override error. * a `function(errors)`, taking an array of errors as argument, where it must either: - * return a `string` - substitutes the error message with this text + * return a `string` substitutes the error message with this text * return a single `object` or an `Array` of it, where: - * `type` - optional parameter providing the type of the error (eg. `number.min`). - * `message` - optional parameter if `template` is provided, containing the text of the error. - * `template` - optional parameter if `message` is provided, containing a template string, using the same format as usual joi language errors. - * `context` - optional parameter, to provide context to your error if you are using the `template`. - * return an `Error` - same as when you directly provide an `Error`, but you can customize the error message based on the errors. + * `type` optional parameter providing the type of the error (eg. `number.min`). + * `message` optional parameter if `template` is provided, containing the text of the error. + * `template` optional parameter if `message` is provided, containing a template string, using the same format as usual joi language errors. + * `context` optional parameter, to provide context to your error if you are using the `template`. + * return an `Error` same as when you directly provide an `Error`, but you can customize the error message based on the errors. * * Note that if you provide an `Error`, it will be returned as-is, unmodified and undecorated with any of the * normal joi error properties. If validation fails and another error is found before the error @@ -396,7 +396,7 @@ export interface BooleanSchema extends AnySchema { * Allows for additional values to be considered valid booleans by converting them to true during validation. * Accepts a value or an array of values. String comparisons are by default case insensitive, * see boolean.insensitive() to change this behavior. - * @param values - strings, numbers or arrays of them + * @param values strings, numbers or arrays of them */ truthy(...values: Array): this; @@ -404,7 +404,7 @@ export interface BooleanSchema extends AnySchema { * Allows for additional values to be considered valid booleans by converting them to false during validation. * Accepts a value or an array of values. String comparisons are by default case insensitive, * see boolean.insensitive() to change this behavior. - * @param values - strings, numbers or arrays of them + * @param values strings, numbers or arrays of them */ falsy(...values: Array): this; @@ -452,7 +452,7 @@ export interface NumberSchema extends AnySchema { /** * Specifies the maximum number of decimal places where: - * limit - the maximum number of decimal places allowed. + * limit the maximum number of decimal places allowed. */ precision(limit: number): this; @@ -480,16 +480,16 @@ export interface StringSchema extends AnySchema { /** * Specifies the minimum number string characters. - * @param limit - the minimum number of string characters required. It can also be a reference to another field. - * @param encoding - if specified, the string length is calculated in bytes using the provided encoding. + * @param limit the minimum number of string characters required. It can also be a reference to another field. + * @param encoding if specified, the string length is calculated in bytes using the provided encoding. */ min(limit: number, encoding?: string): this; min(limit: Reference, encoding?: string): this; /** * Specifies the maximum number of string characters. - * @param limit - the maximum number of string characters allowed. It can also be a reference to another field. - * @param encoding - if specified, the string length is calculated in bytes using the provided encoding. + * @param limit the maximum number of string characters allowed. It can also be a reference to another field. + * @param encoding if specified, the string length is calculated in bytes using the provided encoding. */ max(limit: number, encoding?: string): this; max(limit: Reference, encoding?: string): this; @@ -501,23 +501,23 @@ export interface StringSchema extends AnySchema { /** * Specifies the exact string length required - * @param limit - the required string length. It can also be a reference to another field. - * @param encoding - if specified, the string length is calculated in bytes using the provided encoding. + * @param limit the required string length. It can also be a reference to another field. + * @param encoding if specified, the string length is calculated in bytes using the provided encoding. */ length(limit: number, encoding?: string): this; length(limit: Reference, encoding?: string): this; /** * Defines a regular expression rule. - * @param pattern - a regular expression object the string value must match against. - * @param name - optional name for patterns (useful with multiple patterns). Defaults to 'required'. + * @param pattern a regular expression object the string value must match against. + * @param name optional name for patterns (useful with multiple patterns). Defaults to 'required'. */ regex(pattern: RegExp, name?: string): this; /** * Replace characters matching the given pattern with the specified replacement string where: - * @param pattern - a regular expression object to match against, or a string of which all occurrences will be replaced. - * @param replacement - the string that will replace the pattern. + * @param pattern a regular expression object to match against, or a string of which all occurrences will be replaced. + * @param replacement the string that will replace the pattern. */ replace(pattern: RegExp, replacement: string): this; replace(pattern: string, replacement: string): this; @@ -553,7 +553,7 @@ export interface StringSchema extends AnySchema { guid(options?: GuidOptions): this; /** - * Alias for `guid` -- Requires the string value to be a valid GUID + * Alias for `guid`- Requires the string value to be a valid GUID */ uuid(options?: GuidOptions): this; @@ -610,14 +610,14 @@ export interface ArraySchema extends AnySchema { * Errors will contain the number of items that didn't match. * Any unmatched item having a label will be mentioned explicitly. * - * @param type - a joi schema object to validate each array item against. + * @param type a joi schema object to validate each array item against. */ items(...types: SchemaLike[]): this; items(types: SchemaLike[]): this; /** * Lists the types in sequence order for the array values where: - * @param type - a joi schema object to validate against each array item in sequence order. type can be an array of values, or multiple values can be passed as individual arguments. + * @param type a joi schema object to validate against each array item in sequence order. type can be an array of values, or multiple values can be passed as individual arguments. * If a given type is .required() then there must be a matching item with the same index position in the array. Errors will contain the number of items that didn't match. Any unmatched item having a label will be mentioned explicitly. */ ordered(...types: SchemaLike[]): this; @@ -675,7 +675,7 @@ export interface ObjectSchema extends AnySchema { /** * Defines an all-or-nothing relationship between keys where if one of the peers is present, all of them are required as well. - * @param peers - the key names of which if one present, all are required. peers can be a single string value, + * @param peers the key names of which if one present, all are required. peers can be a single string value, * an array of string values, or each peer provided as an argument. */ and(...peers: string[]): this; @@ -683,7 +683,7 @@ export interface ObjectSchema extends AnySchema { /** * Defines a relationship between keys where not all peers can be present at the same time. - * @param peers - the key names of which if one present, the others may not all be present. + * @param peers the key names of which if one present, the others may not all be present. * peers can be a single string value, an array of string values, or each peer provided as an argument. */ nand(...peers: string[]): this; @@ -732,15 +732,15 @@ export interface ObjectSchema extends AnySchema { /** * Requires the object to be an instance of a given constructor. * - * @param constructor - the constructor function that the object must be an instance of. - * @param name - an alternate name to use in validation errors. This is useful when the constructor function does not have a name. + * @param constructor the constructor function that the object must be an instance of. + * @param name an alternate name to use in validation errors. This is useful when the constructor function does not have a name. */ type(constructor: Function, name?: string): this; /** * Sets the specified children to required. * - * @param children - can be a single string value, an array of string values, or each child provided as an argument. + * @param children can be a single string value, an array of string values, or each child provided as an argument. * * var schema = Joi.object().keys({ a: { b: Joi.number() }, c: { d: Joi.string() } }); * var requiredSchema = schema.requiredKeys('', 'a.b', 'c', 'c.d'); @@ -753,7 +753,7 @@ export interface ObjectSchema extends AnySchema { /** * Sets the specified children to optional. * - * @param children - can be a single string value, an array of string values, or each child provided as an argument. + * @param children can be a single string value, an array of string values, or each child provided as an argument. * * The behavior is exactly the same as requiredKeys. */ @@ -809,7 +809,7 @@ export interface DateSchema extends AnySchema { /** * Specifies the allowed date format: - * @param format - string or array of strings that follow the moment.js format. + * @param format string or array of strings that follow the moment.js format. */ format(format: string): this; format(format: string[]): this; @@ -821,7 +821,7 @@ export interface DateSchema extends AnySchema { /** * Requires the value to be a timestamp interval from Unix Time. - * @param type - the type of timestamp (allowed values are unix or javascript [default]) + * @param type the type of timestamp (allowed values are unix or javascript [default]) */ timestamp(type?: 'javascript' | 'unix'): this; } @@ -830,19 +830,19 @@ export interface FunctionSchema extends AnySchema { /** * Specifies the arity of the function where: - * @param n - the arity expected. + * @param n the arity expected. */ arity(n: number): this; /** * Specifies the minimal arity of the function where: - * @param n - the minimal arity expected. + * @param n the minimal arity expected. */ minArity(n: number): this; /** * Specifies the minimal arity of the function where: - * @param n - the minimal arity expected. + * @param n the minimal arity expected. */ maxArity(n: number): this; @@ -875,17 +875,17 @@ export type ExtensionBoundSchema = Schema & { /** * Creates a joi error object. * Used in conjunction with custom rules. - * @param type - the type of rule to create the error for. - * @param context - provide properties that will be available in the `language` templates. - * @param state - should the context passed into the `validate` function in a custom rule - * @param options - should the context passed into the `validate` function in a custom rule + * @param type the type of rule to create the error for. + * @param context provide properties that will be available in the `language` templates. + * @param state should the context passed into the `validate` function in a custom rule + * @param options should the context passed into the `validate` function in a custom rule */ createError(type: string, context: Context, state: State, options: ValidationOptions): Err; } export interface Rules

{ name: string; - params?: ObjectSchema | { [key in keyof P]: SchemaLike; }; + params?: ObjectSchema | {[key in keyof P]: SchemaLike; }; setup?(this: ExtensionBoundSchema, params: P): Schema | void; validate?(this: ExtensionBoundSchema, params: P, value: any, state: State, options: ValidationOptions): Err | R; description?: string | ((params: P) => string); @@ -905,7 +905,7 @@ export interface Err extends JoiObject { toString(): string; } -// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- +//-------------------------------------------------- /** * Current version of the joi package. @@ -996,18 +996,18 @@ export function compile(schema: SchemaLike): T; /** * Validates a value against a schema and throws if validation fails. * - * @param value - the value to validate. - * @param schema - the schema object. - * @param message - optional message string prefix added in front of the error message. may also be an Error object. + * @param value the value to validate. + * @param schema the schema object. + * @param message optional message string prefix added in front of the error message. may also be an Error object. */ export function assert(value: any, schema: SchemaLike, message?: string | Error): void; /** * Validates a value against a schema, returns valid object, and throws if validation fails where: * - * @param value - the value to validate. - * @param schema - the schema object. - * @param message - optional message string prefix added in front of the error message. may also be an Error object. + * @param value the value to validate. + * @param schema the schema object. + * @param message optional message string prefix added in front of the error message. may also be an Error object. */ export function attempt(value: T, schema: SchemaLike, message?: string | Error): T; @@ -1032,8 +1032,124 @@ export function reach(schema: ObjectSchema, path: string): T; */ export function extend(extention: Extension): any; +// Below are undocumented APIs. use them at your own risk + /** * Returns a plain object representing the schema's rules and properties */ export function describe(schema: Schema): Description; +/** +* Whitelists a value +*/ +export function allow(value: any, ...values: any[]): Schema; +export function allow(values: any[]): Schema; + +/** + * Adds the provided values into the allowed whitelist and marks them as the only valid values allowed. + */ +export function valid(value: any, ...values: any[]): Schema; +export function valid(values: any[]): Schema; +export function only(value: any, ...values: any[]): Schema; +export function only(values: any[]): Schema; +export function equal(value: any, ...values: any[]): Schema; +export function equal(values: any[]): Schema; + +/** + * Blacklists a value + */ +export function invalid(value: any, ...values: any[]): Schema; +export function invalid(values: any[]): Schema; +export function disallow(value: any, ...values: any[]): Schema; +export function disallow(values: any[]): Schema; +export function not(value: any, ...values: any[]): Schema; +export function not(values: any[]): Schema; + +/** + * Marks a key as required which will not allow undefined as value. All keys are optional by default. + */ +export function required(): Schema; + +/** + * Marks a key as optional which will allow undefined as values. Used to annotate the schema for readability as all keys are optional by default. + */ +export function optional(): Schema; + +/** + * Marks a key as forbidden which will not allow any value except undefined. Used to explicitly forbid keys. + */ +export function forbidden(): Schema; + +/** + * Marks a key to be removed from a resulting object or array after validation. Used to sanitize output. + */ +export function strip(): Schema; + +/** + * Annotates the key + */ +export function description(desc: string): Schema; + +/** + * Annotates the key + */ +export function notes(notes: string): Schema; +export function notes(notes: string[]): Schema; + +/** + * Annotates the key + */ +export function tags(notes: string): Schema; +export function tags(notes: string[]): Schema; + +/** + * Attaches metadata to the key. + */ +export function meta(meta: Object): Schema; + +/** + * Annotates the key with an example value, must be valid. + */ +export function example(value: any): Schema; + +/** + * Annotates the key with an unit name. + */ +export function unit(name: string): Schema; + +/** + * Overrides the global validate() options for the current key and any sub-key. + */ +export function options(options: ValidationOptions): Schema; + +/** + * Sets the options.convert options to false which prevent type casting for the current key and any child keys. + */ +export function strict(isStrict?: boolean): Schema; + +/** + * Returns a new type that is the result of adding the rules of one type to another. + */ +export function concat(schema: T): T; + +/** + * Converts the type into an alternatives type where the conditions are merged into the type definition where: + */ +export function when(ref: string, options: WhenOptions): AlternativesSchema; +export function when(ref: Reference, options: WhenOptions): AlternativesSchema; + +/** + * Overrides the key name in error messages. + */ +export function label(name: string): Schema; + +/** + * Outputs the original untouched value instead of the casted value. + */ +export function raw(isRaw?: boolean): Schema; + +/** + * Considers anything that matches the schema to be empty (undefined). + * @param schema any object or joi schema to match. An undefined schema unsets that rule. + */ +export function empty(schema?: any): Schema; diff --git a/types/joi/joi-tests.ts b/types/joi/joi-tests.ts index 5f44e6ad01..f83196a983 100644 --- a/types/joi/joi-tests.ts +++ b/types/joi/joi-tests.ts @@ -957,3 +957,56 @@ const Joi3 = Joi.extend({ }, ], }); + +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + +schema = Joi.allow(x, x); +schema = Joi.allow([x, x, x]); +schema = Joi.valid(x); +schema = Joi.valid(x, x); +schema = Joi.valid([x, x, x]); +schema = Joi.only(x); +schema = Joi.only(x, x); +schema = Joi.only([x, x, x]); +schema = Joi.equal(x); +schema = Joi.equal(x, x); +schema = Joi.equal([x, x, x]); +schema = Joi.invalid(x); +schema = Joi.invalid(x, x); +schema = Joi.invalid([x, x, x]); +schema = Joi.disallow(x); +schema = Joi.disallow(x, x); +schema = Joi.disallow([x, x, x]); +schema = Joi.not(x); +schema = Joi.not(x, x); +schema = Joi.not([x, x, x]); + +schema = Joi.required(); +schema = Joi.optional(); +schema = Joi.forbidden(); +schema = Joi.strip(); + +schema = Joi.description(str); +schema = Joi.notes(str); +schema = Joi.notes(strArr); +schema = Joi.tags(str); +schema = Joi.tags(strArr); + +schema = Joi.meta(obj); +schema = Joi.example(obj); +schema = Joi.unit(str); + +schema = Joi.options(validOpts); +schema = Joi.strict(); +schema = Joi.strict(bool); +schema = Joi.concat(x); + +schema = Joi.when(str, whenOpts); +schema = Joi.when(ref, whenOpts); + +schema = Joi.label(str); +schema = Joi.raw(); +schema = Joi.raw(bool); +schema = Joi.empty(); +schema = Joi.empty(str); +schema = Joi.empty(anySchema); From 67356265a43372ed5b3364e74927d6199ba5c350 Mon Sep 17 00:00:00 2001 From: Young Rok Kim Date: Sun, 24 Sep 2017 02:32:41 +0900 Subject: [PATCH 03/79] joi: updated to v11.0.3 --- types/joi/index.d.ts | 142 ++++++++++++++++++++++++---------------- types/joi/joi-tests.ts | 72 ++++++++++++++++++-- types/joi/tsconfig.json | 2 +- 3 files changed, 153 insertions(+), 63 deletions(-) diff --git a/types/joi/index.d.ts b/types/joi/index.d.ts index af07b0bb2a..e71fa86a89 100644 --- a/types/joi/index.d.ts +++ b/types/joi/index.d.ts @@ -1,6 +1,13 @@ -// Type definitions for joi v10.4.2 +// Type definitions for joi v11.0.3 // Project: https://github.com/hapijs/joi -// Definitions by: Bart van der Schoor , Laurence Dougal Myers , Christopher Glantschnig , David Broder-Rodgers , Gael Magnan de Bornier , Rytis Alekna , Pavel Ivanov , Youngrok Kim +// Definitions by: Bart van der Schoor +// Laurence Dougal Myers +// Christopher Glantschnig +// David Broder-Rodgers +// Gael Magnan de Bornier +// Rytis Alekna +// Pavel Ivanov +// Youngrok Kim // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 2.4 @@ -37,10 +44,10 @@ export interface ValidationOptions { skipFunctions?: boolean; /** * remove unknown elements from objects and arrays. Defaults to false - * when true, all unknown elements will be removed - * when an object: - * arrays set to true to remove unknown items from arrays. - * objects set to true to remove unknown keys from objects + * - when true, all unknown elements will be removed + * - when an object: + * - arrays - set to true to remove unknown items from arrays. + * - objects - set to true to remove unknown keys from objects */ stripUnknown?: boolean | { arrays?: boolean; objects?: boolean }; /** @@ -158,7 +165,7 @@ export interface ValidationError extends Error, JoiObject { export interface ValidationErrorItem { message: string; type: string; - path: string; + path: string[]; options?: ValidationOptions; context?: Context; } @@ -192,6 +199,8 @@ export type Schema = AnySchema export interface AnySchema extends JoiObject { + schemaType?: Types | string; + /** * Validates a value using the schema and options. */ @@ -291,7 +300,7 @@ export interface AnySchema extends JoiObject { /** * Sets a default value if the original value is undefined. - * @param value the value. + * @param value - the value. * value supports references. * value may also be a function which returns the default value. * If value is specified as a function that accepts a single parameter, that parameter will be a context @@ -333,22 +342,22 @@ export interface AnySchema extends JoiObject { /** * Considers anything that matches the schema to be empty (undefined). - * @param schema any object or joi schema to match. An undefined schema unsets that rule. + * @param schema - any object or joi schema to match. An undefined schema unsets that rule. */ empty(schema?: SchemaLike): this; /** * Overrides the default joi error with a custom error if the rule fails where: - * @param err can be: - * an instance of `Error` the override error. + * @param err - can be: + * an instance of `Error` - the override error. * a `function(errors)`, taking an array of errors as argument, where it must either: - * return a `string` substitutes the error message with this text - * return a single `object` or an `Array` of it, where: - * `type` optional parameter providing the type of the error (eg. `number.min`). - * `message` optional parameter if `template` is provided, containing the text of the error. - * `template` optional parameter if `message` is provided, containing a template string, using the same format as usual joi language errors. - * `context` optional parameter, to provide context to your error if you are using the `template`. - * return an `Error` same as when you directly provide an `Error`, but you can customize the error message based on the errors. + * return a `string` - substitutes the error message with this text + * return a single ` object` or an `Array` of it, where: + * `type` - optional parameter providing the type of the error (eg. `number.min`). + * `message` - optional parameter if `template` is provided, containing the text of the error. + * `template` - optional parameter if `message` is provided, containing a template string, using the same format as usual joi language errors. + * `context` - optional parameter, to provide context to your error if you are using the `template`. + * return an `Error` - same as when you directly provide an `Error`, but you can customize the error message based on the errors. * * Note that if you provide an `Error`, it will be returned as-is, unmodified and undecorated with any of the * normal joi error properties. If validation fails and another error is found before the error @@ -396,7 +405,7 @@ export interface BooleanSchema extends AnySchema { * Allows for additional values to be considered valid booleans by converting them to true during validation. * Accepts a value or an array of values. String comparisons are by default case insensitive, * see boolean.insensitive() to change this behavior. - * @param values strings, numbers or arrays of them + * @param values - strings, numbers or arrays of them */ truthy(...values: Array): this; @@ -404,7 +413,7 @@ export interface BooleanSchema extends AnySchema { * Allows for additional values to be considered valid booleans by converting them to false during validation. * Accepts a value or an array of values. String comparisons are by default case insensitive, * see boolean.insensitive() to change this behavior. - * @param values strings, numbers or arrays of them + * @param values - strings, numbers or arrays of them */ falsy(...values: Array): this; @@ -452,7 +461,7 @@ export interface NumberSchema extends AnySchema { /** * Specifies the maximum number of decimal places where: - * limit the maximum number of decimal places allowed. + * @param limit - the maximum number of decimal places allowed. */ precision(limit: number): this; @@ -480,16 +489,16 @@ export interface StringSchema extends AnySchema { /** * Specifies the minimum number string characters. - * @param limit the minimum number of string characters required. It can also be a reference to another field. - * @param encoding if specified, the string length is calculated in bytes using the provided encoding. + * @param limit - the minimum number of string characters required. It can also be a reference to another field. + * @param encoding - if specified, the string length is calculated in bytes using the provided encoding. */ min(limit: number, encoding?: string): this; min(limit: Reference, encoding?: string): this; /** * Specifies the maximum number of string characters. - * @param limit the maximum number of string characters allowed. It can also be a reference to another field. - * @param encoding if specified, the string length is calculated in bytes using the provided encoding. + * @param limit - the maximum number of string characters allowed. It can also be a reference to another field. + * @param encoding - if specified, the string length is calculated in bytes using the provided encoding. */ max(limit: number, encoding?: string): this; max(limit: Reference, encoding?: string): this; @@ -501,23 +510,23 @@ export interface StringSchema extends AnySchema { /** * Specifies the exact string length required - * @param limit the required string length. It can also be a reference to another field. - * @param encoding if specified, the string length is calculated in bytes using the provided encoding. + * @param limit - the required string length. It can also be a reference to another field. + * @param encoding - if specified, the string length is calculated in bytes using the provided encoding. */ length(limit: number, encoding?: string): this; length(limit: Reference, encoding?: string): this; /** * Defines a regular expression rule. - * @param pattern a regular expression object the string value must match against. - * @param name optional name for patterns (useful with multiple patterns). Defaults to 'required'. + * @param pattern - a regular expression object the string value must match against. + * @param name - optional name for patterns (useful with multiple patterns). Defaults to 'required'. */ regex(pattern: RegExp, name?: string): this; /** * Replace characters matching the given pattern with the specified replacement string where: - * @param pattern a regular expression object to match against, or a string of which all occurrences will be replaced. - * @param replacement the string that will replace the pattern. + * @param pattern - a regular expression object to match against, or a string of which all occurrences will be replaced. + * @param replacement - the string that will replace the pattern. */ replace(pattern: RegExp, replacement: string): this; replace(pattern: string, replacement: string): this; @@ -553,7 +562,7 @@ export interface StringSchema extends AnySchema { guid(options?: GuidOptions): this; /** - * Alias for `guid`- Requires the string value to be a valid GUID + * Alias for `guid` -- Requires the string value to be a valid GUID */ uuid(options?: GuidOptions): this; @@ -610,14 +619,14 @@ export interface ArraySchema extends AnySchema { * Errors will contain the number of items that didn't match. * Any unmatched item having a label will be mentioned explicitly. * - * @param type a joi schema object to validate each array item against. + * @param type - a joi schema object to validate each array item against. */ items(...types: SchemaLike[]): this; items(types: SchemaLike[]): this; /** * Lists the types in sequence order for the array values where: - * @param type a joi schema object to validate against each array item in sequence order. type can be an array of values, or multiple values can be passed as individual arguments. + * @param type - a joi schema object to validate against each array item in sequence order. type can be an array of values, or multiple values can be passed as individual arguments. * If a given type is .required() then there must be a matching item with the same index position in the array. Errors will contain the number of items that didn't match. Any unmatched item having a label will be mentioned explicitly. */ ordered(...types: SchemaLike[]): this; @@ -648,6 +657,7 @@ export interface ArraySchema extends AnySchema { } export interface ObjectSchema extends AnySchema { + /** * Sets the allowed object keys. */ @@ -675,7 +685,7 @@ export interface ObjectSchema extends AnySchema { /** * Defines an all-or-nothing relationship between keys where if one of the peers is present, all of them are required as well. - * @param peers the key names of which if one present, all are required. peers can be a single string value, + * @param peers - the key names of which if one present, all are required. peers can be a single string value, * an array of string values, or each peer provided as an argument. */ and(...peers: string[]): this; @@ -683,7 +693,7 @@ export interface ObjectSchema extends AnySchema { /** * Defines a relationship between keys where not all peers can be present at the same time. - * @param peers the key names of which if one present, the others may not all be present. + * @param peers - the key names of which if one present, the others may not all be present. * peers can be a single string value, an array of string values, or each peer provided as an argument. */ nand(...peers: string[]): this; @@ -732,15 +742,15 @@ export interface ObjectSchema extends AnySchema { /** * Requires the object to be an instance of a given constructor. * - * @param constructor the constructor function that the object must be an instance of. - * @param name an alternate name to use in validation errors. This is useful when the constructor function does not have a name. + * @param constructor - the constructor function that the object must be an instance of. + * @param name - an alternate name to use in validation errors. This is useful when the constructor function does not have a name. */ type(constructor: Function, name?: string): this; /** * Sets the specified children to required. * - * @param children can be a single string value, an array of string values, or each child provided as an argument. + * @param children - can be a single string value, an array of string values, or each child provided as an argument. * * var schema = Joi.object().keys({ a: { b: Joi.number() }, c: { d: Joi.string() } }); * var requiredSchema = schema.requiredKeys('', 'a.b', 'c', 'c.d'); @@ -753,7 +763,7 @@ export interface ObjectSchema extends AnySchema { /** * Sets the specified children to optional. * - * @param children can be a single string value, an array of string values, or each child provided as an argument. + * @param children - can be a single string value, an array of string values, or each child provided as an argument. * * The behavior is exactly the same as requiredKeys. */ @@ -809,7 +819,7 @@ export interface DateSchema extends AnySchema { /** * Specifies the allowed date format: - * @param format string or array of strings that follow the moment.js format. + * @param format - string or array of strings that follow the moment.js format. */ format(format: string): this; format(format: string[]): this; @@ -821,7 +831,7 @@ export interface DateSchema extends AnySchema { /** * Requires the value to be a timestamp interval from Unix Time. - * @param type the type of timestamp (allowed values are unix or javascript [default]) + * @param type - the type of timestamp (allowed values are unix or javascript [default]) */ timestamp(type?: 'javascript' | 'unix'): this; } @@ -830,19 +840,19 @@ export interface FunctionSchema extends AnySchema { /** * Specifies the arity of the function where: - * @param n the arity expected. + * @param n - the arity expected. */ arity(n: number): this; /** * Specifies the minimal arity of the function where: - * @param n the minimal arity expected. + * @param n - the minimal arity expected. */ minArity(n: number): this; /** * Specifies the minimal arity of the function where: - * @param n the minimal arity expected. + * @param n - the minimal arity expected. */ maxArity(n: number): this; @@ -875,10 +885,10 @@ export type ExtensionBoundSchema = Schema & { /** * Creates a joi error object. * Used in conjunction with custom rules. - * @param type the type of rule to create the error for. - * @param context provide properties that will be available in the `language` templates. - * @param state should the context passed into the `validate` function in a custom rule - * @param options should the context passed into the `validate` function in a custom rule + * @param type - the type of rule to create the error for. + * @param context - provide properties that will be available in the `language` templates. + * @param state - should the context passed into the `validate` function in a custom rule + * @param options - should the context passed into the `validate` function in a custom rule */ createError(type: string, context: Context, state: State, options: ValidationOptions): Err; } @@ -905,7 +915,7 @@ export interface Err extends JoiObject { toString(): string; } -//-------------------------------------------------- +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- /** * Current version of the joi package. @@ -996,18 +1006,18 @@ export function compile(schema: SchemaLike): T; /** * Validates a value against a schema and throws if validation fails. * - * @param value the value to validate. - * @param schema the schema object. - * @param message optional message string prefix added in front of the error message. may also be an Error object. + * @param value - the value to validate. + * @param schema - the schema object. + * @param message - optional message string prefix added in front of the error message. may also be an Error object. */ export function assert(value: any, schema: SchemaLike, message?: string | Error): void; /** * Validates a value against a schema, returns valid object, and throws if validation fails where: * - * @param value the value to validate. - * @param schema the schema object. - * @param message optional message string prefix added in front of the error message. may also be an Error object. + * @param value - the value to validate. + * @param schema - the schema object. + * @param message - optional message string prefix added in front of the error message. may also be an Error object. */ export function attempt(value: T, schema: SchemaLike, message?: string | Error): T; @@ -1032,7 +1042,23 @@ export function reach(schema: ObjectSchema, path: string): T; */ export function extend(extention: Extension): any; -// Below are undocumented APIs. use them at your own risk +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + +import * as Module from 'joi'; +export type Root = typeof Module; +export type DefaultsFunction = (root: Schema) => Schema; + +/** + * Creates a new Joi instance that will apply defaults onto newly created schemas + * through the use of the fn function that takes exactly one argument, the schema being created. + * + * @param fn - The function must always return a schema, even if untransformed. + */ +export function defaults(fn: DefaultsFunction): Root; + +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- +// Below are undocumented APIs. use at your own risk +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- /** * Returns a plain object representing the schema's rules and properties @@ -1150,6 +1176,6 @@ export function raw(isRaw?: boolean): Schema; /** * Considers anything that matches the schema to be empty (undefined). - * @param schema any object or joi schema to match. An undefined schema unsets that rule. + * @param schema - any object or joi schema to match. An undefined schema unsets that rule. */ export function empty(schema?: any): Schema; diff --git a/types/joi/joi-tests.ts b/types/joi/joi-tests.ts index f83196a983..939059bdd3 100644 --- a/types/joi/joi-tests.ts +++ b/types/joi/joi-tests.ts @@ -136,13 +136,13 @@ var validErrFunc: Joi.ValidationErrorFunction; validErrItem = { message: str, type: str, - path: str + path: [str] }; validErrItem = { message: str, type: str, - path: str, + path: [str], options: validOpts, context: obj }; @@ -851,14 +851,14 @@ namespace validate_tests { Joi.validate(value, schema, validOpts, (err, value) => { x = value; str = err.message; - str = err.details[0].path; + str = err.details[0].path[0]; str = err.details[0].message; str = err.details[0].type; }); Joi.validate(value, schema, (err, value) => { x = value; str = err.message; - str = err.details[0].path; + str = err.details[0].path.join('.'); str = err.details[0].message; str = err.details[0].type; }); @@ -960,6 +960,70 @@ const Joi3 = Joi.extend({ // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- +const defaultsJoi = Joi.defaults((schema) => { + switch (schema.schemaType) { + case 'string': + return schema.allow(''); + case 'object': + return (schema as Joi.ObjectSchema).min(1); + default: + return schema; + } +}); + +schema = Joi.allow(x, x); +schema = Joi.allow([x, x, x]); +schema = Joi.valid(x); +schema = Joi.valid(x, x); +schema = Joi.valid([x, x, x]); +schema = Joi.only(x); +schema = Joi.only(x, x); +schema = Joi.only([x, x, x]); +schema = Joi.equal(x); +schema = Joi.equal(x, x); +schema = Joi.equal([x, x, x]); +schema = Joi.invalid(x); +schema = Joi.invalid(x, x); +schema = Joi.invalid([x, x, x]); +schema = Joi.disallow(x); +schema = Joi.disallow(x, x); +schema = Joi.disallow([x, x, x]); +schema = Joi.not(x); +schema = Joi.not(x, x); +schema = Joi.not([x, x, x]); + +schema = Joi.required(); +schema = Joi.optional(); +schema = Joi.forbidden(); +schema = Joi.strip(); + +schema = Joi.description(str); +schema = Joi.notes(str); +schema = Joi.notes(strArr); +schema = Joi.tags(str); +schema = Joi.tags(strArr); + +schema = Joi.meta(obj); +schema = Joi.example(obj); +schema = Joi.unit(str); + +schema = Joi.options(validOpts); +schema = Joi.strict(); +schema = Joi.strict(bool); +schema = Joi.concat(x); + +schema = Joi.when(str, whenOpts); +schema = Joi.when(ref, whenOpts); + +schema = Joi.label(str); +schema = Joi.raw(); +schema = Joi.raw(bool); +schema = Joi.empty(); +schema = Joi.empty(str); +schema = Joi.empty(anySchema); + +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + schema = Joi.allow(x, x); schema = Joi.allow([x, x, x]); schema = Joi.valid(x); diff --git a/types/joi/tsconfig.json b/types/joi/tsconfig.json index 8d3ca40052..a45ccb7c63 100644 --- a/types/joi/tsconfig.json +++ b/types/joi/tsconfig.json @@ -19,4 +19,4 @@ "index.d.ts", "joi-tests.ts" ] -} \ No newline at end of file +} From 9839cce53d7398d40f2a7d04270944a0065e4a9b Mon Sep 17 00:00:00 2001 From: Joel Hegg Date: Wed, 4 Oct 2017 19:38:10 -0400 Subject: [PATCH 04/79] Add type defintions for actions-on-google --- .../actions-on-google-tests.ts | 33 + types/actions-on-google/actions-sdk-app.d.ts | 408 +++++ types/actions-on-google/api-ai-app.d.ts | 572 +++++++ types/actions-on-google/assistant-app.d.ts | 1399 ++++++++++++++++ types/actions-on-google/index.d.ts | 39 + types/actions-on-google/response-builder.d.ts | 333 ++++ types/actions-on-google/transactions.d.ts | 1406 +++++++++++++++++ types/actions-on-google/tsconfig.json | 27 + types/actions-on-google/tslint.json | 1 + 9 files changed, 4218 insertions(+) create mode 100644 types/actions-on-google/actions-on-google-tests.ts create mode 100644 types/actions-on-google/actions-sdk-app.d.ts create mode 100644 types/actions-on-google/api-ai-app.d.ts create mode 100644 types/actions-on-google/assistant-app.d.ts create mode 100644 types/actions-on-google/index.d.ts create mode 100644 types/actions-on-google/response-builder.d.ts create mode 100644 types/actions-on-google/transactions.d.ts create mode 100644 types/actions-on-google/tsconfig.json create mode 100644 types/actions-on-google/tslint.json diff --git a/types/actions-on-google/actions-on-google-tests.ts b/types/actions-on-google/actions-on-google-tests.ts new file mode 100644 index 0000000000..b7632217e2 --- /dev/null +++ b/types/actions-on-google/actions-on-google-tests.ts @@ -0,0 +1,33 @@ +import { ActionsSdkApp, ActionsSdkAppOptions, ApiAiApp, ApiAiAppOptions, AssistantApp, + Responses, Transactions } from 'actions-on-google'; +import * as express from 'express'; + +function testActionsSdk(request: express.Request, response: express.Response) { + const app = new ActionsSdkApp({request, response}); + const actionMap = new Map(); + actionMap.set(app.StandardIntents.MAIN, () => { + const richResponse: Responses.RichResponse = app.buildRichResponse() + .addSimpleResponse('Hello world') + .addSuggestions(['foo', 'bar']); + app.ask(richResponse); + }); + app.handleRequest(actionMap); +} + +function testApiAi(request: express.Request, response: express.Response) { + const app = new ApiAiApp({request, response}); + const actionMap = new Map(); + actionMap.set(app.StandardIntents.MAIN, () => { + const order: Transactions.Order = app.buildOrder('foo'); + app.askForTransactionDecision(order, { + type: app.Transactions.PaymentType.PAYMENT_CARD, + displayName: 'VISA-1234', + deliveryAddressRequired: true + }); + }); + app.handleRequest(actionMap); +} + +const expressApp = express(); +expressApp.get('/actionssdk', (req, res) => testActionsSdk); +expressApp.get('/apiai', (req, res) => testApiAi); diff --git a/types/actions-on-google/actions-sdk-app.d.ts b/types/actions-on-google/actions-sdk-app.d.ts new file mode 100644 index 0000000000..be399b4783 --- /dev/null +++ b/types/actions-on-google/actions-sdk-app.d.ts @@ -0,0 +1,408 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Request, Response } from 'express'; + +import { AssistantApp, DeviceLocation, SessionStartedFunction, User } from './assistant-app'; +import { Carousel, List, RichResponse, SimpleResponse } from './response-builder'; +import { TransactionDecision } from './transactions'; + +// --------------------------------------------------------------------------- +// Actions SDK support +// --------------------------------------------------------------------------- + +export interface ActionsSdkAppOptions { + request: Request; + response: Response; + sessionStarted?: SessionStartedFunction; +} + +/** + * This is the class that handles the conversation API directly from Assistant, + * providing implementation for all the methods available in the API. + */ +export class ActionsSdkApp extends AssistantApp { + /** + * Constructor for ActionsSdkApp object. + * To be used in the Actions SDK HTTP endpoint logic. + * + * @example + * const ActionsSdkApp = require('actions-on-google').ActionsSdkApp; + * const app = new ActionsSdkApp({request: request, response: response, + * sessionStarted:sessionStarted}); + * + * @param {Object} options JSON configuration. + * @param {Object} options.request Express HTTP request object. + * @param {Object} options.response Express HTTP response object. + * @param {Function=} options.sessionStarted Function callback when session starts. + * @actionssdk + */ + constructor(options: ActionsSdkAppOptions); + + /** + * Validates whether request is from Assistant through signature verification. + * Uses Google-Auth-Library to verify authorization token against given + * Google Cloud Project ID. Auth token is given in request header with key, + * "Authorization". + * + * @example + * const app = new ActionsSdkApp({request, response}); + * app.isRequestFromAssistant('nodejs-cloud-test-project-1234') + * .then(() => { + * app.ask('Hey there, thanks for stopping by!'); + * }) + * .catch(err => { + * response.status(400).send(); + * }); + * + * @param {string} projectId Google Cloud Project ID for the Assistant app. + * @param {Promise} Promise resolving with ID token if request is from + * a valid source, otherwise rejects with the error reason for an invalid + * token. + * @actionssdk + */ + isRequestFromAssistant(projectId: string): Promise; + + /** + * Gets the request Conversation API version. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * const apiVersion = app.getApiVersion(); + * + * @return {string} Version value or null if no value. + * @actionssdk + */ + getApiVersion(): string; + + /** + * Gets the user's raw input query. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * app.tell('You said ' + app.getRawInput()); + * + * @return {string} User's raw query or null if no value. + * @actionssdk + */ + getRawInput(): string; + + /** + * Gets previous JSON dialog state that the app sent to Assistant. + * Alternatively, use the app.data field to store JSON values between requests. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * const dialogState = app.getDialogState(); + * + * @return {Object} JSON object provided to the Assistant in the previous + * user turn or {} if no value. + * @actionssdk + */ + getDialogState(): object; + + /** + * Gets the "versionLabel" specified inside the Action Package. + * Used by app to do version control. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * const actionVersionLabel = app.getActionVersionLabel(); + * + * @return {string} The specified version label or null if unspecified. + * @actionssdk + */ + getActionVersionLabel(): string; + + /** + * Gets the unique conversation ID. It's a new ID for the initial query, + * and stays the same until the end of the conversation. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * const conversationId = app.getConversationId(); + * + * @return {string} Conversation ID or null if no value. + * @actionssdk + */ + getConversationId(): string; + + /** + * Get the current intent. Alternatively, using a handler Map with + * {@link AssistantApp#handleRequest|handleRequest}, the client library will + * automatically handle the incoming intents. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * + * function responseHandler (app) { + * const intent = app.getIntent(); + * switch (intent) { + * case app.StandardIntents.MAIN: + * const inputPrompt = app.buildInputPrompt(false, 'Welcome to action snippets! Say anything.'); + * app.ask(inputPrompt); + * break; + * + * case app.StandardIntents.TEXT: + * app.tell('You said ' + app.getRawInput()); + * break; + * } + * } + * + * app.handleRequest(responseHandler); + * + * @return {string} Intent id or null if no value. + * @actionssdk + */ + getIntent(): string; + + /** + * Get the argument value by name from the current intent. If the argument + * is not a text argument, the entire argument object is returned. + * + * Note: If incoming request is using an API version under 2 (e.g. 'v1'), + * the argument object will be in Proto2 format (snake_case, etc). + * + * @param {string} argName Name of the argument. + * @return {string} Argument value matching argName + * or null if no matching argument. + * @actionssdk + */ + getArgument(argName: string): string; + + /** + * Returns the option key user chose from options response. + * + * @example + * const app = new App({request: req, response: res}); + * + * function pickOption (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askWithCarousel('Which of these looks good?', + * app.buildCarousel().addItems( + * app.buildOptionItem('another_choice', ['Another choice']). + * setTitle('Another choice').setDescription('Choose me!'))); + * } else { + * app.ask('What would you like?'); + * } + * } + * + * function optionPicked (app) { + * app.ask('You picked ' + app.getSelectedOption()); + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.TEXT, pickOption); + * actionMap.set(app.StandardIntents.OPTION, optionPicked); + * + * app.handleRequest(actionMap); + * + * @return {string} Option key of selected item. Null if no option selected or + * if current intent is not OPTION intent. + * @actionssdk + */ + getSelectedOption(): string; + + /** + * Asks to collect user's input; all user's queries need to be sent to + * the app. + * {@link https://developers.google.com/actions/policies/general-policies#user_experience|The guidelines when prompting the user for a response must be followed at all times}. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * + * function mainIntent (app) { + * const inputPrompt = app.buildInputPrompt(true, 'Hi! ' + + * 'I can read out an ordinal like ' + + * '123. Say a number.', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * + * function rawInput (app) { + * if (app.getRawInput() === 'bye') { + * app.tell('Goodbye!'); + * } else { + * const inputPrompt = app.buildInputPrompt(true, 'You said, ' + + * app.getRawInput() + '', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, mainIntent); + * actionMap.set(app.StandardIntents.TEXT, rawInput); + * + * app.handleRequest(actionMap); + * + * @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and + * no-input prompts. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by App. + * @return The response that is sent to Assistant to ask user to provide input. + * @actionssdk + */ + ask(inputPrompt: object | SimpleResponse | RichResponse, dialogState?: object): object; + + /** + * Asks to collect user's input with a list. + * + * @example + * const app = new ActionsSdkApp({request, response}); + * + * function welcomeIntent (app) { + * app.askWithlist('Which of these looks good?', + * app.buildList('List title') + * .addItems([ + * app.buildOptionItem(SELECTION_KEY_ONE, + * ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2']) + * .setTitle('Number one'), + * app.buildOptionItem(SELECTION_KEY_TWO, + * ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2']) + * .setTitle('Number two'), + * ])); + * } + * + * function optionIntent (app) { + * if (app.getSelectedOption() === SELECTION_KEY_ONE) { + * app.tell('Number one is a great choice!'); + * } else { + * app.tell('Number two is a great choice!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.TEXT, welcomeIntent); + * actionMap.set(app.StandardIntents.OPTION, optionIntent); + * app.handleRequest(actionMap); + * + * @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and + * no-input prompts. Cannot contain basic card. + * @param {List} list List built with {@link AssistantApp#buildList|buildList}. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return The response that is sent to Assistant to ask user to provide input. + * @actionssdk + */ + askWithList(inputPrompt: object | SimpleResponse | RichResponse, list: List, dialogState?: object): object; + + /** + * Asks to collect user's input with a carousel. + * + * @example + * const app = new ActionsSdkApp({request, response}); + * + * function welcomeIntent (app) { + * app.askWithCarousel('Which of these looks good?', + * app.buildCarousel() + * .addItems([ + * app.buildOptionItem(SELECTION_KEY_ONE, + * ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2']) + * .setTitle('Number one'), + * app.buildOptionItem(SELECTION_KEY_TWO, + * ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2']) + * .setTitle('Number two'), + * ])); + * } + * + * function optionIntent (app) { + * if (app.getSelectedOption() === SELECTION_KEY_ONE) { + * app.tell('Number one is a great choice!'); + * } else { + * app.tell('Number two is a great choice!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.TEXT, welcomeIntent); + * actionMap.set(app.StandardIntents.OPTION, optionIntent); + * app.handleRequest(actionMap); + * + * @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and + * no-input prompts. Cannot contain basic card. + * @param {Carousel} carousel Carousel built with + * {@link AssistantApp#buildCarousel|buildCarousel}. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return The response that is sent to Assistant to ask user to provide input. + * @actionssdk + */ + askWithCarousel(inputPrompt: object | SimpleResponse | RichResponse, carousel: Carousel, dialogState?: object): object; + + /** + * Tells Assistant to render the speech response and close the mic. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * + * function mainIntent (app) { + * const inputPrompt = app.buildInputPrompt(true, 'Hi! ' + + * 'I can read out an ordinal like ' + + * '123. Say a number.', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * + * function rawInput (app) { + * if (app.getRawInput() === 'bye') { + * app.tell('Goodbye!'); + * } else { + * const inputPrompt = app.buildInputPrompt(true, 'You said, ' + + * app.getRawInput() + '', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, mainIntent); + * actionMap.set(app.StandardIntents.TEXT, rawInput); + * + * app.handleRequest(actionMap); + * + * @param {string|SimpleResponse|RichResponse} textToSpeech Final response. + * Spoken response can be SSML. + * @return The HTTP response that is sent back to Assistant. + * @actionssdk + */ + tell(textToSpeech: string | SimpleResponse | RichResponse): object; + + /** + * Builds the {@link https://developers.google.com/actions/reference/conversation#InputPrompt|InputPrompt object} + * from initial prompt and no-input prompts. + * + * The App needs one initial prompt to start the conversation. If there is no user response, + * the App re-opens the mic and renders the no-input prompts three times + * (one for each no-input prompt that was configured) to help the user + * provide the right response. + * + * Note: we highly recommend app to provide all the prompts required here in order to ensure a + * good user experience. + * + * @example + * const inputPrompt = app.buildInputPrompt(false, 'Welcome to action snippets! Say a number.', + * ['Say any number', 'Pick a number', 'What is the number?']); + * app.ask(inputPrompt); + * + * @param {boolean} isSsml Indicates whether the text to speech is SSML or not. + * @param {string} initialPrompt The initial prompt the App asks the user. + * @param {Array=} noInputs Array of re-prompts when the user does not respond (max 3). + * @return {Object} An {@link https://developers.google.com/actions/reference/conversation#InputPrompt|InputPrompt object}. + * @actionssdk + */ + buildInputPrompt(isSsml: boolean, initialPrompt: string, noInputs: string[]): object; +} diff --git a/types/actions-on-google/api-ai-app.d.ts b/types/actions-on-google/api-ai-app.d.ts new file mode 100644 index 0000000000..b7dc28b86a --- /dev/null +++ b/types/actions-on-google/api-ai-app.d.ts @@ -0,0 +1,572 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Request, Response } from 'express'; + +import { AssistantApp, DeviceLocation, SessionStartedFunction, User } from './assistant-app'; +import { Carousel, List, RichResponse, SimpleResponse } from './response-builder'; +import { TransactionDecision } from './transactions'; + +// --------------------------------------------------------------------------- +// API.AI support +// --------------------------------------------------------------------------- + +/** + * API.AI {@link https://docs.api.ai/docs/concept-contexts|Context}. + */ +export interface Context { + /** Full name of the context. */ + name: string; + /** + * Parameters carried within this context. + * See {@link https://docs.api.ai/docs/concept-actions#section-extracting-values-from-contexts|here}. + */ + parameters: object; + /** Remaining number of intents */ + lifespan: number; +} + +export interface ApiAiAppOptions { + request: Request; + response: Response; + sessionStarted?: SessionStartedFunction; +} + +/** + * This is the class that handles the communication with API.AI's fulfillment API. + */ +export class ApiAiApp extends AssistantApp { + /** + * Constructor for ApiAiApp object. + * To be used in the API.AI fulfillment webhook logic. + * + * @example + * const ApiAiApp = require('actions-on-google').ApiAiApp; + * const app = new ApiAiApp({request: request, response: response, + * sessionStarted:sessionStarted}); + * + * @param {Object} options JSON configuration. + * @param {Object} options.request Express HTTP request object. + * @param {Object} options.response Express HTTP response object. + * @param {Function=} options.sessionStarted Function callback when session starts. + * Only called if webhook is enabled for welcome/triggering intents, and + * called from Web Simulator or Google Home device (i.e., not API.AI simulator). + * @apiai + */ + constructor(options: ApiAiAppOptions); + + /** + * Verifies whether the request comes from API.AI. + * + * @param {string} key The header key specified by the developer in the + * API.AI Fulfillment settings of the app. + * @param {string} value The private value specified by the developer inside the + * fulfillment header. + * @return {boolean} True if the request comes from API.AI. + * @apiai + */ + isRequestFromApiAi(key: string, value: string): boolean; + + /** + * Get the current intent. Alternatively, using a handler Map with + * {@link AssistantApp#handleRequest|handleRequest}, + * the client library will automatically handle the incoming intents. + * 'Intent' in the API.ai context translates into the current action. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * + * function responseHandler (app) { + * const intent = app.getIntent(); + * switch (intent) { + * case WELCOME_INTENT: + * app.ask('Welcome to action snippets! Say a number.'); + * break; + * + * case NUMBER_INTENT: + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * break; + * } + * } + * + * app.handleRequest(responseHandler); + * + * @return {string} Intent id or null if no value (action name). + * @apiai + */ + getIntent(): string; + + /** + * Get the argument value by name from the current intent. If the argument + * is included in originalRequest, and is not a text argument, the entire + * argument object is returned. + * + * Note: If incoming request is using an API version under 2 (e.g. 'v1'), + * the argument object will be in Proto2 format (snake_case, etc). + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * + * function welcomeIntent (app) { + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} argName Name of the argument. + * @return {Object} Argument value matching argName + * or null if no matching argument. + * @apiai + */ + getArgument(argName: string): object; + + /** + * Get the context argument value by name from the current intent. Context + * arguments include parameters collected in previous intents during the + * lifespan of the given context. If the context argument has an original + * value, usually representing the underlying entity value, that will be given + * as part of the return object. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * const OUT_CONTEXT = 'output_context'; + * const NUMBER_ARG = 'myNumberArg'; + * + * function welcomeIntent (app) { + * const parameters = {}; + * parameters[NUMBER_ARG] = '42'; + * app.setContext(OUT_CONTEXT, 1, parameters); + * app.ask('Welcome to action snippets! Ask me for your number.'); + * } + * + * function numberIntent (app) { + * const number = app.getContextArgument(OUT_CONTEXT, NUMBER_ARG); + * // number === { value: 42 } + * app.tell('Your number is ' + number.value); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} contextName Name of the context. + * @param {string} argName Name of the argument. + * @return {Object} Object containing value property and optional original + * property matching context argument. Null if no matching argument. + * @apiai + */ + getContextArgument(contextName: string, argName: string): object; + + /** + * Returns the RichResponse constructed in API.AI response builder. + * + * @example + * const app = new App({request: req, response: res}); + * + * function tellFact (app) { + * let fact = 'Google was founded in 1998'; + * + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.ask(app.getIncomingRichResponse().addSimpleResponse('Here\'s a ' + + * 'fact for you. ' + fact + ' Which one do you want to hear about ' + + * 'next, Google\'s history or headquarters?')); + * } else { + * app.ask('Here\'s a fact for you. ' + fact + ' Which one ' + + * 'do you want to hear about next, Google\'s history or headquarters?'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set('tell.fact', tellFact); + * + * app.handleRequest(actionMap); + * + * @return {RichResponse} RichResponse created in API.AI. If no RichResponse was + * created, an empty RichResponse is returned. + * @apiai + */ + getIncomingRichResponse(): RichResponse; + + /** + * Returns the List constructed in API.AI response builder. + * + * @example + * const app = new App({request: req, response: res}); + * + * function pickOption (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askWithList('Which of these looks good?', + * app.getIncomingList().addItems( + * app.buildOptionItem('another_choice', ['Another choice']). + * setTitle('Another choice'))); + * } else { + * app.ask('What would you like?'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set('pick.option', pickOption); + * + * app.handleRequest(actionMap); + * + * @return {List} List created in API.AI. If no List was created, an empty + * List is returned. + * @apiai + */ + getIncomingList(): List; + + /** + * Returns the Carousel constructed in API.AI response builder. + * + * @example + * const app = new App({request: req, response: res}); + * + * function pickOption (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askWithCarousel('Which of these looks good?', + * app.getIncomingCarousel().addItems( + * app.buildOptionItem('another_choice', ['Another choice']). + * setTitle('Another choice').setDescription('Choose me!'))); + * } else { + * app.ask('What would you like?'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set('pick.option', pickOption); + * + * app.handleRequest(actionMap); + * + * @return {Carousel} Carousel created in API.AI. If no Carousel was created, + * an empty Carousel is returned. + * @apiai + */ + getIncomingCarousel(): Carousel; + + /** + * Returns the option key user chose from options response. + * + * @example + * const app = new App({request: req, response: res}); + * + * function pickOption (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askWithCarousel('Which of these looks good?', + * app.getIncomingCarousel().addItems( + * app.buildOptionItem('another_choice', ['Another choice']). + * setTitle('Another choice').setDescription('Choose me!'))); + * } else { + * app.ask('What would you like?'); + * } + * } + * + * function optionPicked (app) { + * app.ask('You picked ' + app.getSelectedOption()); + * } + * + * const actionMap = new Map(); + * actionMap.set('pick.option', pickOption); + * actionMap.set('option.picked', optionPicked); + * + * app.handleRequest(actionMap); + * + * @return {string} Option key of selected item. Null if no option selected or + * if current intent is not OPTION intent. + * @apiai + */ + getSelectedOption(): string; + + /** + * Asks to collect the user's input. + * {@link https://developers.google.com/actions/policies/general-policies#user_experience|The guidelines when prompting the user for a response must be followed at all times}. + * + * NOTE: Due to a bug, if you specify the no-input prompts, + * the mic is closed after the 3rd prompt, so you should use the 3rd prompt + * for a bye message until the bug is fixed. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * + * function welcomeIntent (app) { + * app.ask('Welcome to action snippets! Say a number.', + * ['Say any number', 'Pick a number', 'We can stop here. See you soon.']); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string|SimpleResponse|RichResponse} inputPrompt The input prompt + * response. + * @param {Array=} noInputs Array of re-prompts when the user does not respond (max 3). + * @return {Object} HTTP response. + * @apiai + */ + ask(inputPrompt: string | SimpleResponse | RichResponse, noInputs?: string[]): object; + + /** + * Asks to collect the user's input with a list. + * + * @example + * const app = new ApiAiApp({request, response}); + * const WELCOME_INTENT = 'input.welcome'; + * const OPTION_INTENT = 'option.select'; + * + * function welcomeIntent (app) { + * app.askWithList('Which of these looks good?', + * app.buildList('List title') + * .addItems([ + * app.buildOptionItem(SELECTION_KEY_ONE, + * ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2']) + * .setTitle('Title of First List Item'), + * app.buildOptionItem(SELECTION_KEY_TWO, + * ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2']) + * .setTitle('Title of Second List Item'), + * ])); + * } + * + * function optionIntent (app) { + * if (app.getSelectedOption() === SELECTION_KEY_ONE) { + * app.tell('Number one is a great choice!'); + * } else { + * app.tell('Number two is a great choice!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(OPTION_INTENT, optionIntent); + * app.handleRequest(actionMap); + * + * @param {string|RichResponse|SimpleResponse} inputPrompt The input prompt + * response. + * @param {List} list List built with {@link AssistantApp#buildList|buildList}. + * @return {Object} HTTP response. + * @apiai + */ + askWithList(inputPrompt: string | RichResponse | SimpleResponse, list: List): object; + + /** + * Asks to collect the user's input with a carousel. + * + * @example + * const app = new ApiAiApp({request, response}); + * const WELCOME_INTENT = 'input.welcome'; + * const OPTION_INTENT = 'option.select'; + * + * function welcomeIntent (app) { + * app.askWithCarousel('Which of these looks good?', + * app.buildCarousel() + * .addItems([ + * app.buildOptionItem(SELECTION_KEY_ONE, + * ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2']) + * .setTitle('Number one'), + * app.buildOptionItem(SELECTION_KEY_TWO, + * ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2']) + * .setTitle('Number two'), + * ])); + * } + * + * function optionIntent (app) { + * if (app.getSelectedOption() === SELECTION_KEY_ONE) { + * app.tell('Number one is a great choice!'); + * } else { + * app.tell('Number two is a great choice!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(OPTION_INTENT, optionIntent); + * app.handleRequest(actionMap); + * + * @param {string|RichResponse|SimpleResponse} inputPrompt The input prompt + * response. + * @param {Carousel} carousel Carousel built with + * {@link AssistantApp#buildCarousel|buildCarousel}. + * @return {Object} HTTP response. + * @apiai + */ + askWithCarousel(inputPrompt: string | RichResponse | SimpleResponse, carousel: Carousel): object; + + /** + * Tells the Assistant to render the speech response and close the mic. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * + * function welcomeIntent (app) { + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string|SimpleResponse|RichResponse} textToSpeech Final response. + * Spoken response can be SSML. + * @return The response that is sent back to Assistant. + * @apiai + */ + tell(speechResponse: string | SimpleResponse | RichResponse): object; + + /** + * Set a new context for the current intent. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const CONTEXT_NUMBER = 'number'; + * const NUMBER_ARGUMENT = 'myNumber'; + * + * function welcomeIntent (app) { + * app.setContext(CONTEXT_NUMBER); + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} name Name of the context. API.AI converts to lowercase. + * @param {int} [lifespan=1] Context lifespan. + * @param {Object=} parameters Context JSON parameters. + * @apiai + */ + setContext(name: string, lifespan: number, parameters?: object): void; + + /** + * Returns the incoming contexts for this intent. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const CONTEXT_NUMBER = 'number'; + * const NUMBER_ARGUMENT = 'myNumber'; + * + * function welcomeIntent (app) { + * app.setContext(CONTEXT_NUMBER); + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * let contexts = app.getContexts(); + * // contexts === [{ + * // name: 'number', + * // lifespan: 0, + * // parameters: { + * // myNumber: '23', + * // myNumber.original: '23' + * // } + * // }] + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @return {Context[]} Empty if no active contexts. + * @apiai + */ + getContexts(): Context[]; + + /** + * Returns the incoming context by name for this intent. + * + * @example + * const app = new ApiAiapp({request: request, response: response}); + * const CONTEXT_NUMBER = 'number'; + * const NUMBER_ARGUMENT = 'myNumber'; + * + * function welcomeIntent (app) { + * app.setContext(CONTEXT_NUMBER); + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * let context = app.getContext(CONTEXT_NUMBER); + * // context === { + * // name: 'number', + * // lifespan: 0, + * // parameters: { + * // myNumber: '23', + * // myNumber.original: '23' + * // } + * // } + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @return {Object} Context value matching name + * or null if no matching context. + * @apiai + */ + getContext(name: string): object; + + /** + * Gets the user's raw input query. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * app.tell('You said ' + app.getRawInput()); + * + * @return {string} User's raw query or null if no value. + * @apiai + */ + getRawInput(): string; +} diff --git a/types/actions-on-google/assistant-app.d.ts b/types/actions-on-google/assistant-app.d.ts new file mode 100644 index 0000000000..01d3bcf046 --- /dev/null +++ b/types/actions-on-google/assistant-app.d.ts @@ -0,0 +1,1399 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Request, Response } from 'express'; + +import { BasicCard, Carousel, List, OptionItem, RichResponse } from './response-builder'; +import { ActionPaymentTransactionConfig, Cart, GooglePaymentTransactionConfig, + LineItem, Location, Order, OrderUpdate, TransactionDecision, + TransactionValues } from './transactions'; + +/** + * User provided date/time info. + */ +export interface DateTime { + date: { + year: number; + month: number; + day: number; + }; + time: { + hours: number; + minutes: number; + seconds: number; + nanos: number; + }; +} + +/** + * User's permissioned name info. + */ +export interface UserName { + /** User's display name. */ + displayName: string; + /** User's given name. */ + givenName: string; + /** User's family name. */ + familyName: string; +} + +/** + * User's permissioned device location. + */ +export interface DeviceLocation { + /** {latitude, longitude}. Requested with SupportedPermissions.DEVICE_PRECISE_LOCATION. */ + coordinates: object; + /** Full, formatted street address. Requested with SupportedPermissions.DEVICE_PRECISE_LOCATION. */ + address: string; + /** Zip code. Requested with SupportedPermissions.DEVICE_COARSE_LOCATION. */ + zipCode: string; + /** Device city. Requested with SupportedPermissions.DEVICE_COARSE_LOCATION. */ + city: string; +} + +/** + * User object. + */ +export interface User { + /** Random string ID for Google user. */ + userId: string; + /** User name information. Null if not requested with {@link AssistantApp#askForPermission|askForPermission(SupportedPermissions.NAME)}. */ + userName: UserName; + /** Unique Oauth2 token. Only available with account linking. */ + accessToken: string; +} + +/** + * Actions on Google Surface. + */ +export interface Surface { + /** Capabilities of the surface. */ + capabilities: Capability[]; +} + +/** + * Surface capability. + */ +export interface Capability { + /** Name of the capability. */ + name: string; +} + +/** + * List of standard intents that the app provides. + * @enum {string} + */ +export type StandardIntents = + /** App fires MAIN intent for queries like [talk to $app]. */ + 'actions.intent.MAIN' | 'assistant.intent.action.MAIN' | + /** App fires TEXT intent when action issues ask intent. */ + 'actions.intent.TEXT' | 'assistant.intent.action.TEXT' | + /** App fires PERMISSION intent when action invokes askForPermission. */ + 'actions.intent.PERMISSION' | 'assistant.intent.action.PERMISSION' | + /** App fires OPTION intent when user chooses from options provided. */ + 'actions.intent.OPTION' | + /** App fires TRANSACTION_REQUIREMENTS_CHECK intent when action sets up transaction. */ + 'actions.intent.TRANSACTION_REQUIREMENTS_CHECK' | + /** App fires DELIVERY_ADDRESS intent when action asks for delivery address. */ + 'actions.intent.DELIVERY_ADDRESS' | + /** App fires TRANSACTION_DECISION intent when action asks for transaction decision. */ + 'actions.intent.TRANSACTION_DECISION' | + /** App fires CONFIRMATION intent when requesting affirmation from user. */ + 'actions.intent.CONFIRMATION' | + /** App fires DATETIME intent when requesting date/time from user. */ + 'actions.intent.DATETIME' | + /** App fires SIGN_IN intent when requesting sign-in from user. */ + 'actions.intent.SIGN_IN' | + /** App fires NO_INPUT intent when user doesn't provide input. */ + 'actions.intent.NO_INPUT' | + /** App fires CANCEL intent when user exits app mid-dialog. */ + 'actions.intent.CANCEL' | + /** App fires NEW_SURFACE intent when requesting handoff to a new surface from user. */ + 'actions.intent.NEW_SURFACE'; + +/** + * List of supported permissions the app supports. + * @enum {string} + */ +export type SupportedPermissions = + /** + * The user's name as defined in the + * {@link https://developers.google.com/actions/reference/conversation#UserProfile|UserProfile object} + */ + 'NAME' | + /** + * The location of the user's current device, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Location|Location object}. + */ + 'DEVICE_PRECISE_LOCATION' | + /** + * City and zipcode corresponding to the location of the user's current device, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Location|Location object}. + */ + 'DEVICE_COARSE_LOCATION'; + +/** + * List of built-in argument names. + * @enum {string} + */ +export type BuiltInArgNames = + /** Permission granted argument. */ + 'PERMISSION' | 'permission_granted' | + /** Option selected argument. */ + 'OPTION' | + /** Transaction requirements check result argument. */ + 'TRANSACTION_REQUIREMENTS_CHECK_RESULT' | + /** Delivery address value argument. */ + 'DELIVERY_ADDRESS_VALUE' | + /** Transactions decision argument. */ + 'TRANSACTION_DECISION_VALUE' | + /** Confirmation argument. */ + 'CONFIRMATION' | + /** DateTime argument. */ + 'DATETIME' | + /** Sign in status argument. */ + 'SIGN_IN' | + /** Reprompt count for consecutive NO_INPUT intents. */ + 'REPROMPT_COUNT' | + /** Flag representing finality of NO_INPUT intent. */ + 'IS_FINAL_REPROMPT' | + /** New surface value argument. */ + 'NEW_SURFACE'; + +/** + * List of possible conversation stages, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Conversation|Conversation object}. + * @enum {number} + */ +export type ConversationStages = + /** + * Unspecified conversation state. + */ + 'UNSPECIFIED' | 0 | + /** + * A new conversation. + */ + 'NEW' | 1 | + /** + * An active (ongoing) conversation. + */ + 'ACTIVE' | 2; + +/** + * List of surface capabilities supported by the app. + * @enum {string} + */ +export type SurfaceCapabilities = + /** + * The ability to output audio. + */ + 'actions.capability.AUDIO_OUTPUT' | + /** + * The ability to output on a screen + */ + 'actions.capability.SCREEN_OUTPUT'; + +/** + * List of possible user input types. + * @enum {number} + */ +export type InputTypes = + /** + * Unspecified. + */ + 'UNSPECIFIED' | 0 | + /** + * Input given by touch. + */ + 'TOUCH' | 1 | + /** + * Input given by voice (spoken). + */ + 'VOICE' | 2 | + /** + * Input given by keyboard (typed). + */ + 'KEYBOARD' | 3; + +/** + * List of possible sign in result status values. + * @enum {string} + */ +export type SignInStatus = + // Unknown status. + 'SIGN_IN_STATUS_UNSPECIFIED' | + // User successfully completed the account linking. + 'OK' | + // Cancelled or dismissed account linking. + 'CANCELLED' | + // System or network error. + 'ERROR'; + +export type SessionStartedFunction = () => any; + +export interface AssistantAppOptions { + request: Request; + response: Response; + sessionStarted?: SessionStartedFunction; +} + +export type AssistantAppRequestData = () => any; + +export type RequestHandler = (app: AssistantApp) => any; + +/** + * The Actions on Google client library AssistantApp base class. + * + * This class contains the methods that are shared between platforms to support the conversation API + * protocol from Assistant. It also exports the 'State' class as a helper to represent states by + * name. + */ +export class AssistantApp { + /** + * The session state. + */ + readonly state: string; + + /** + * The session data in JSON format. + */ + readonly data: object; + + /** + * List of standard intents that the app provides. + * @enum {string} + */ + readonly StandardIntents: { + /** App fires MAIN intent for queries like [talk to $app]. */ + MAIN: StandardIntents, + /** App fires TEXT intent when action issues ask intent. */ + TEXT: StandardIntents, + /** App fires PERMISSION intent when action invokes askForPermission. */ + PERMISSION: StandardIntents, + /** App fires OPTION intent when user chooses from options provided. */ + OPTION: StandardIntents, + /** App fires TRANSACTION_REQUIREMENTS_CHECK intent when action sets up transaction. */ + TRANSACTION_REQUIREMENTS_CHECK: StandardIntents, + /** App fires DELIVERY_ADDRESS intent when action asks for delivery address. */ + DELIVERY_ADDRESS: StandardIntents, + /** App fires TRANSACTION_DECISION intent when action asks for transaction decision. */ + TRANSACTION_DECISION: StandardIntents, + /** App fires CONFIRMATION intent when requesting affirmation from user. */ + CONFIRMATION: StandardIntents, + /** App fires DATETIME intent when requesting date/time from user. */ + DATETIME: StandardIntents, + /** App fires SIGN_IN intent when requesting sign-in from user. */ + SIGN_IN: StandardIntents, + /** App fires NO_INPUT intent when user doesn't provide input. */ + NO_INPUT: StandardIntents, + /** App fires CANCEL intent when user exits app mid-dialog. */ + CANCEL: StandardIntents, + /** App fires NEW_SURFACE intent when requesting handoff to a new surface from user. */ + NEW_SURFACE: StandardIntents, + }; + + /** + * List of supported permissions the app supports. + * @enum {string} + */ + readonly SupportedPermissions: { + /** + * The user's name as defined in the + * {@link https://developers.google.com/actions/reference/conversation#UserProfile|UserProfile object} + */ + NAME: SupportedPermissions, + /** + * The location of the user's current device, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Location|Location object}. + */ + DEVICE_PRECISE_LOCATION: SupportedPermissions, + /** + * City and zipcode corresponding to the location of the user's current device, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Location|Location object}. + */ + DEVICE_COARSE_LOCATION: SupportedPermissions, + }; + + /** + * List of built-in argument names. + * @enum {string} + */ + readonly BuiltInArgNames: { + /** Permission granted argument. */ + PERMISSION_GRANTED: BuiltInArgNames, + /** Option selected argument. */ + OPTION: BuiltInArgNames, + /** Transaction requirements check result argument. */ + TRANSACTION_REQ_CHECK_RESULT: BuiltInArgNames, + /** Delivery address value argument. */ + DELIVERY_ADDRESS_VALUE: BuiltInArgNames, + /** Transactions decision argument. */ + TRANSACTION_DECISION_VALUE: BuiltInArgNames, + /** Confirmation argument. */ + CONFIRMATION: BuiltInArgNames, + /** DateTime argument. */ + DATETIME: BuiltInArgNames, + /** Sign in status argument. */ + SIGN_IN: BuiltInArgNames, + /** Reprompt count for consecutive NO_INPUT intents. */ + REPROMPT_COUNT: BuiltInArgNames, + /** Flag representing finality of NO_INPUT intent. */ + IS_FINAL_REPROMPT: BuiltInArgNames, + /** New surface value argument. */ + NEW_SURFACE: BuiltInArgNames, + }; + + /** + * List of possible conversation stages, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Conversation|Conversation object}. + * @enum {number} + */ + readonly ConversationStages: { + /** + * Unspecified conversation state. + */ + UNSPECIFIED: ConversationStages, + /** + * A new conversation. + */ + NEW: ConversationStages, + /** + * An active (ongoing) conversation. + */ + ACTIVE: ConversationStages, + }; + + /** + * List of surface capabilities supported by the app. + * @enum {string} + */ + readonly SurfaceCapabilities: { + /** + * The ability to output audio. + */ + AUDIO_OUTPUT: SurfaceCapabilities, + /** + * The ability to output on a screen + */ + SCREEN_OUTPUT: SurfaceCapabilities, + }; + + /** + * List of possible user input types. + * @enum {number} + */ + readonly InputTypes: { + /** + * Unspecified. + */ + UNSPECIFIED: InputTypes, + /** + * Input given by touch. + */ + TOUCH: InputTypes, + /** + * Input given by voice (spoken). + */ + VOICE: InputTypes, + /** + * Input given by keyboard (typed). + */ + KEYBOARD: InputTypes, + }; + + /** + * List of possible sign in result status values. + * @enum {string} + */ + readonly SignInStatus: { + // Unknown status. + UNSPECIFIED: SignInStatus, + // User successfully completed the account linking. + OK: SignInStatus, + // Cancelled or dismissed account linking. + CANCELLED: SignInStatus, + // System or network error. + ERROR: SignInStatus, + }; + + /** + * Values related to supporting {@link Transactions}. + * @type {object} + */ + readonly Transactions: typeof TransactionValues; + + readonly requestData: AssistantAppRequestData; + + /** + * Constructor for AssistantApp object. + * Should not be instantiated; rather instantiate one of the subclasses + * + * {@link ActionsSdkApp} or {@link ApiAiApp}. + * + * @param {Object} options JSON configuration. + * @param {Object} options.request Express HTTP request object. + * @param {Object} options.response Express HTTP response object. + * @param {Function=} options.sessionStarted Function callback when session starts. + * @param {function(): *} requestData Function that returns the + * request data object to be processed. + */ + constructor(options: AssistantAppOptions, requestData: AssistantAppRequestData); + + // --------------------------------------------------------------------------- + // Public APIs + // --------------------------------------------------------------------------- + + /** + * Handles the incoming Assistant request using a handler or Map of handlers. + * Each handler can be a function callback or Promise. + * + * @example + * // Actions SDK + * const app = new ActionsSdkApp({request: request, response: response}); + * + * function mainIntent (app) { + * const inputPrompt = app.buildInputPrompt(true, 'Hi! ' + + * 'I can read out an ordinal like ' + + * '123. Say a number.', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * + * function rawInput (app) { + * if (app.getRawInput() === 'bye') { + * app.tell('Goodbye!'); + * } else { + * const inputPrompt = app.buildInputPrompt(true, 'You said, ' + + * app.getRawInput() + '', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, mainIntent); + * actionMap.set(app.StandardIntents.TEXT, rawInput); + * + * app.handleRequest(actionMap); + * + * // API.AI + * const app = new ApiAiApp({request: req, response: res}); + * const NAME_ACTION = 'make_name'; + * const COLOR_ARGUMENT = 'color'; + * const NUMBER_ARGUMENT = 'number'; + * + * function makeName (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * const color = app.getArgument(COLOR_ARGUMENT); + * app.tell('Alright, your silly name is ' + + * color + ' ' + number + + * '! I hope you like it. See you next time.'); + * } + * + * const actionMap = new Map(); + * actionMap.set(NAME_ACTION, makeName); + * app.handleRequest(actionMap); + * + * @param {(Function|Map)} handler The handler (or Map of handlers) for the request. + * @actionssdk + * @apiai + */ + handleRequest(handler?: RequestHandler | Map): void; + + /** + * Equivalent to {@link AssistantApp#askForPermission|askForPermission}, + * but allows you to prompt the user for more than one permission at once. + * + * Notes: + * + * * The order in which you specify the permission prompts does not matter - + * it is controlled by the Assistant to provide a consistent user experience. + * * The user will be able to either accept all permissions at once, or none. + * If you wish to allow them to selectively accept one or other, make several + * dialog turns asking for each permission independently with askForPermission. + * * Asking for DEVICE_COARSE_LOCATION and DEVICE_PRECISE_LOCATION at once is + * equivalent to just asking for DEVICE_PRECISE_LOCATION + * + * @example + * const app = new ApiAIApp({request: req, response: res}); + * const REQUEST_PERMISSION_ACTION = 'request_permission'; + * const GET_RIDE_ACTION = 'get_ride'; + * + * function requestPermission (app) { + * const permission = [ + * app.SupportedPermissions.NAME, + * app.SupportedPermissions.DEVICE_PRECISE_LOCATION + * ]; + * app.askForPermissions('To pick you up', permissions); + * } + * + * function sendRide (app) { + * if (app.isPermissionGranted()) { + * const displayName = app.getUserName().displayName; + * const address = app.getDeviceLocation().address; + * app.tell('I will tell your driver to pick up ' + displayName + + * ' at ' + address); + * } else { + * // Response shows that user did not grant permission + * app.tell('Sorry, I could not figure out where to pick you up.'); + * } + * } + * const actionMap = new Map(); + * actionMap.set(REQUEST_PERMISSION_ACTION, requestPermission); + * actionMap.set(GET_RIDE_ACTION, sendRide); + * app.handleRequest(actionMap); + * + * @param {string} context Context why the permission is being asked; it's the TTS + * prompt prefix (action phrase) we ask the user. + * @param {Array} permissions Array of permissions App supports, each of + * which comes from AssistantApp.SupportedPermissions. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return A response is sent to Assistant to ask for the user's permission; for any + * invalid input, we return null. + * @actionssdk + * @apiai + */ + askForPermissions(context: string, permissions: string[], dialogState?: object): object; + + /** + * Checks whether user is in transactable state. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const TXN_REQ_COMPLETE = 'txn.req.complete'; + * + * let transactionConfig = { + * deliveryAddressRequired: false, + * type: app.Transactions.PaymentType.BANK, + * displayName: 'Checking-1234' + * }; + * function welcomeIntent (app) { + * app.askForTransactionRequirements(transactionConfig); + * } + * + * function txnReqCheck (app) { + * if (app.getTransactionRequirementsResult() === app.Transactions.ResultType.OK) { + * // continue cart building flow + * } else { + * // don't continue cart building + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(TXN_REQ_COMPLETE, txnReqCheck); + * app.handleRequest(actionMap); + * + * @param {ActionPaymentTransactionConfig|GooglePaymentTransactionConfig=} + * transactionConfig Configuration for the transaction. Includes payment + * options and order options. Optional if order has no payment or + * delivery. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return {Object} HTTP response. + * @actionssdk + * @apiai + */ + askForTransactionRequirements(transactionConfig?: ActionPaymentTransactionConfig | GooglePaymentTransactionConfig, dialogState?: object): object; + + /** + * Asks user to confirm transaction information. + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const TXN_COMPLETE = 'txn.complete'; + * + * let transactionConfig = { + * deliveryAddressRequired: false, + * type: app.Transactions.PaymentType.BANK, + * displayName: 'Checking-1234' + * }; + * + * let order = app.buildOrder(); + * // fill order cart + * + * function welcomeIntent (app) { + * app.askForTransaction(order, transactionConfig); + * } + * + * function txnComplete (app) { + * // respond with order update + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(TXN_COMPLETE, txnComplete); + * app.handleRequest(actionMap); + * + * @param {Object} order Order built with buildOrder(). + * @param {ActionPaymentTransactionConfig|GooglePaymentTransactionConfig} + * transactionConfig Configuration for the transaction. Includes payment + * options and order options. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @apiai + */ + askForTransactionDecision(order: object, transactionConfig?: ActionPaymentTransactionConfig | GooglePaymentTransactionConfig, dialogState?: object): object; + + /** + * Asks the Assistant to guide the user to grant a permission. For example, + * if you want your app to get access to the user's name, you would invoke + * the askForPermission method with a context containing the reason for the request, + * and the AssistantApp.SupportedPermissions.NAME permission. With this, the Assistant will ask + * the user, in your agent's voice, the following: '[Context with reason for the request], + * I'll just need to get your name from Google, is that OK?'. + * + * Once the user accepts or denies the request, the Assistant will fire another intent: + * assistant.intent.action.PERMISSION with a boolean argument: AssistantApp.BuiltInArgNames.PERMISSION_GRANTED + * and, if granted, the information that you requested. + * + * Read more: + * + * * {@link https://developers.google.com/actions/reference/conversation#ExpectedIntent|Supported Permissions} + * * Check if the permission has been granted with {@link AssistantApp#isPermissionGranted|isPermissionsGranted} + * * {@link AssistantApp#getDeviceLocation|getDeviceLocation} + * * {@link AssistantApp#getUserName|getUserName} + * + * @example + * const app = new ApiAiApp({request: req, response: res}); + * const REQUEST_PERMISSION_ACTION = 'request_permission'; + * const GET_RIDE_ACTION = 'get_ride'; + * + * function requestPermission (app) { + * const permission = app.SupportedPermissions.NAME; + * app.askForPermission('To pick you up', permission); + * } + * + * function sendRide (app) { + * if (app.isPermissionGranted()) { + * const displayName = app.getUserName().displayName; + * app.tell('I will tell your driver to pick up ' + displayName); + * } else { + * // Response shows that user did not grant permission + * app.tell('Sorry, I could not figure out who to pick up.'); + * } + * } + * const actionMap = new Map(); + * actionMap.set(REQUEST_PERMISSION_ACTION, requestPermission); + * actionMap.set(GET_RIDE_ACTION, sendRide); + * app.handleRequest(actionMap); + * + * @param {string} context Context why permission is asked; it's the TTS + * prompt prefix (action phrase) we ask the user. + * @param {string} permission One of the permissions Assistant supports, each of + * which comes from AssistantApp.SupportedPermissions. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return A response is sent to the Assistant to ask for the user's permission; + * for any invalid input, we return null. + * @actionssdk + * @apiai + */ + askForPermission(context: string, permission: string, dialogState?: object): object; + + /** + * Returns true if the request follows a previous request asking for + * permission from the user and the user granted the permission(s). Otherwise, + * false. Use with {@link AssistantApp#askForPermissions|askForPermissions}. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * // or + * const app = new ApiAiApp({request: request, response: response}); + * app.askForPermissions("To get you a ride", [ + * app.SupportedPermissions.NAME, + * app.SupportedPermissions.DEVICE_PRECISE_LOCATION + * ]); + * // ... + * // In response handler for subsequent intent: + * if (app.isPermissionGranted()) { + * // Use the requested permission(s) to get the user a ride + * } + * + * @return {boolean} true if permissions granted. + * @apiai + * @actionssdk + */ + isPermissionGranted(): boolean; + + /** + * Asks user for delivery address. + * + * @example + * // For ApiAiApp: + * const app = new ApiAiApp({request, response}); + * const WELCOME_INTENT = 'input.welcome'; + * const DELIVERY_INTENT = 'delivery.address'; + * + * function welcomeIntent (app) { + * app.askForDeliveryAddress('To make sure I can deliver to you'); + * } + * + * function addressIntent (app) { + * const postalCode = app.getDeliveryAddress().postalAddress.postalCode; + * if (isInDeliveryZone(postalCode)) { + * app.tell('Great looks like you\'re in our delivery area!'); + * } else { + * app.tell('I\'m sorry it looks like we can\'t deliver to you.'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(DELIVERY_INTENT, addressIntent); + * app.handleRequest(actionMap); + * + * // For ActionsSdkApp: + * const app = new ActionsSdkApp({request, response}); + * const WELCOME_INTENT = app.StandardIntents.MAIN; + * const DELIVERY_INTENT = app.StandardIntents.DELIVERY_ADDRESS; + * + * function welcomeIntent (app) { + * app.askForDeliveryAddress('To make sure I can deliver to you'); + * } + * + * function addressIntent (app) { + * const postalCode = app.getDeliveryAddress().postalAddress.postalCode; + * if (isInDeliveryZone(postalCode)) { + * app.tell('Great looks like you\'re in our delivery area!'); + * } else { + * app.tell('I\'m sorry it looks like we can\'t deliver to you.'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(DELIVERY_INTENT, addressIntent); + * app.handleRequest(actionMap); + * + * @param {string} reason Reason given to user for asking delivery address. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {Object} HTTP response. + * @actionssdk + * @apiai + */ + askForDeliveryAddress(reason: string, dialogState?: object): object; + + /** + * Asks user for a confirmation. + * + * @example + * const app = new ApiAiApp({ request, response }); + * const WELCOME_INTENT = 'input.welcome'; + * const CONFIRMATION = 'confirmation'; + * + * function welcomeIntent (app) { + * app.askForConfirmation('Are you sure you want to do that?'); + * } + * + * function confirmation (app) { + * if (app.getUserConfirmation()) { + * app.tell('Great! I\'m glad you want to do it!'); + * } else { + * app.tell('That\'s okay. Let\'s not do it now.'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(CONFIRMATION, confirmation); + * app.handleRequest(actionMap); + * + * @param {string=} prompt The confirmation prompt presented to the user to + * query for an affirmative or negative response. If undefined or null, + * Google will use a generic yes/no prompt. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @actionssdk + * @apiai + */ + askForConfirmation(prompt?: string, dialogState?: object): object; + + /** + * Asks user for a timezone-agnostic date and time. + * + * @example + * const app = new ApiAiApp({ request, response }); + * const WELCOME_INTENT = 'input.welcome'; + * const DATETIME = 'datetime'; + * + * function welcomeIntent (app) { + * app.askForDateTime('When do you want to come in?', + * 'Which date works best for you?', + * 'What time of day works best for you?'); + * } + * + * function datetime (app) { + * app.tell({speech: 'Great see you at your appointment!', + * displayText: 'Great, we will see you on ' + * + app.getDateTime().date.month + * + '/' + app.getDateTime().date.day + * + ' at ' + app.getDateTime().time.hours + * + (app.getDateTime().time.minutes || '')}); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(DATETIME, datetime); + * app.handleRequest(actionMap); + * + * @param {string=} initialPrompt The initial prompt used to ask for a + * date and time. If undefined or null, Google will use a generic + * prompt. + * @param {string=} datePrompt The prompt used to specifically ask for the + * date if not provided by user. If undefined or null, Google will use a + * generic prompt. + * @param {string=} timePrompt The prompt used to specifically ask for the + * time if not provided by user. If undefined or null, Google will use a + * generic prompt. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @actionssdk + * @apiai + */ + askForDateTime(initialPrompt?: string, datePrompt?: string, timePrompt?: string, dialogState?: object): object; + + /** + * Hands the user off to a web sign in flow. App sign in and OAuth credentials + * are set in the {@link https://console.actions.google.com|Actions Console}. + * Retrieve the access token in subsequent intents using + * app.getUser().accessToken. + * + * Note: Currently this API requires enabling the app for Transactions APIs. + * To do this, fill out the App Info section of the Actions Console project + * and check the box indicating the use of Transactions under "Privacy and + * consent". + * + * @example + * const app = new ApiAiApp({ request, response }); + * const WELCOME_INTENT = 'input.welcome'; + * const SIGN_IN = 'sign.in'; + * + * function welcomeIntent (app) { + * app.askForSignIn(); + * } + * + * function signIn (app) { + * if (app.getSignInStatus() === app.SignInstatus.OK) { + * let accessToken = app.getUser().accessToken; + * app.ask('Great, thanks for signing in!'); + * } else { + * app.ask('I won\'t be able to save your data, but let\'s continue!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(SIGN_IN, signIn); + * app.handleRequest(actionMap); + * + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @actionssdk + * @apiai + */ + askForSignIn(dialogState?: object): object; + + /** + * Requests the user to switch to another surface during the conversation. + * + * @example + * const app = new ApiAiApp({ request, response }); + * const WELCOME_INTENT = 'input.welcome'; + * const SHOW_IMAGE = 'show.image'; + * + * function welcomeIntent (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * showPicture(app); + * } else if (app.hasAvailableSurfaceCapabilities(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askForNewSurface('To show you an image', + * 'Check out this image', + * [app.SurfaceCapabilities.SCREEN_OUTPUT] + * ); + * } else { + * app.tell('This part of the app only works on screen devices. Sorry about that'); + * } + * } + * + * function showImage (app) { + * if (!app.isNewSurface()) { + * app.tell('Ok, I understand. You don't want to see pictures. Bye'); + * } else { + * showPicture(app, pictureType); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(SHOW_IMAGE, showImage); + * app.handleRequest(actionMap); + * + * @param {string} context Context why new surface is requested; it's the TTS + * prompt prefix (action phrase) we ask the user. + * @param {string} notificationTitle Title of the notification appearing on + * new surface device. + * @param {Array} capabilities The list of capabilities required in + * the surface. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @apiai + * @actionssdk + */ + askForNewSurface(context: string, notificationTitle: string, capabilities: string[], dialogState?: object): object; + + /** + * Gets the {@link User} object. + * The user object contains information about the user, including + * a string identifier and personal information (requires requesting permissions, + * see {@link AssistantApp#askForPermissions|askForPermissions}). + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * // or + * const app = new ActionsSdkApp({request: request, response: response}); + * const userId = app.getUser().userId; + * + * @return {User} Null if no value. + * @actionssdk + * @apiai + */ + getUser(): User; + + /** + * If granted permission to user's name in previous intent, returns user's + * display name, family name, and given name. If name info is unavailable, + * returns null. + * + * @example + * const app = new ApiAIApp({request: req, response: res}); + * const REQUEST_PERMISSION_ACTION = 'request_permission'; + * const SAY_NAME_ACTION = 'get_name'; + * + * function requestPermission (app) { + * const permission = app.SupportedPermissions.NAME; + * app.askForPermission('To know who you are', permission); + * } + * + * function sayName (app) { + * if (app.isPermissionGranted()) { + * app.tell('Your name is ' + app.getUserName().displayName)); + * } else { + * // Response shows that user did not grant permission + * app.tell('Sorry, I could not get your name.'); + * } + * } + * const actionMap = new Map(); + * actionMap.set(REQUEST_PERMISSION_ACTION, requestPermission); + * actionMap.set(SAY_NAME_ACTION, sayName); + * app.handleRequest(actionMap); + * @return {UserName} Null if name permission is not granted. + * @actionssdk + * @apiai + */ + getUserName(): UserName; + + /** + * Gets the user locale. Returned string represents the regional language + * information of the user set in their Assistant settings. + * For example, 'en-US' represents US English. + * + * @example + * const app = new ApiAiApp({request, response}); + * const locale = app.getUserLocale(); + * + * @return {string} User's locale, e.g. 'en-US'. Null if no locale given. + * @actionssdk + * @apiai + */ + getUserLocale(): string; + + /** + * If granted permission to device's location in previous intent, returns device's + * location (see {@link AssistantApp#askForPermissions|askForPermissions}). + * If device info is unavailable, returns null. + * + * @example + * const app = new ApiAiApp({request: req, response: res}); + * // or + * const app = new ActionsSdkApp({request: req, response: res}); + * app.askForPermission("To get you a ride", + * app.SupportedPermissions.DEVICE_PRECISE_LOCATION); + * // ... + * // In response handler for permissions fallback intent: + * if (app.isPermissionGranted()) { + * sendCarTo(app.getDeviceLocation().coordinates); + * } + * + * @return {DeviceLocation} Null if location permission is not granted. + * @actionssdk + * @apiai + */ + getDeviceLocation(): DeviceLocation; + + /** + * Gets type of input used for this request. + * + * @return {number} One of AssistantApp.InputTypes. + * Null if no input type given. + * @apiai + * @actionssdk + */ + getInputType(): number; + + /** + * Get the argument value by name from the current intent. + * If the argument is included in originalRequest, and is not a text argument, + * the entire argument object is returned. + * + * Note: If incoming request is using an API version under 2 (e.g. 'v1'), + * the argument object will be in Proto2 format (snake_case, etc). + * + * @example + * const app = new ApiAiApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * + * function welcomeIntent (app) { + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} argName Name of the argument. + * @return {Object} Argument value matching argName + * or null if no matching argument. + * @apiai + * @actionssdk + */ + getArgumentCommon(argName: string): object; + + /** + * Gets transactability of user. Only use after calling + * askForTransactionRequirements. Null if no result given. + * + * @return {string} One of Transactions.ResultType. + * @apiai + * @actionssdk + */ + getTransactionRequirementsResult(): string; + + /** + * Gets order delivery address. Only use after calling askForDeliveryAddress. + * + * @return {DeliveryAddress} Delivery address information. Null if user + * denies permission, or no address given. + * @apiai + * @actionssdk + */ + getDeliveryAddress(): Location; + + /** + * Gets transaction decision information. Only use after calling + * askForTransactionDecision. + * + * @return {TransactionDecision} Transaction decision data. Returns object with + * userDecision only if user declines. userDecision will be one of + * Transactions.ConfirmationDecision. Null if no decision given. + * @apiai + * @actionssdk + */ + getTransactionDecision(): TransactionDecision; + + /** + * Gets confirmation decision. Use after askForConfirmation. + * + * False if user replied with negative response. Null if no user + * confirmation decision given. + * @apiai + * @actionssdk + */ + getUserConfirmation(): boolean | null; + + /** + * Gets user provided date and time. Use after askForDateTime. + * + * @return {DateTime} Date and time given by the user. Null if no user + * date and time given. + * @apiai + * @actionssdk + */ + getDateTime(): DateTime; + + /** + * Gets status of user sign in request. + * + * @return {string} Result of user sign in request. One of + * ApiAiApp.SignInStatus or ActionsSdkApp.SignInStatus + * Null if no sign in status. + * @apiai + * @actionssdk + */ + getSignInStatus(): string; + + /** + * Returns true if user device has a given surface capability. + * + * @param {string} capability Must be one of {@link SurfaceCapabilities}. + * @return {boolean} True if user device has the given capability. + * + * @example + * const app = new ApiAIApp({request: req, response: res}); + * const DESCRIBE_SOMETHING = 'DESCRIBE_SOMETHING'; + * + * function describe (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.tell(richResponseWithBasicCard); + * } else { + * app.tell('Let me tell you about ...'); + * } + * } + * const actionMap = new Map(); + * actionMap.set(DESCRIBE_SOMETHING, describe); + * app.handleRequest(actionMap); + * + * @apiai + * @actionssdk + */ + hasSurfaceCapability(requestedCapability: string): boolean; + + /** + * Gets surface capabilities of user device. + * + * @return {Array} Supported surface capabilities, as defined in + * AssistantApp.SurfaceCapabilities. + * @apiai + * @actionssdk + */ + getSurfaceCapabilities(): string[]; + + /** + * Returns the set of other available surfaces for the user. + * + * @return {Array} Empty if no available surfaces. + * @actionssdk + * @apiai + */ + getAvailableSurfaces(): Surface[]; + + /** + * Returns true if user has an available surface which includes all given + * capabilities. Available surfaces capabilities may exist on surfaces other + * than that used for an ongoing conversation. + * + * @param {string|Array} capabilities Must be one of + * {@link SurfaceCapabilities}. + * @return {boolean} True if user has a capability available on some surface. + * + * @apiai + * @actionssdk + */ + hasAvailableSurfaceCapabilities(capabilities: string | string[]): boolean; + + /** + * Returns the result of the AskForNewSurface helper. + * + * @return {boolean} True if user has triggered conversation on a new device + * following the NEW_SURFACE intent. + * @actionssdk + * @apiai + */ + isNewSurface(): boolean; + + /** + * Returns true if the app is being tested in sandbox mode. Enable sandbox + * mode in the (Actions console)[console.actions.google.com] to test + * transactions. + * + * @return {boolean} True if app is being used in Sandbox mode. + * @apiai + * @actionssdk + */ + isInSandbox(): boolean; + + /** + * Returns the number of subsequent reprompts related to silent input from the + * user. This should be used along with the NO_INPUT intent to reprompt the + * user for input in cases where the Google Assistant could not pick up any + * speech. + * + * @example + * const app = new ActionsSdkApp({request, response}); + * + * function welcome (app) { + * app.ask('Welcome to your app!'); + * } + * + * function noInput (app) { + * if (app.getRepromptCount() === 0) { + * app.ask(`What was that?`); + * } else if (app.getRepromptCount() === 1) { + * app.ask(`Sorry I didn't catch that. Could you repeat yourself?`); + * } else if (app.isFinalReprompt()) { + * app.tell(`Okay let's try this again later.`); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, welcome); + * actionMap.set(app.StandardIntents.NO_INPUT, noInput); + * app.handleRequest(actionMap); + * + * @return {number} The current reprompt count. Null if no reprompt count + * available (e.g. not in the NO_INPUT intent). + * @apiai + * @actionssdk + */ + getRepromptCount(): number; + + /** + * Returns true if it is the final reprompt related to silent input from the + * user. This should be used along with the NO_INPUT intent to give the final + * response to the user after multiple silences and should be an app.tell + * which ends the conversation. + * + * @example + * const app = new ActionsSdkApp({request, response}); + * + * function welcome (app) { + * app.ask('Welcome to your app!'); + * } + * + * function noInput (app) { + * if (app.getRepromptCount() === 0) { + * app.ask(`What was that?`); + * } else if (app.getRepromptCount() === 1) { + * app.ask(`Sorry I didn't catch that. Could you repeat yourself?`); + * } else if (app.isFinalReprompt()) { + * app.tell(`Okay let's try this again later.`); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, welcome); + * actionMap.set(app.StandardIntents.NO_INPUT, noInput); + * app.handleRequest(actionMap); + * + * @return {boolean} True if in a NO_INPUT intent and this is the final turn + * of dialog. + * @apiai + * @actionssdk + */ + isFinalReprompt(): boolean; + + // --------------------------------------------------------------------------- + // Response Builders + // --------------------------------------------------------------------------- + + /** + * Constructs RichResponse with chainable property setters. + * + * @param {RichResponse=} richResponse RichResponse to clone. + * @return {RichResponse} Constructed RichResponse. + */ + buildRichResponse(richResponse?: RichResponse): RichResponse; + + /** + * Constructs BasicCard with chainable property setters. + * + * @param {string=} bodyText Body text of the card. Can be set using setTitle + * instead. + * @return {BasicCard} Constructed BasicCard. + */ + buildBasicCard(bodyText?: string): BasicCard; + + /** + * Constructs List with chainable property setters. + * + * @param {string=} title A title to set for a new List. + * @return {List} Constructed List. + */ + buildList(title?: string): List; + + /** + * Constructs Carousel with chainable property setters. + * + * @return {Carousel} Constructed Carousel. + */ + buildCarousel(): Carousel; + + /** + * Constructs OptionItem with chainable property setters. + * + * @param {string=} key A unique key to identify this option. This key will + * be returned as an argument in the resulting actions.intent.OPTION + * intent. + * @param {string|Array=} synonyms A list of synonyms which the user may + * use to identify this option instead of the option key. + * @return {OptionItem} Constructed OptionItem. + */ + buildOptionItem(key?: string, synonyms?: string | string[]): OptionItem; + + // --------------------------------------------------------------------------- + // Transaction Builders + // --------------------------------------------------------------------------- + + /** + * Constructs Order with chainable property setters. + * + * @param {string} orderId Unique identifier for the order. + * @return {Order} Constructed Order. + */ + buildOrder(orderId: string): Order; + + /** + * Constructs Cart with chainable property setters. + * + * @param {string=} cartId Unique identifier for the cart. + * @return {Cart} Constructed Cart. + */ + buildCart(cartId?: string): Cart; + + /** + * Constructs LineItem with chainable property setters. + * + * @param {string} id Unique identifier for the item. + * @param {string} name Name of the line item. + * @return {LineItem} Constructed LineItem. + */ + buildLineItem(id: string, name: string): LineItem; + + /** + * Constructs OrderUpdate with chainable property setters. + * + * @param {string} orderId Unique identifier of the order. + * @param {boolean} isGoogleOrderId True if the order ID is provided by + * Google. False if the order ID is app provided. + * @return {OrderUpdate} Constructed OrderUpdate. + */ + buildOrderUpdate(orderId: string, isGoogleOrderId: boolean): OrderUpdate; +} + +export class State { + constructor(name: string); + getName(): string; +} diff --git a/types/actions-on-google/index.d.ts b/types/actions-on-google/index.d.ts new file mode 100644 index 0000000000..3c0daa5a28 --- /dev/null +++ b/types/actions-on-google/index.d.ts @@ -0,0 +1,39 @@ +// Type definitions for actions-on-google 1.4 +// Project: https://github.com/actions-on-google/actions-on-google-nodejs +// Definitions by: Joel Hegg , Pilwon Huh +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.4 + +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Actions on Google client library. + * https://developers.google.com/actions/ + */ + +import * as Transactions from './transactions'; +import * as Responses from './response-builder'; + +export { AssistantApp, AssistantAppOptions, RequestHandler, SessionStartedFunction, State } from './assistant-app'; +export { ActionsSdkApp, ActionsSdkAppOptions } from './actions-sdk-app'; +export { ApiAiApp, ApiAiAppOptions } from './api-ai-app'; +export { Transactions }; +export { Responses }; +// Backwards compatibility +export { AssistantApp as Assistant } from './assistant-app'; +export { ActionsSdkApp as ActionsSdkAssistant } from './actions-sdk-app'; +export { ApiAiApp as ApiAiAssistant } from './api-ai-app'; diff --git a/types/actions-on-google/response-builder.d.ts b/types/actions-on-google/response-builder.d.ts new file mode 100644 index 0000000000..add7b9fc03 --- /dev/null +++ b/types/actions-on-google/response-builder.d.ts @@ -0,0 +1,333 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A collection of response builders. + */ + +import { OrderUpdate } from './transactions'; + +/** + * Simple Response type. + */ +export interface SimpleResponse { + /** Speech to be spoken to user. SSML allowed. */ + speech: string; + /** Optional text to be shown to user */ + displayText?: string; +} + +/** + * Suggestions to show with response. + */ +export interface Suggestion { + /** Text of the suggestion. */ + title: string; +} + +/** + * Link Out Suggestion. Used in rich response as a suggestion chip which, when + * selected, links out to external URL. + */ +export interface LinkOutSuggestion { + /** Text shown on the suggestion chip. */ + title: string; + /** String URL to open. */ + url: string; +} + +/** + * Image type shown on visual elements. + */ +export interface Image { + /** Image source URL. */ + url: string; + /** Text to replace for image for accessibility. */ + accessibilityText: string; + /** Width of the image. */ + width: number; + /** Height of the image. */ + height: number; +} + +/** + * Basic Card Button. Shown below basic cards. Open a URL when selected. + */ +export interface Button { + /** Text shown on the button. */ + title: string; + /** Action to take when selected. */ + openUrlAction: { + /** String URL to open. */ + url: string; + }; +} + +/** + * Option item. Used in actions.intent.OPTION intent. + */ +export interface OptionItem { + /** Option item identifier information. */ + optionInfo: OptionInfo; + /** Name of the item. */ + title: string; + /** Optional text describing the item. */ + description: string; + /** Square image to show for this item. */ + image: Image; +} + +/** + * Option info. Provides unique identifier for a given OptionItem. + */ +export interface OptionInfo { + /** Unique string ID for this option. */ + key: string; + /** Synonyms that can be used by the user to indicate this option if they do not use the key. */ + synonyms: string[]; +} + +/** + * Class for initializing and constructing Rich Responses with chainable interface. + */ +export class RichResponse { + /** + * Constructor for RichResponse. Accepts optional RichResponse to clone. + * + * @param {RichResponse} richResponse + */ + constructor(richResponse: RichResponse); + + /** + * Adds a SimpleResponse to list of items. + * + * @param {string|SimpleResponse} simpleResponse Simple response to present to + * user. If just a string, display text will not be set. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addSimpleResponse(simpleResponse: string | SimpleResponse): RichResponse; + + /** + * Adds a BasicCard to list of items. + * + * @param {BasicCard} basicCard Basic card to include in response. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addBasicCard(basicCard: BasicCard): RichResponse; + + /** + * Adds a single suggestion or list of suggestions to list of items. + * + * @param {string|Array} suggestions Either a single string suggestion + * or list of suggestions to add. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addSuggestions(suggestions: string | string[]): RichResponse; + + /** + * Returns true if the given suggestion text is valid to be added to the suggestion list. A valid + * text string is not longer than 25 characters. + */ + isValidSuggestionText(suggestionText: string): boolean; + + /** + * Sets the suggestion link for this rich response. + * + * @param {string} destinationName Name of the link out destination. + * @param {string} suggestionUrl - String URL to open when suggestion is used. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addSuggestionLink(destinationName: string, suggestionUrl: string): RichResponse; + + /** + * Adds an order update to this response. Use after a successful transaction + * decision to confirm the order. + * + * @param {OrderUpdate} orderUpdate + * @return {RichResponse} Returns current constructed RichResponse. + */ + addOrderUpdate(orderUpdate: OrderUpdate): RichResponse; +} + +/** + * Class for initializing and constructing Basic Cards with chainable interface. + */ +export class BasicCard { + /** + * Constructor for BasicCard. Accepts optional BasicCard to clone. + * + * @param {BasicCard} basicCard + */ + constructor(basicCard: BasicCard); + + /** + * Sets the title for this Basic Card. + * + * @param {string} title Title to show on card. + * @return {BasicCard} Returns current constructed BasicCard. + */ + setTitle(title: string): BasicCard; + + /** + * Sets the subtitle for this Basic Card. + * + * @param {string} subtitle Subtitle to show on card. + * @return {BasicCard} Returns current constructed BasicCard. + */ + setSubtitle(subtitle: string): BasicCard; + + /** + * Sets the body text for this Basic Card. + * + * @param {string} bodyText Body text to show on card. + * @return {BasicCard} Returns current constructed BasicCard. + */ + setBodyText(bodyText: string): BasicCard; + + /** + * Sets the image for this Basic Card. + * + * @param {string} url Image source URL. + * @param {string} accessibilityText Text to replace for image for + * accessibility. + * @param {number=} width Width of the image. + * @param {number=} height Height of the image. + * @return {BasicCard} Returns current constructed BasicCard. + */ + setImage(url: string, accessibilityText: string, width?: number, height?: number): BasicCard; + + /** + * Adds a button below card. + * + * @param {string} text Text to show on button. + * @param {string} url URL to open when button is selected. + * @return {BasicCard} Returns current constructed BasicCard. + */ + addButton(text: string, url: string): BasicCard; +} + +/** + * Class for initializing and constructing Lists with chainable interface. + */ +export class List { + /** + * Constructor for List. Accepts optional List to clone, string title, or + * list of items to copy. + * + * @param {List|string|Array} list Either a list to clone, a title + * to set for a new List, or an array of OptionItem to initialize a new + * list. + */ + constructor(list: List | string | OptionItem[]); + + /** + * Sets the title for this List. + * + * @param {string} title Title to show on list. + * @return {List} Returns current constructed List. + */ + setTitle(title: string): List; + + /** + * Adds a single item or list of items to the list. + * + * @param {OptionItem|Array} optionItems OptionItems to add. + * @return {List} Returns current constructed List. + */ + addItems(optionItems: OptionItem | OptionItem[]): List; +} + +/** + * Class for initializing and constructing Carousel with chainable interface. + */ +export class Carousel { + /** + * Constructor for Carousel. Accepts optional Carousel to clone or list of + * items to copy. + * + * @param {Carousel|Array} carousel Either a carousel to clone, a + * or an array of OptionItem to initialize a new carousel + */ + constructor(carousel: Carousel | OptionItem[]); + + /** + * Adds a single item or list of items to the carousel. + * + * @param {OptionItem|Array} optionItems OptionItems to add. + * @return {Carousel} Returns current constructed Carousel. + */ + addItems(optionItems: OptionItem | OptionItem[]): Carousel; +} + +/** + * Class for initializing and constructing Option Items with chainable interface. + */ +export class OptionItem { + /** + * Constructor for OptionItem. Accepts optional OptionItem to clone. + * + * @param {OptionItem} optionItem + */ + constructor(optionItem: OptionItem); + + /** + * Sets the title for this Option Item. + * + * @param {string} title Title to show on item. + * @return {OptionItem} Returns current constructed OptionItem. + */ + setTitle(title: string): OptionItem; + + /** + * Sets the description for this Option Item. + * + * @param {string} description Description to show on item. + * @return {OptionItem} Returns current constructed OptionItem. + */ + setDescription(description: string): OptionItem; + + /** + * Sets the image for this Option Item. + * + * @param {string} url Image source URL. + * @param {string} accessibilityText Text to replace for image for + * accessibility. + * @param {number=} width Width of the image. + * @param {number=} height Height of the image. + * @return {OptionItem} Returns current constructed OptionItem. + */ + setImage(url: string, accessibilityText: string, width?: number, height?: number): OptionItem; + + /** + * Sets the key for the OptionInfo of this Option Item. This will be returned + * as an argument in the resulting actions.intent.OPTION intent. + * + * @param {string} key Key to uniquely identify this item. + * @return {OptionItem} Returns current constructed OptionItem. + */ + setKey(key: string): OptionItem; + + /** + * Adds a single synonym or list of synonyms to item. + * + * @param {string|Array} synonyms Either a single string synonyms + * or list of synonyms to add. + * @return {OptionItem} Returns current constructed OptionItem. + */ + addSynonyms(synonyms: string | string[]): OptionItem; +} + +export function isSsml(text: string): boolean; diff --git a/types/actions-on-google/transactions.d.ts b/types/actions-on-google/transactions.d.ts new file mode 100644 index 0000000000..e64e69bdf1 --- /dev/null +++ b/types/actions-on-google/transactions.d.ts @@ -0,0 +1,1406 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A collection of Transaction related constants, utility functions, and + * builders. + */ + +import { Image } from './response-builder'; + +/** + * Price type. + */ +export interface Price { + /** One of Transaction.PriceType. */ + type: PriceType; + amount: { + /** Currency code of price. */ + currencyCode: string; + /** Unit count of price. */ + units: number; + /** Partial unit count of price. */ + nanos?: number; + }; +} + +/** + * Order rejection info. + */ +export interface RejectionInfo { + /** One of Transaction.RejectionType. */ + type: RejectionType; + /** Reason for the order rejection. */ + reason: string; +} + +/** + * Order receipt info. + */ +export interface ReceiptInfo { + /** Action provided order ID. Used when the order has been received by the integrator. */ + confirmedActionOrderId: string; +} + +/** + * Order cancellation info. + */ +export interface CancellationInfo { + /** Reason for the cancellation. */ + reason: string; +} + +/** + * Order transit info. + */ +export interface TransitInfo { + /** UTC timestamp of the transit update. */ + updatedTime: { + /** Seconds since Unix epoch. */ + seconds: number; + /** Partial seconds since Unix epoch. */ + nanos?: number; + }; +} + +/** + * Order fulfillment info. + */ +export interface FulfillmentInfo { + /** UTC timestamp of the fulfillment update. */ + deliveryTime: { + /** Seconds since Unix epoch. */ + seconds: number; + /** Partial seconds since Unix epoch. */ + nanos?: number; + }; +} + +/** + * Order return info. + */ +export interface ReturnInfo { + /** Reason for the return. */ + reason: string; +} + +/** + * Transaction config for transactions not involving a Google provided + * payment instrument. + */ +export interface ActionPaymentTransactionConfig { + /** True if delivery address is required for the transaction. */ + deliveryAddressRequired: boolean; + /** One of Transactions.PaymentType. */ + type: PaymentType; + /** The name of the instrument displayed on receipt. For example, for card payment, could be "VISA-1234". */ + displayName: string; + customerInfoOptions?: CustomerInfoOptions; +} + +/** + * Transaction config for transactions involving a Google provided payment + * instrument. + */ +export interface GooglePaymentTransactionConfig { + /** True if delivery address is required for the transaction. */ + deliveryAddressRequired: boolean; + /** Tokenization parameters provided by payment gateway. */ + tokenizationParameters: object; + /** List of accepted card networks. Must be any number of Transactions.CardNetwork. */ + cardNetworks: CardNetwork[]; + /** True if prepaid cards are not allowed for transaction. */ + prepaidCardDisallowed: boolean; + customerInfoOptions?: CustomerInfoOptions; +} + +/** + * Customer information requested as part of the transaction + */ +export interface CustomerInfoOptions { + /** one of Transactions.CustomerInfoProperties */ + customerInfoProperties: string[]; +} + +/** + * Generic Location type. + */ +export interface Location { + postalAddress: { + regionCode: string; + languageCode: string; + postalCode: string; + administrativeArea: string; + locality: string; + addressLines: string[]; + recipients: string; + }; + phoneNumber: string; + notes: string; +} + +/** + * Decision and order information returned when calling getTransactionDecision(). + */ +export interface TransactionDecision { + /** One of Transactions.ConfirmationDecision. */ + userDecision: ConfirmationDecision; + checkResult: { + /** One of Transactions.ResultType. */ + resultType: string; + }; + order: { + /** The proposed order used in the transaction decision. */ + finalOrder: Order; + /** Order ID assigned by Google. */ + googleOrderId: string; + /** User visible order ID set in proposed order. */ + actionOrderId: string; + /** The date and time the order was created */ + orderDate: { + seconds: string; + nanos: number; + }; + /** The details regarding the payment method that must be used to charge the user. */ + paymentInfo: { + /** One of Transactions.PaymentType. */ + paymentType: PaymentType; + googleProvidedPaymentInstrument: { + /** + * Contains a Base64-encoded payment token provided by a third-party payment processor + * Returned for Google-provided payment methods only + */ + instrumentToken: string; + }; + /** + * Name of the instrument displayed on the receipt + * Returned for payment methods provided by your app only + */ + displayName: string; + }; + // Any customer information (e.g. email address) requested + customerInfo: { + /** Customer email. */ + email: string; + }; + }; + /** + * The delivery address if user requested. + * Will appear if userDecision is Transactions.DELIVERY_ADDRESS_UPDATED. + */ + deliveryAddress: Location; +} + +/** + * List of transaction card networks available when paying with Google. + */ +export type CardNetwork = + /** + * Unspecified. + */ + 'UNSPECIFIED' | + /** + * American Express. + */ + 'AMEX' | + /** + * Discover. + */ + 'DISCOVER' | + /** + * Master Card. + */ + 'MASTERCARD' | + /** + * Visa. + */ + 'VISA' | + /** + * JCB. + */ + 'JCB'; + +/** + * List of possible item types. + * @enum {string} + */ +export type ItemType = + /** + * Unspecified. + */ + 'UNSPECIFIED' | + /** + * Regular. + */ + 'REGULAR' | + /** + * Tax. + */ + 'TAX' | + /** + * Discount + */ + 'DISCOUNT' | + /** + * Gratuity + */ + 'GRATUITY' | + /** + * Delivery + */ + 'DELIVERY' | + /** + * Subtotal + */ + 'SUBTOTAL' | + /** + * Fee. For everything else, there's fee. + */ + 'FEE'; + +/** + * List of price types. + * @enum {string} + */ +export type PriceType = + /** + * Unknown. + */ + 'UNKNOWN' | + /** + * Estimate. + */ + 'ESTIMATE' | + /** + * Actual. + */ + 'ACTUAL'; + +/** + * List of possible item types. + * @enum {string} + */ +export type PaymentType = + /** + * Unspecified. + */ + 'UNSPECIFIED' | + /** + * Payment card. + */ + 'PAYMENT_CARD' | + /** + * Bank. + */ + 'BANK' | + /** + * Loyalty program. + */ + 'LOYALTY_PROGRAM' | + /** + * On order fulfillment, such as cash on delivery. + */ + 'ON_FULFILLMENT' | + /** + * Gift card. + */ + 'GIFT_CARD'; + +/** + * List of customer information properties that can be requested. + */ +export type CustomerInfoProperties = 'EMAIL'; + +/** + * List of possible order confirmation user decisions + * @enum {string} + */ +export type ConfirmationDecision = + /** + * Order was approved by user. + */ + 'ORDER_ACCEPTED' | + /** + * Order was declined by user. + */ + 'ORDER_REJECTED' | + /** + * Order was not declined, but the delivery address was updated during + * confirmation. + */ + 'DELIVERY_ADDRESS_UPDATED' | + /** + * Order was not declined, but the cart was updated during confirmation. + */ + 'CART_CHANGE_REQUESTED'; + +/** + * List of possible order states. + * @enum {string} + */ +export type OrderState = + /** + * Order was rejected. + */ + 'REJECTED' | + /** + * Order was confirmed by integrator and is active. + */ + 'CONFIRMED' | + /** + * User cancelled the order. + */ + 'CANCELLED' | + /** + * Order is being delivered. + */ + 'IN_TRANSIT' | + /** + * User performed a return. + */ + 'RETURNED' | + /** + * User received what was ordered. + */ + 'FULFILLED'; + +/** + * List of possible actions to take on the order. + * @enum {string} + */ +export type OrderAction = + /** + * View details. + */ + 'VIEW_DETAILS' | + /** + * Modify order. + */ + 'MODIFY' | + /** + * Cancel order. + */ + 'CANCEL' | + /** + * Return order. + */ + 'RETURN' | + /** + * Exchange order. + */ + 'EXCHANGE' | + /** + * Email. + */ + 'EMAIL' | + /** + * Call. + */ + 'CALL' | + /** + * Reorder. + */ + 'REORDER' | + /** + * Review. + */ + 'REVIEW'; + +/** + * List of possible types of order rejection. + * @enum {string} + */ +export type RejectionType = + /** + * Unknown + */ + 'UNKNOWN' | + /** + * Payment was declined. + */ + 'PAYMENT_DECLINED'; + +/** + * List of possible order state objects. + * @enum {string} + */ +export type OrderStateInfo = + /** + * Information about order rejection. Used with {@link RejectionInfo}. + */ + 'rejectionInfo' | + /** + * Information about order receipt. Used with {@link ReceiptInfo}. + */ + 'receipt' | + /** + * Information about order cancellation. Used with {@link CancellationInfo}. + */ + 'cancellationInfo' | + /** + * Information about in-transit order. Used with {@link TransitInfo}. + */ + 'inTransitInfo' | + /** + * Information about order fulfillment. Used with {@link FulfillmentInfo}. + */ + 'fulfillmentInfo' | + /** + * Information about order return. Used with {@link ReturnInfo}. + */ + 'returnInfo'; + +/** + * List of possible order transaction requirements check result types. + * @enum {string} + */ +export type ResultType = + /** + * Unspecified. + */ + 'RESULT_TYPE_UNSPECIFIED' | + /** + * OK to continue transaction. + */ + 'OK' | + /** + * User is expected to take action, e.g. enable payments, to continue + * transaction. + */ + 'USER_ACTION_REQUIRED' | + /** + * Transactions are not supported on current device/surface. + */ + 'ASSISTANT_SURFACE_NOT_SUPPORTED' | + /** + * Transactions are not supported for current region/country. + */ + 'REGION_NOT_SUPPORTED'; + +/** + * List of possible user decisions to give delivery address. + * @enum {string} + */ +export type DeliveryAddressDecision = + /** + * Unknown. + */ + 'UNKNOWN_USER_DECISION' | + /** + * User granted delivery address. + */ + 'ACCEPTED' | + /** + * User denied to give delivery address. + */ + 'REJECTED'; + +/** + * List of possible order location types. + * @enum {string} + */ +export type LocationType = + /** + * Unknown. + */ + 'UNKNOWN' | + /** + * Delivery location for an order. + */ + 'DELIVERY' | + /** + * Business location of order provider. + */ + 'BUSINESS' | + /** + * Origin of the order. + */ + 'ORIGIN' | + /** + * Destination of the order. + */ + 'DESTINATION'; + +/** + * List of possible order time types. + * @enum {string} + */ + +export type TimeType = + /** + * Unknown. + */ + 'UNKNOWN' | + /** + * Date of delivery for the order. + */ + 'DELIVERY_DATE' | + /** + * Estimated Time of Arrival for order. + */ + 'ETA' | + /** + * Reservation time. + */ + 'RESERVATION_SLOT'; + +/** + * Values related to supporting transactions. + * @type {Object} + */ +export const TransactionValues: { + /** + * List of transaction card networks available when paying with Google. + * @enum {string} + */ + readonly CardNetwork: { + /** + * Unspecified. + */ + UNSPECIFIED: CardNetwork, + /** + * American Express. + */ + AMEX: CardNetwork, + /** + * Discover. + */ + DISCOVER: CardNetwork, + /** + * Master Card. + */ + MASTERCARD: CardNetwork, + /** + * Visa. + */ + VISA: CardNetwork, + /** + * JCB. + */ + JCB: CardNetwork, + }, + + /** + * List of possible item types. + * @enum {string} + */ + readonly ItemType: { + /** + * Unspecified. + */ + UNSPECIFIED: ItemType, + /** + * Regular. + */ + REGULAR: ItemType, + /** + * Tax. + */ + TAX: ItemType, + /** + * Discount + */ + DISCOUNT: ItemType, + /** + * Gratuity + */ + GRATUITY: ItemType, + /** + * Delivery + */ + DELIVERY: ItemType, + /** + * Subtotal + */ + SUBTOTAL: ItemType, + /** + * Fee. For everything else, there's fee. + */ + FEE: ItemType, + }, + + /** + * List of price types. + * @enum {string} + */ + readonly PriceType: { + /** + * Unknown. + */ + UNKNOWN: PriceType, + /** + * Estimate. + */ + ESTIMATE: PriceType, + /** + * Actual. + */ + ACTUAL: PriceType, + }, + + /** + * List of possible item types. + * @enum {string} + */ + readonly PaymentType: { + /** + * Unspecified. + */ + UNSPECIFIED: PaymentType, + /** + * Payment card. + */ + PAYMENT_CARD: PaymentType, + /** + * Bank. + */ + BANK: PaymentType, + /** + * Loyalty program. + */ + LOYALTY_PROGRAM: PaymentType, + /** + * On order fulfillment, such as cash on delivery. + */ + ON_FULFILLMENT: PaymentType, + /** + * Gift card. + */ + GIFT_CARD: PaymentType, + }, + + /** + * List of possible order confirmation user decisions + * @enum {string} + */ + readonly CustomerInfoProperties: { + EMAIL: CustomerInfoProperties, + }, + + /** + * List of possible order confirmation user decisions + * @enum {string} + */ + readonly ConfirmationDecision: { + /** + * Order was approved by user. + */ + ACCEPTED: ConfirmationDecision, + /** + * Order was declined by user. + */ + REJECTED: ConfirmationDecision, + /** + * Order was not declined, but the delivery address was updated during + * confirmation. + */ + DELIVERY_ADDRESS_UPDATED: ConfirmationDecision, + /** + * Order was not declined, but the cart was updated during confirmation. + */ + CART_CHANGE_REQUESTED: ConfirmationDecision, + }, + + /** + * List of possible order states. + * @enum {string} + */ + readonly OrderState: { + /** + * Order was rejected. + */ + REJECTED: OrderState, + /** + * Order was confirmed by integrator and is active. + */ + CONFIRMED: OrderState, + /** + * User cancelled the order. + */ + CANCELLED: OrderState, + /** + * Order is being delivered. + */ + IN_TRANSIT: OrderState, + /** + * User performed a return. + */ + RETURNED: OrderState, + /** + * User received what was ordered. + */ + FULFILLED: OrderState, + }, + + /** + * List of possible actions to take on the order. + * @enum {string} + */ + readonly OrderAction: { + /** + * View details. + */ + VIEW_DETAILS: OrderAction, + /** + * Modify order. + */ + MODIFY: OrderAction, + /** + * Cancel order. + */ + CANCEL: OrderAction, + /** + * Return order. + */ + RETURN: OrderAction, + /** + * Exchange order. + */ + EXCHANGE: OrderAction, + /** + * Email. + */ + EMAIL: OrderAction, + /** + * Call. + */ + CALL: OrderAction, + /** + * Reorder. + */ + REORDER: OrderAction, + /** + * Review. + */ + REVIEW: OrderAction, + }, + + /** + * List of possible types of order rejection. + * @enum {string} + */ + readonly RejectionType: { + /** + * Unknown + */ + UNKNOWN: RejectionType, + /** + * Payment was declined. + */ + PAYMENT_DECLINED: RejectionType, + }, + + /** + * List of possible order state objects. + * @enum {string} + */ + readonly OrderStateInfo: { + /** + * Information about order rejection. Used with {@link RejectionInfo}. + */ + REJECTION: OrderStateInfo, + /** + * Information about order receipt. Used with {@link ReceiptInfo}. + */ + RECEIPT: OrderStateInfo, + /** + * Information about order cancellation. Used with {@link CancellationInfo}. + */ + CANCELLATION: OrderStateInfo, + /** + * Information about in-transit order. Used with {@link TransitInfo}. + */ + IN_TRANSIT: OrderStateInfo, + /** + * Information about order fulfillment. Used with {@link FulfillmentInfo}. + */ + FULFILLMENT: OrderStateInfo, + /** + * Information about order return. Used with {@link ReturnInfo}. + */ + RETURN: OrderStateInfo, + }, + + /** + * List of possible order transaction requirements check result types. + * @enum {string} + */ + readonly ResultType: { + /** + * Unspecified. + */ + UNSPECIFIED: ResultType, + /** + * OK to continue transaction. + */ + OK: ResultType, + /** + * User is expected to take action, e.g. enable payments, to continue + * transaction. + */ + USER_ACTION_REQUIRED: ResultType, + /** + * Transactions are not supported on current device/surface. + */ + ASSISTANT_SURFACE_NOT_SUPPORTED: ResultType, + /** + * Transactions are not supported for current region/country. + */ + REGION_NOT_SUPPORTED: ResultType, + }, + + /** + * List of possible user decisions to give delivery address. + * @enum {string} + */ + readonly DeliveryAddressDecision: { + /** + * Unknown. + */ + UNKNOWN: DeliveryAddressDecision, + /** + * User granted delivery address. + */ + ACCEPTED: DeliveryAddressDecision, + /** + * User denied to give delivery address. + */ + REJECTED: DeliveryAddressDecision, + }, + + /** + * List of possible user decisions to give delivery address. + * @enum {string} + */ + readonly LocationType: { + /** + * Unknown. + */ + UNKNOWN: LocationType, + /** + * Delivery location for an order. + */ + DELIVERY: LocationType, + /** + * Business location of order provider. + */ + BUSINESS: LocationType, + /** + * Origin of the order. + */ + ORIGIN: LocationType, + /** + * Destination of the order. + */ + DESTINATION: LocationType, + }, + + /** + * List of possible user decisions to give delivery address. + * @enum {string} + */ + readonly TimeType: { + /** + * Unknown. + */ + UNKNOWN: TimeType, + /** + * Date of delivery for the order. + */ + DELIVERY_DATE: TimeType, + /** + * Estimated Time of Arrival for order. + */ + ETA: TimeType, + /** + * Reservation time. + */ + RESERVATION_SLOT: TimeType, + }, +}; + +/** + * Class for initializing and constructing Order with chainable interface. + */ +export class Order { + /** + * ID for the order. Required. + */ + readonly id: string; + + /** + * Cart for the order. + */ + readonly cart: Cart; + + /** + * Items not held in the order cart. + */ + readonly otherItems: LineItem[]; + + /** + * Image for the order. + */ + readonly image: Image; + + /** + * TOS for the order. + */ + readonly termsOfServiceUrl: string; + + /** + * Total price for the order. + * @type {Price} + */ + readonly totalPrice: Price; + + /** + * Extensions for this order. Used for vertical-specific order attributes, + * like times and locations. + */ + readonly extension: object; + + /** + * Constructor for Order. + * + * @param {string} orderId Unique identifier for the order. + */ + constructor(orderId: string); + + /** + * Set the cart for this order. + * + * @param {Cart} cart Cart for this order. + * @return {Order} Returns current constructed Order. + */ + setCart(cart: Cart): Order; + + /** + * Adds a single item or list of items to the non-cart items list. + * + * @param {LineItem|Array} items Line Items to add. + * @return {Order} Returns current constructed Order. + */ + addOtherItems(items: LineItem | LineItem[]): Order; + + /** + * Sets the image for this order. + * + * @param {string} url Image source URL. + * @param {string} accessibilityText Text to replace for image for + * accessibility. + * @param {number=} width Width of the image. + * @param {number=} height Height of the image. + * @return {Order} Returns current constructed Order. + */ + setImage(url: string, accessibilityText: string, width?: number, height?: number): Order; + + /** + * Set the TOS for this order. + * + * @param {string} tos String URL of the TOS. + * @return {Order} Returns current constructed Order. + */ + setTermsOfService(url: string): Order; + + /** + * Sets the total price for this order. + * + * @param {string} priceType One of TransactionValues.PriceType. + * @param {string} currencyCode Currency code of price. + * @param {number} units Unit count of price. + * @param {number=} nanos Partial unit count of price. + * @return {Order} Returns current constructed Order. + */ + setTotalPrice(priceType: PriceType, currencyCode: string, units: number, nanos?: number): Order; + + /** + * Adds an associated location to the order. Up to 2 locations can be added. + * + * @param {string} type One of TransactionValues.LocationType. + * @param {Location} location Location to add. + * @return {Order} Returns current constructed Order. + */ + addLocation(type: string, location: Location): Order; + + /** + * Sets an associated time to the order. + * + * @param {string} type One of TransactionValues.TimeType. + * @param {string} time Time to add. Time should be ISO 8601 representation + * of time value. Could be date, datetime, or duration. + * @return {Order} Returns current constructed Order. + */ + setTime(type: string, time: string): Order; +} + +/** + * Class for initializing and constructing Cart with chainable interface. + */ +export class Cart { + /** + * ID for the cart. Optional. + */ + readonly id: string; + + /** + * Merchant providing the cart. + */ + readonly merchant: object; + + /** + * Optional notes about the cart. + */ + readonly notes: string; + + /** + * Items held in the order cart. + */ + readonly lineItems: LineItem[]; + + /** + * Non-line items. + */ + readonly otherItems: LineItem[]; + + /** + * Constructor for Cart. + * + * @param {string=} cartId Optional unique identifier for the cart. + */ + constructor(cartId?: string); + + /** + * Set the merchant for this cart. + * + * @param {string} id Merchant ID. + * @param {string} name Name of the merchant. + * @return {Cart} Returns current constructed Cart. + */ + setMerchant(id: string, name: string): Cart; + + /** + * Set the notes for this cart. + * + * @param {string} notes Notes. + * @return {Cart} Returns current constructed Cart. + */ + setNotes(notes: string): Cart; + + /** + * Adds a single item or list of items to the cart. + * + * @param {LineItem|Array} items Line Items to add. + * @return {Cart} Returns current constructed Cart. + */ + addLineItems(items: LineItem | LineItem[]): Cart; + + /** + * Adds a single item or list of items to the non-items list of this cart. + * + * @param {LineItem|Array} items Line Items to add. + * @return {Cart} Returns current constructed Cart. + */ + addOtherItems(items: LineItem | LineItem[]): Cart; +} + +/** + * Class for initializing and constructing LineItem with chainable interface. + */ +export class LineItem { + /** + * Item ID. + */ + readonly id: string; + + /** + * Name of the item. + */ + readonly name: string; + + /** + * Item price. + */ + readonly price: Price; + + /** + * Sublines for current item. Only valid if item type is REGULAR. + */ + readonly sublines: string[] | LineItem[]; + + /** + * Image of the item. + */ + readonly image: Image; + + /** + * Type of the item. One of TransactionValues.ItemType. + */ + readonly type: ItemType; + + /** + * Quantity of the item. + */ + readonly quantity: number; + + /** + * Description for the item. + */ + readonly description: string; + + /** + * Offer ID for the item. + */ + readonly offerId: string; + + /** + * Constructor for LineItem. + * + * @param {string} lineItemId Unique identifier for the item. + * @param {string} name Name of the item. + */ + constructor(lineItemId: string, name: string); + + /** + * Adds a single item or list of items or notes to the sublines. Only valid + * if item type is REGULAR. + * + * @param {string|LineItem|Array} items Sublines to add. + * @return {LineItem} Returns current constructed LineItem. + */ + addSublines(items: string | LineItem | string[] | LineItem[]): LineItem; + + /** + * Sets the image for this item. + * + * @param {string} url Image source URL. + * @param {string} accessibilityText Text to replace for image for + * accessibility. + * @param {number=} width Width of the image. + * @param {number=} height Height of the image. + * @return {LineItem} Returns current constructed LineItem. + */ + setImage(url: string, accessibilityText: string, width?: number, height?: number): LineItem; + + /** + * Sets the price of this item. + * + * @param {string} priceType One of TransactionValues.PriceType. + * @param {string} currencyCode Currency code of price. + * @param {number} units Unit count of price. + * @param {number=} nanos Partial unit count of price. + * @return {LineItem} Returns current constructed LineItem. + */ + setPrice(priceType: PriceType, currencyCode: string, units: number, nanos?: number): LineItem; + + /** + * Set the type of the item. + * + * @param {string} type Type of the item. One of TransactionValues.ItemType. + * @return {LineItem} Returns current constructed LineItem. + */ + setType(type: ItemType): LineItem; + + /** + * Set the quantity of the item. + * + * @param {number} quantity Quantity of the item. + * @return {LineItem} Returns current constructed LineItem. + */ + setQuantity(quantity: number): LineItem; + + /** + * Set the description of the item. + * + * @param {string} description Description of the item. + * @return {LineItem} Returns current constructed LineItem. + */ + setDescription(description: string): LineItem; + + /** + * Set the Offer ID of the item. + * + * @param {string} offerId Offer ID of the item. + * @return {LineItem} Returns current constructed LineItem. + */ + setOfferId(offerId: string): LineItem; +} + +/** + * Class for initializing and constructing OrderUpdate with chainable interface. + */ +export class OrderUpdate { + /** + * Google provided identifier of the order. + * @type {string} + */ + readonly googleOrderId?: string; + + /** + * App provided identifier of the order. + * @type {string} + */ + readonly actionOrderId?: string; + + /** + * State of the order. + * @type {Object} + */ + readonly orderState?: OrderState; + + /** + * Updates for items in the order. Mapped by item id to state or price. + * @type {Object} + */ + readonly lineItemUpdates?: object; + + /** + * UTC timestamp of the order update. + * @type {Object} + */ + readonly updateTime?: object; + + /** + * Actionable items presented to the user to manage the order. + * @type {Object} + */ + readonly orderManagementActions: object[]; + + /** + * Notification content to the user for the order update. + * @type {Object} + */ + readonly userNotification: object; + + /** + * Updated total price of the order. + * @type {Price} + */ + readonly totalPrice: Price; + + /** + * Constructor for OrderUpdate. + * + * @param {string} orderId Unique identifier of the order. + * @param {boolean} isGoogleOrderId True if the order ID is provided by + * Google. False if the order ID is app provided. + */ + constructor(orderId: string, isGoogleOrderId: boolean); + + /** + * Set the Google provided order ID of the order. + * + * @param {string} orderId Google provided order ID. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + setGoogleOrderId(orderId: string): OrderUpdate; + + /** + * Set the Action provided order ID of the order. + * + * @param {string} orderId Action provided order ID. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + setActionOrderId(orderId: string): OrderUpdate; + + /** + * Set the state of the order. + * + * @param {string} state One of TransactionValues.OrderState. + * @param {string} label Label for the order state. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + setOrderState(state: OrderState, label: string): OrderUpdate; + + /** + * Set the update time of the order. + * + * @param {number} seconds Seconds since Unix epoch. + * @param {number=} nanos Partial time units. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + setUpdateTime(seconds: number, nanos?: number): OrderUpdate; + + /** + * Set the user notification content of the order update. + * + * @param {string} title Title of the notification. + * @param {text} text Text of the notification. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + setUserNotification(title: string, text: object): OrderUpdate; + + /** + * Sets the total price for this order. + * + * @param {string} priceType One of TransactionValues.PriceType. + * @param {string} currencyCode Currency code of price. + * @param {number} units Unit count of price. + * @param {number=} nanos Partial unit count of price. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + setTotalPrice(priceType: PriceType, currencyCode: string, units: number, nanos?: number): OrderUpdate; + + /** + * Adds an actionable item for the user to manage the order. + * + * @param {string} type One of TransactionValues.OrderActions. + * @param {string} label Button label. + * @param {string} url URL to open when button is clicked. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + addOrderManagementAction(type: OrderAction, label: string, url: string): OrderUpdate; + + /** + * Adds a single price update for a particular line item in the order. + * + * @param {string} itemId Line item ID for the order item updated. + * @param {string} priceType One of TransactionValues.PriceType. + * @param {string} currencyCode Currency code of new price. + * @param {number} units Unit count of new price. + * @param {number=} nanos Partial unit count of new price. + * @param {string=} reason Reason for the price change. Required unless a + * reason for this line item change was already declared in + * addLineItemStateUpdate. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + addLineItemPriceUpdate(itemId: string, priceType: PriceType, currencyCode: string, units: number, nanos?: number, reason?: string): OrderUpdate; + + /** + * Adds a single state update for a particular line item in the order. + * + * @param {string} itemId Line item ID for the order item updated. + * @param {string} state One of TransactionValues.OrderState. + * @param {string} label Label for the new item state. + * @param {string=} reason Reason for the price change. This will overwrite + * any reason given in addLineitemPriceUpdate. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + addLineItemStateUpdate(itemId: string, state: OrderState, label: string, reason?: string): OrderUpdate; + + /** + * Sets some extra information about the order. Takes an order update info + * type, and any accompanying data. This should only be called once per + * order update. + * + * @param {string} type One of TransactionValues.OrderStateInfo. + * @param {Object} data Proper Object matching the data necessary for the info + * type. For instance, for the TransactionValues.OrderStateInfo.RECEIPT info + * type, use the {@link ReceiptInfo} data type. + * @return {OrderUpdate} Returns current constructed OrderUpdate. + */ + setInfo(type: string, data: object): OrderUpdate; +} diff --git a/types/actions-on-google/tsconfig.json b/types/actions-on-google/tsconfig.json new file mode 100644 index 0000000000..cc0a4b24d9 --- /dev/null +++ b/types/actions-on-google/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "assistant-app.d.ts", + "actions-sdk-app.d.ts", + "api-ai-app.d.ts", + "response-builder.d.ts", + "transactions.d.ts", + "actions-on-google-tests.ts" + ] +} diff --git a/types/actions-on-google/tslint.json b/types/actions-on-google/tslint.json new file mode 100644 index 0000000000..3db14f85ea --- /dev/null +++ b/types/actions-on-google/tslint.json @@ -0,0 +1 @@ +{ "extends": "dtslint/dt.json" } From 5035da30ca8f48c167b3b0c9deba1b617ac48acd Mon Sep 17 00:00:00 2001 From: Joel Hegg Date: Fri, 6 Oct 2017 23:56:24 -0400 Subject: [PATCH 05/79] Remove license headers and add a NOTICE file per PR feedback --- types/actions-on-google/NOTICE | 13 +++++++++++++ .../actions-on-google/actions-on-google-tests.ts | 4 ++-- types/actions-on-google/actions-sdk-app.d.ts | 16 ---------------- types/actions-on-google/api-ai-app.d.ts | 16 ---------------- types/actions-on-google/assistant-app.d.ts | 16 ---------------- types/actions-on-google/index.d.ts | 16 ---------------- types/actions-on-google/response-builder.d.ts | 16 ---------------- types/actions-on-google/transactions.d.ts | 16 ---------------- 8 files changed, 15 insertions(+), 98 deletions(-) create mode 100644 types/actions-on-google/NOTICE diff --git a/types/actions-on-google/NOTICE b/types/actions-on-google/NOTICE new file mode 100644 index 0000000000..262b08db36 --- /dev/null +++ b/types/actions-on-google/NOTICE @@ -0,0 +1,13 @@ +License Notices: + +The API definitions are from Actions on Google reference site [1] and actions-on-google library [2]. +The actions-on-google library is licensed under the Apache 2.0 License [3]. + +The code documentation is reproduced from work created and shared by Google [4] +and used according to terms described in the Creative Commons 3.0 Attribution License [5]. + +[1] https://developers.google.com/actions/ +[2] https://github.com/actions-on-google/actions-on-google-nodejs +[3] http://www.apache.org/licenses/LICENSE-2.0 +[4] https://developers.google.com/readme/policies/ +[5] http://creativecommons.org/licenses/by/3.0/ diff --git a/types/actions-on-google/actions-on-google-tests.ts b/types/actions-on-google/actions-on-google-tests.ts index b7632217e2..dfa431da78 100644 --- a/types/actions-on-google/actions-on-google-tests.ts +++ b/types/actions-on-google/actions-on-google-tests.ts @@ -29,5 +29,5 @@ function testApiAi(request: express.Request, response: express.Response) { } const expressApp = express(); -expressApp.get('/actionssdk', (req, res) => testActionsSdk); -expressApp.get('/apiai', (req, res) => testApiAi); +expressApp.get('/actionssdk', testActionsSdk); +expressApp.get('/apiai', testApiAi); diff --git a/types/actions-on-google/actions-sdk-app.d.ts b/types/actions-on-google/actions-sdk-app.d.ts index be399b4783..6511ed7f6d 100644 --- a/types/actions-on-google/actions-sdk-app.d.ts +++ b/types/actions-on-google/actions-sdk-app.d.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { Request, Response } from 'express'; import { AssistantApp, DeviceLocation, SessionStartedFunction, User } from './assistant-app'; diff --git a/types/actions-on-google/api-ai-app.d.ts b/types/actions-on-google/api-ai-app.d.ts index b7dc28b86a..13f5f852dd 100644 --- a/types/actions-on-google/api-ai-app.d.ts +++ b/types/actions-on-google/api-ai-app.d.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { Request, Response } from 'express'; import { AssistantApp, DeviceLocation, SessionStartedFunction, User } from './assistant-app'; diff --git a/types/actions-on-google/assistant-app.d.ts b/types/actions-on-google/assistant-app.d.ts index 01d3bcf046..d964940c54 100644 --- a/types/actions-on-google/assistant-app.d.ts +++ b/types/actions-on-google/assistant-app.d.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import { Request, Response } from 'express'; import { BasicCard, Carousel, List, OptionItem, RichResponse } from './response-builder'; diff --git a/types/actions-on-google/index.d.ts b/types/actions-on-google/index.d.ts index 3c0daa5a28..997ae3a7e8 100644 --- a/types/actions-on-google/index.d.ts +++ b/types/actions-on-google/index.d.ts @@ -4,22 +4,6 @@ // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 2.4 -/** - * Copyright 2016 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /** * The Actions on Google client library. * https://developers.google.com/actions/ diff --git a/types/actions-on-google/response-builder.d.ts b/types/actions-on-google/response-builder.d.ts index add7b9fc03..1133879819 100644 --- a/types/actions-on-google/response-builder.d.ts +++ b/types/actions-on-google/response-builder.d.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /** * A collection of response builders. */ diff --git a/types/actions-on-google/transactions.d.ts b/types/actions-on-google/transactions.d.ts index e64e69bdf1..d2773896b8 100644 --- a/types/actions-on-google/transactions.d.ts +++ b/types/actions-on-google/transactions.d.ts @@ -1,19 +1,3 @@ -/** - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /** * A collection of Transaction related constants, utility functions, and * builders. From f277b21d513336ad9b4e5d6a27fe2dacd9dee539 Mon Sep 17 00:00:00 2001 From: Joel Hegg Date: Sat, 7 Oct 2017 00:23:17 -0400 Subject: [PATCH 06/79] Add strictFunctionTypes parameter now required by Travis --- types/actions-on-google/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/types/actions-on-google/tsconfig.json b/types/actions-on-google/tsconfig.json index cc0a4b24d9..79fed964d6 100644 --- a/types/actions-on-google/tsconfig.json +++ b/types/actions-on-google/tsconfig.json @@ -7,6 +7,7 @@ "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, + "strictFunctionTypes": true, "baseUrl": "../", "typeRoots": [ "../" From ec116e2efb133f4efbc8b4925e5055c736dde548 Mon Sep 17 00:00:00 2001 From: Joel Hegg Date: Tue, 10 Oct 2017 14:14:01 -0400 Subject: [PATCH 07/79] Upgrade to actions-on-google 1.5 --- .../actions-on-google-tests.ts | 8 +- types/actions-on-google/assistant-app.d.ts | 104 ++++++++-------- .../{api-ai-app.d.ts => dialogflow-app.d.ts} | 115 ++++++++++-------- types/actions-on-google/index.d.ts | 7 +- types/actions-on-google/tsconfig.json | 2 +- 5 files changed, 125 insertions(+), 111 deletions(-) rename types/actions-on-google/{api-ai-app.d.ts => dialogflow-app.d.ts} (84%) diff --git a/types/actions-on-google/actions-on-google-tests.ts b/types/actions-on-google/actions-on-google-tests.ts index dfa431da78..e57e1e8d98 100644 --- a/types/actions-on-google/actions-on-google-tests.ts +++ b/types/actions-on-google/actions-on-google-tests.ts @@ -1,4 +1,4 @@ -import { ActionsSdkApp, ActionsSdkAppOptions, ApiAiApp, ApiAiAppOptions, AssistantApp, +import { ActionsSdkApp, ActionsSdkAppOptions, DialogflowApp, DialogflowAppOptions, AssistantApp, Responses, Transactions } from 'actions-on-google'; import * as express from 'express'; @@ -14,8 +14,8 @@ function testActionsSdk(request: express.Request, response: express.Response) { app.handleRequest(actionMap); } -function testApiAi(request: express.Request, response: express.Response) { - const app = new ApiAiApp({request, response}); +function testDialogflow(request: express.Request, response: express.Response) { + const app = new DialogflowApp({request, response}); const actionMap = new Map(); actionMap.set(app.StandardIntents.MAIN, () => { const order: Transactions.Order = app.buildOrder('foo'); @@ -30,4 +30,4 @@ function testApiAi(request: express.Request, response: express.Response) { const expressApp = express(); expressApp.get('/actionssdk', testActionsSdk); -expressApp.get('/apiai', testApiAi); +expressApp.get('/dialogflow', testDialogflow); diff --git a/types/actions-on-google/assistant-app.d.ts b/types/actions-on-google/assistant-app.d.ts index d964940c54..3bfd78f514 100644 --- a/types/actions-on-google/assistant-app.d.ts +++ b/types/actions-on-google/assistant-app.d.ts @@ -425,7 +425,7 @@ export class AssistantApp { * Constructor for AssistantApp object. * Should not be instantiated; rather instantiate one of the subclasses * - * {@link ActionsSdkApp} or {@link ApiAiApp}. + * {@link ActionsSdkApp} or {@link DialogflowApp}. * * @param {Object} options JSON configuration. * @param {Object} options.request Express HTTP request object. @@ -473,8 +473,8 @@ export class AssistantApp { * * app.handleRequest(actionMap); * - * // API.AI - * const app = new ApiAiApp({request: req, response: res}); + * // Dialogflow + * const app = new DialogflowApp({request: req, response: res}); * const NAME_ACTION = 'make_name'; * const COLOR_ARGUMENT = 'color'; * const NUMBER_ARGUMENT = 'number'; @@ -493,7 +493,7 @@ export class AssistantApp { * * @param {(Function|Map)} handler The handler (or Map of handlers) for the request. * @actionssdk - * @apiai + * @dialogflow */ handleRequest(handler?: RequestHandler | Map): void; @@ -512,7 +512,7 @@ export class AssistantApp { * equivalent to just asking for DEVICE_PRECISE_LOCATION * * @example - * const app = new ApiAIApp({request: req, response: res}); + * const app = new DialogflowApp({request: req, response: res}); * const REQUEST_PERMISSION_ACTION = 'request_permission'; * const GET_RIDE_ACTION = 'get_ride'; * @@ -549,7 +549,7 @@ export class AssistantApp { * @return A response is sent to Assistant to ask for the user's permission; for any * invalid input, we return null. * @actionssdk - * @apiai + * @dialogflow */ askForPermissions(context: string, permissions: string[], dialogState?: object): object; @@ -557,7 +557,7 @@ export class AssistantApp { * Checks whether user is in transactable state. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const WELCOME_INTENT = 'input.welcome'; * const TXN_REQ_COMPLETE = 'txn.req.complete'; * @@ -591,7 +591,7 @@ export class AssistantApp { * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. * @return {Object} HTTP response. * @actionssdk - * @apiai + * @dialogflow */ askForTransactionRequirements(transactionConfig?: ActionPaymentTransactionConfig | GooglePaymentTransactionConfig, dialogState?: object): object; @@ -599,7 +599,7 @@ export class AssistantApp { * Asks user to confirm transaction information. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const WELCOME_INTENT = 'input.welcome'; * const TXN_COMPLETE = 'txn.complete'; * @@ -631,7 +631,7 @@ export class AssistantApp { * options and order options. * @param {Object=} dialogState JSON object the app uses to hold dialog state that * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. - * @apiai + * @dialogflow */ askForTransactionDecision(order: object, transactionConfig?: ActionPaymentTransactionConfig | GooglePaymentTransactionConfig, dialogState?: object): object; @@ -655,7 +655,7 @@ export class AssistantApp { * * {@link AssistantApp#getUserName|getUserName} * * @example - * const app = new ApiAiApp({request: req, response: res}); + * const app = new DialogflowApp({request: req, response: res}); * const REQUEST_PERMISSION_ACTION = 'request_permission'; * const GET_RIDE_ACTION = 'get_ride'; * @@ -687,7 +687,7 @@ export class AssistantApp { * @return A response is sent to the Assistant to ask for the user's permission; * for any invalid input, we return null. * @actionssdk - * @apiai + * @dialogflow */ askForPermission(context: string, permission: string, dialogState?: object): object; @@ -699,7 +699,7 @@ export class AssistantApp { * @example * const app = new ActionsSdkApp({request: request, response: response}); * // or - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * app.askForPermissions("To get you a ride", [ * app.SupportedPermissions.NAME, * app.SupportedPermissions.DEVICE_PRECISE_LOCATION @@ -711,7 +711,7 @@ export class AssistantApp { * } * * @return {boolean} true if permissions granted. - * @apiai + * @dialogflow * @actionssdk */ isPermissionGranted(): boolean; @@ -720,8 +720,8 @@ export class AssistantApp { * Asks user for delivery address. * * @example - * // For ApiAiApp: - * const app = new ApiAiApp({request, response}); + * // For DialogflowApp: + * const app = new DialogflowApp({request, response}); * const WELCOME_INTENT = 'input.welcome'; * const DELIVERY_INTENT = 'delivery.address'; * @@ -771,7 +771,7 @@ export class AssistantApp { * will be circulated back by Assistant. * @return {Object} HTTP response. * @actionssdk - * @apiai + * @dialogflow */ askForDeliveryAddress(reason: string, dialogState?: object): object; @@ -779,7 +779,7 @@ export class AssistantApp { * Asks user for a confirmation. * * @example - * const app = new ApiAiApp({ request, response }); + * const app = new DialogflowApp({ request, response }); * const WELCOME_INTENT = 'input.welcome'; * const CONFIRMATION = 'confirmation'; * @@ -806,7 +806,7 @@ export class AssistantApp { * @param {Object=} dialogState JSON object the app uses to hold dialog state that * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. * @actionssdk - * @apiai + * @dialogflow */ askForConfirmation(prompt?: string, dialogState?: object): object; @@ -814,7 +814,7 @@ export class AssistantApp { * Asks user for a timezone-agnostic date and time. * * @example - * const app = new ApiAiApp({ request, response }); + * const app = new DialogflowApp({ request, response }); * const WELCOME_INTENT = 'input.welcome'; * const DATETIME = 'datetime'; * @@ -850,7 +850,7 @@ export class AssistantApp { * @param {Object=} dialogState JSON object the app uses to hold dialog state that * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. * @actionssdk - * @apiai + * @dialogflow */ askForDateTime(initialPrompt?: string, datePrompt?: string, timePrompt?: string, dialogState?: object): object; @@ -866,7 +866,7 @@ export class AssistantApp { * consent". * * @example - * const app = new ApiAiApp({ request, response }); + * const app = new DialogflowApp({ request, response }); * const WELCOME_INTENT = 'input.welcome'; * const SIGN_IN = 'sign.in'; * @@ -891,7 +891,7 @@ export class AssistantApp { * @param {Object=} dialogState JSON object the app uses to hold dialog state that * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. * @actionssdk - * @apiai + * @dialogflow */ askForSignIn(dialogState?: object): object; @@ -899,7 +899,7 @@ export class AssistantApp { * Requests the user to switch to another surface during the conversation. * * @example - * const app = new ApiAiApp({ request, response }); + * const app = new DialogflowApp({ request, response }); * const WELCOME_INTENT = 'input.welcome'; * const SHOW_IMAGE = 'show.image'; * @@ -937,7 +937,7 @@ export class AssistantApp { * the surface. * @param {Object=} dialogState JSON object the app uses to hold dialog state that * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. - * @apiai + * @dialogflow * @actionssdk */ askForNewSurface(context: string, notificationTitle: string, capabilities: string[], dialogState?: object): object; @@ -949,14 +949,14 @@ export class AssistantApp { * see {@link AssistantApp#askForPermissions|askForPermissions}). * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * // or * const app = new ActionsSdkApp({request: request, response: response}); * const userId = app.getUser().userId; * * @return {User} Null if no value. * @actionssdk - * @apiai + * @dialogflow */ getUser(): User; @@ -966,7 +966,7 @@ export class AssistantApp { * returns null. * * @example - * const app = new ApiAIApp({request: req, response: res}); + * const app = new DialogflowApp({request: req, response: res}); * const REQUEST_PERMISSION_ACTION = 'request_permission'; * const SAY_NAME_ACTION = 'get_name'; * @@ -989,7 +989,7 @@ export class AssistantApp { * app.handleRequest(actionMap); * @return {UserName} Null if name permission is not granted. * @actionssdk - * @apiai + * @dialogflow */ getUserName(): UserName; @@ -999,12 +999,12 @@ export class AssistantApp { * For example, 'en-US' represents US English. * * @example - * const app = new ApiAiApp({request, response}); + * const app = new DialogflowApp({request, response}); * const locale = app.getUserLocale(); * * @return {string} User's locale, e.g. 'en-US'. Null if no locale given. * @actionssdk - * @apiai + * @dialogflow */ getUserLocale(): string; @@ -1014,7 +1014,7 @@ export class AssistantApp { * If device info is unavailable, returns null. * * @example - * const app = new ApiAiApp({request: req, response: res}); + * const app = new DialogflowApp({request: req, response: res}); * // or * const app = new ActionsSdkApp({request: req, response: res}); * app.askForPermission("To get you a ride", @@ -1027,7 +1027,7 @@ export class AssistantApp { * * @return {DeviceLocation} Null if location permission is not granted. * @actionssdk - * @apiai + * @dialogflow */ getDeviceLocation(): DeviceLocation; @@ -1036,7 +1036,7 @@ export class AssistantApp { * * @return {number} One of AssistantApp.InputTypes. * Null if no input type given. - * @apiai + * @dialogflow * @actionssdk */ getInputType(): number; @@ -1050,7 +1050,7 @@ export class AssistantApp { * the argument object will be in Proto2 format (snake_case, etc). * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const WELCOME_INTENT = 'input.welcome'; * const NUMBER_INTENT = 'input.number'; * @@ -1071,7 +1071,7 @@ export class AssistantApp { * @param {string} argName Name of the argument. * @return {Object} Argument value matching argName * or null if no matching argument. - * @apiai + * @dialogflow * @actionssdk */ getArgumentCommon(argName: string): object; @@ -1081,7 +1081,7 @@ export class AssistantApp { * askForTransactionRequirements. Null if no result given. * * @return {string} One of Transactions.ResultType. - * @apiai + * @dialogflow * @actionssdk */ getTransactionRequirementsResult(): string; @@ -1091,7 +1091,7 @@ export class AssistantApp { * * @return {DeliveryAddress} Delivery address information. Null if user * denies permission, or no address given. - * @apiai + * @dialogflow * @actionssdk */ getDeliveryAddress(): Location; @@ -1103,7 +1103,7 @@ export class AssistantApp { * @return {TransactionDecision} Transaction decision data. Returns object with * userDecision only if user declines. userDecision will be one of * Transactions.ConfirmationDecision. Null if no decision given. - * @apiai + * @dialogflow * @actionssdk */ getTransactionDecision(): TransactionDecision; @@ -1113,7 +1113,7 @@ export class AssistantApp { * * False if user replied with negative response. Null if no user * confirmation decision given. - * @apiai + * @dialogflow * @actionssdk */ getUserConfirmation(): boolean | null; @@ -1123,7 +1123,7 @@ export class AssistantApp { * * @return {DateTime} Date and time given by the user. Null if no user * date and time given. - * @apiai + * @dialogflow * @actionssdk */ getDateTime(): DateTime; @@ -1132,9 +1132,9 @@ export class AssistantApp { * Gets status of user sign in request. * * @return {string} Result of user sign in request. One of - * ApiAiApp.SignInStatus or ActionsSdkApp.SignInStatus + * DialogflowApp.SignInStatus or ActionsSdkApp.SignInStatus * Null if no sign in status. - * @apiai + * @dialogflow * @actionssdk */ getSignInStatus(): string; @@ -1146,7 +1146,7 @@ export class AssistantApp { * @return {boolean} True if user device has the given capability. * * @example - * const app = new ApiAIApp({request: req, response: res}); + * const app = new DialogflowApp({request: req, response: res}); * const DESCRIBE_SOMETHING = 'DESCRIBE_SOMETHING'; * * function describe (app) { @@ -1160,7 +1160,7 @@ export class AssistantApp { * actionMap.set(DESCRIBE_SOMETHING, describe); * app.handleRequest(actionMap); * - * @apiai + * @dialogflow * @actionssdk */ hasSurfaceCapability(requestedCapability: string): boolean; @@ -1170,7 +1170,7 @@ export class AssistantApp { * * @return {Array} Supported surface capabilities, as defined in * AssistantApp.SurfaceCapabilities. - * @apiai + * @dialogflow * @actionssdk */ getSurfaceCapabilities(): string[]; @@ -1180,7 +1180,7 @@ export class AssistantApp { * * @return {Array} Empty if no available surfaces. * @actionssdk - * @apiai + * @dialogflow */ getAvailableSurfaces(): Surface[]; @@ -1193,7 +1193,7 @@ export class AssistantApp { * {@link SurfaceCapabilities}. * @return {boolean} True if user has a capability available on some surface. * - * @apiai + * @dialogflow * @actionssdk */ hasAvailableSurfaceCapabilities(capabilities: string | string[]): boolean; @@ -1204,7 +1204,7 @@ export class AssistantApp { * @return {boolean} True if user has triggered conversation on a new device * following the NEW_SURFACE intent. * @actionssdk - * @apiai + * @dialogflow */ isNewSurface(): boolean; @@ -1214,7 +1214,7 @@ export class AssistantApp { * transactions. * * @return {boolean} True if app is being used in Sandbox mode. - * @apiai + * @dialogflow * @actionssdk */ isInSandbox(): boolean; @@ -1249,7 +1249,7 @@ export class AssistantApp { * * @return {number} The current reprompt count. Null if no reprompt count * available (e.g. not in the NO_INPUT intent). - * @apiai + * @dialogflow * @actionssdk */ getRepromptCount(): number; @@ -1284,7 +1284,7 @@ export class AssistantApp { * * @return {boolean} True if in a NO_INPUT intent and this is the final turn * of dialog. - * @apiai + * @dialogflow * @actionssdk */ isFinalReprompt(): boolean; diff --git a/types/actions-on-google/api-ai-app.d.ts b/types/actions-on-google/dialogflow-app.d.ts similarity index 84% rename from types/actions-on-google/api-ai-app.d.ts rename to types/actions-on-google/dialogflow-app.d.ts index 13f5f852dd..c63a78f2ee 100644 --- a/types/actions-on-google/api-ai-app.d.ts +++ b/types/actions-on-google/dialogflow-app.d.ts @@ -5,41 +5,41 @@ import { Carousel, List, RichResponse, SimpleResponse } from './response-builder import { TransactionDecision } from './transactions'; // --------------------------------------------------------------------------- -// API.AI support +// Dialogflow support // --------------------------------------------------------------------------- /** - * API.AI {@link https://docs.api.ai/docs/concept-contexts|Context}. + * DialogflowApp {@link https://dialogflow.com/docs/concept-contexts|Context}. */ export interface Context { /** Full name of the context. */ name: string; /** * Parameters carried within this context. - * See {@link https://docs.api.ai/docs/concept-actions#section-extracting-values-from-contexts|here}. + * See {@link https://dialogflow.com/docs/concept-actions#section-extracting-values-from-contexts|here}. */ parameters: object; /** Remaining number of intents */ lifespan: number; } -export interface ApiAiAppOptions { +export interface DialogflowAppOptions { request: Request; response: Response; sessionStarted?: SessionStartedFunction; } /** - * This is the class that handles the communication with API.AI's fulfillment API. + * This is the class that handles the communication with Dialogflow's fulfillment API. */ -export class ApiAiApp extends AssistantApp { +export class DialogflowApp extends AssistantApp { /** - * Constructor for ApiAiApp object. - * To be used in the API.AI fulfillment webhook logic. + * Constructor for DialogflowApp object. + * To be used in the Dialogflow fulfillment webhook logic. * * @example - * const ApiAiApp = require('actions-on-google').ApiAiApp; - * const app = new ApiAiApp({request: request, response: response, + * const DialogflowApp = require('actions-on-google').DialogflowApp; + * const app = new DialogflowApp({request: request, response: response, * sessionStarted:sessionStarted}); * * @param {Object} options JSON configuration. @@ -47,31 +47,44 @@ export class ApiAiApp extends AssistantApp { * @param {Object} options.response Express HTTP response object. * @param {Function=} options.sessionStarted Function callback when session starts. * Only called if webhook is enabled for welcome/triggering intents, and - * called from Web Simulator or Google Home device (i.e., not API.AI simulator). - * @apiai + * called from Web Simulator or Google Home device (i.e., not Dialogflow simulator). + * @dialogflow */ - constructor(options: ApiAiAppOptions); + constructor(options: DialogflowAppOptions); /** - * Verifies whether the request comes from API.AI. + * @deprecated + * Verifies whether the request comes from Dialogflow. * * @param {string} key The header key specified by the developer in the - * API.AI Fulfillment settings of the app. + * Dialogflow Fulfillment settings of the app. * @param {string} value The private value specified by the developer inside the * fulfillment header. - * @return {boolean} True if the request comes from API.AI. - * @apiai + * @return {boolean} True if the request comes from Dialogflow. + * @dialogflow */ isRequestFromApiAi(key: string, value: string): boolean; + /** + * Verifies whether the request comes from Dialogflow. + * + * @param {string} key The header key specified by the developer in the + * Dialogflow Fulfillment settings of the app. + * @param {string} value The private value specified by the developer inside the + * fulfillment header. + * @return {boolean} True if the request comes from Dialogflow. + * @dialogflow + */ + isRequestFromDialogflow(key: string, value: string): boolean; + /** * Get the current intent. Alternatively, using a handler Map with * {@link AssistantApp#handleRequest|handleRequest}, * the client library will automatically handle the incoming intents. - * 'Intent' in the API.ai context translates into the current action. + * 'Intent' in the Dialogflow context translates into the current action. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * * function responseHandler (app) { * const intent = app.getIntent(); @@ -90,7 +103,7 @@ export class ApiAiApp extends AssistantApp { * app.handleRequest(responseHandler); * * @return {string} Intent id or null if no value (action name). - * @apiai + * @dialogflow */ getIntent(): string; @@ -103,7 +116,7 @@ export class ApiAiApp extends AssistantApp { * the argument object will be in Proto2 format (snake_case, etc). * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const WELCOME_INTENT = 'input.welcome'; * const NUMBER_INTENT = 'input.number'; * @@ -124,7 +137,7 @@ export class ApiAiApp extends AssistantApp { * @param {string} argName Name of the argument. * @return {Object} Argument value matching argName * or null if no matching argument. - * @apiai + * @dialogflow */ getArgument(argName: string): object; @@ -136,7 +149,7 @@ export class ApiAiApp extends AssistantApp { * as part of the return object. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const WELCOME_INTENT = 'input.welcome'; * const NUMBER_INTENT = 'input.number'; * const OUT_CONTEXT = 'output_context'; @@ -164,12 +177,12 @@ export class ApiAiApp extends AssistantApp { * @param {string} argName Name of the argument. * @return {Object} Object containing value property and optional original * property matching context argument. Null if no matching argument. - * @apiai + * @dialogflow */ getContextArgument(contextName: string, argName: string): object; /** - * Returns the RichResponse constructed in API.AI response builder. + * Returns the RichResponse constructed in Dialogflow response builder. * * @example * const app = new App({request: req, response: res}); @@ -192,14 +205,14 @@ export class ApiAiApp extends AssistantApp { * * app.handleRequest(actionMap); * - * @return {RichResponse} RichResponse created in API.AI. If no RichResponse was + * @return {RichResponse} RichResponse created in Dialogflow. If no RichResponse was * created, an empty RichResponse is returned. - * @apiai + * @dialogflow */ getIncomingRichResponse(): RichResponse; /** - * Returns the List constructed in API.AI response builder. + * Returns the List constructed in Dialogflow response builder. * * @example * const app = new App({request: req, response: res}); @@ -220,14 +233,14 @@ export class ApiAiApp extends AssistantApp { * * app.handleRequest(actionMap); * - * @return {List} List created in API.AI. If no List was created, an empty + * @return {List} List created in Dialogflow. If no List was created, an empty * List is returned. - * @apiai + * @dialogflow */ getIncomingList(): List; /** - * Returns the Carousel constructed in API.AI response builder. + * Returns the Carousel constructed in Dialogflow response builder. * * @example * const app = new App({request: req, response: res}); @@ -248,9 +261,9 @@ export class ApiAiApp extends AssistantApp { * * app.handleRequest(actionMap); * - * @return {Carousel} Carousel created in API.AI. If no Carousel was created, + * @return {Carousel} Carousel created in Dialogflow. If no Carousel was created, * an empty Carousel is returned. - * @apiai + * @dialogflow */ getIncomingCarousel(): Carousel; @@ -283,7 +296,7 @@ export class ApiAiApp extends AssistantApp { * * @return {string} Option key of selected item. Null if no option selected or * if current intent is not OPTION intent. - * @apiai + * @dialogflow */ getSelectedOption(): string; @@ -296,7 +309,7 @@ export class ApiAiApp extends AssistantApp { * for a bye message until the bug is fixed. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const WELCOME_INTENT = 'input.welcome'; * const NUMBER_INTENT = 'input.number'; * @@ -319,7 +332,7 @@ export class ApiAiApp extends AssistantApp { * response. * @param {Array=} noInputs Array of re-prompts when the user does not respond (max 3). * @return {Object} HTTP response. - * @apiai + * @dialogflow */ ask(inputPrompt: string | SimpleResponse | RichResponse, noInputs?: string[]): object; @@ -327,7 +340,7 @@ export class ApiAiApp extends AssistantApp { * Asks to collect the user's input with a list. * * @example - * const app = new ApiAiApp({request, response}); + * const app = new DialogflowApp({request, response}); * const WELCOME_INTENT = 'input.welcome'; * const OPTION_INTENT = 'option.select'; * @@ -361,7 +374,7 @@ export class ApiAiApp extends AssistantApp { * response. * @param {List} list List built with {@link AssistantApp#buildList|buildList}. * @return {Object} HTTP response. - * @apiai + * @dialogflow */ askWithList(inputPrompt: string | RichResponse | SimpleResponse, list: List): object; @@ -369,7 +382,7 @@ export class ApiAiApp extends AssistantApp { * Asks to collect the user's input with a carousel. * * @example - * const app = new ApiAiApp({request, response}); + * const app = new DialogflowApp({request, response}); * const WELCOME_INTENT = 'input.welcome'; * const OPTION_INTENT = 'option.select'; * @@ -404,7 +417,7 @@ export class ApiAiApp extends AssistantApp { * @param {Carousel} carousel Carousel built with * {@link AssistantApp#buildCarousel|buildCarousel}. * @return {Object} HTTP response. - * @apiai + * @dialogflow */ askWithCarousel(inputPrompt: string | RichResponse | SimpleResponse, carousel: Carousel): object; @@ -412,7 +425,7 @@ export class ApiAiApp extends AssistantApp { * Tells the Assistant to render the speech response and close the mic. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const WELCOME_INTENT = 'input.welcome'; * const NUMBER_INTENT = 'input.number'; * @@ -433,7 +446,7 @@ export class ApiAiApp extends AssistantApp { * @param {string|SimpleResponse|RichResponse} textToSpeech Final response. * Spoken response can be SSML. * @return The response that is sent back to Assistant. - * @apiai + * @dialogflow */ tell(speechResponse: string | SimpleResponse | RichResponse): object; @@ -441,7 +454,7 @@ export class ApiAiApp extends AssistantApp { * Set a new context for the current intent. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const CONTEXT_NUMBER = 'number'; * const NUMBER_ARGUMENT = 'myNumber'; * @@ -460,10 +473,10 @@ export class ApiAiApp extends AssistantApp { * actionMap.set(NUMBER_INTENT, numberIntent); * app.handleRequest(actionMap); * - * @param {string} name Name of the context. API.AI converts to lowercase. + * @param {string} name Name of the context. Dialogflow converts to lowercase. * @param {int} [lifespan=1] Context lifespan. * @param {Object=} parameters Context JSON parameters. - * @apiai + * @dialogflow */ setContext(name: string, lifespan: number, parameters?: object): void; @@ -471,7 +484,7 @@ export class ApiAiApp extends AssistantApp { * Returns the incoming contexts for this intent. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const CONTEXT_NUMBER = 'number'; * const NUMBER_ARGUMENT = 'myNumber'; * @@ -500,7 +513,7 @@ export class ApiAiApp extends AssistantApp { * app.handleRequest(actionMap); * * @return {Context[]} Empty if no active contexts. - * @apiai + * @dialogflow */ getContexts(): Context[]; @@ -508,7 +521,7 @@ export class ApiAiApp extends AssistantApp { * Returns the incoming context by name for this intent. * * @example - * const app = new ApiAiapp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * const CONTEXT_NUMBER = 'number'; * const NUMBER_ARGUMENT = 'myNumber'; * @@ -538,7 +551,7 @@ export class ApiAiApp extends AssistantApp { * * @return {Object} Context value matching name * or null if no matching context. - * @apiai + * @dialogflow */ getContext(name: string): object; @@ -546,11 +559,11 @@ export class ApiAiApp extends AssistantApp { * Gets the user's raw input query. * * @example - * const app = new ApiAiApp({request: request, response: response}); + * const app = new DialogflowApp({request: request, response: response}); * app.tell('You said ' + app.getRawInput()); * * @return {string} User's raw query or null if no value. - * @apiai + * @dialogflow */ getRawInput(): string; } diff --git a/types/actions-on-google/index.d.ts b/types/actions-on-google/index.d.ts index 997ae3a7e8..0e8fd831e5 100644 --- a/types/actions-on-google/index.d.ts +++ b/types/actions-on-google/index.d.ts @@ -1,4 +1,4 @@ -// Type definitions for actions-on-google 1.4 +// Type definitions for actions-on-google 1.5 // Project: https://github.com/actions-on-google/actions-on-google-nodejs // Definitions by: Joel Hegg , Pilwon Huh // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped @@ -14,10 +14,11 @@ import * as Responses from './response-builder'; export { AssistantApp, AssistantAppOptions, RequestHandler, SessionStartedFunction, State } from './assistant-app'; export { ActionsSdkApp, ActionsSdkAppOptions } from './actions-sdk-app'; -export { ApiAiApp, ApiAiAppOptions } from './api-ai-app'; +export { DialogflowApp, DialogflowAppOptions } from './dialogflow-app'; export { Transactions }; export { Responses }; // Backwards compatibility export { AssistantApp as Assistant } from './assistant-app'; export { ActionsSdkApp as ActionsSdkAssistant } from './actions-sdk-app'; -export { ApiAiApp as ApiAiAssistant } from './api-ai-app'; +export { DialogflowApp as ApiAiAssistant } from './dialogflow-app'; +export { DialogflowApp as ApiAiApp } from './dialogflow-app'; diff --git a/types/actions-on-google/tsconfig.json b/types/actions-on-google/tsconfig.json index 79fed964d6..6682b5c194 100644 --- a/types/actions-on-google/tsconfig.json +++ b/types/actions-on-google/tsconfig.json @@ -20,7 +20,7 @@ "index.d.ts", "assistant-app.d.ts", "actions-sdk-app.d.ts", - "api-ai-app.d.ts", + "dialogflow-app.d.ts", "response-builder.d.ts", "transactions.d.ts", "actions-on-google-tests.ts" From aad047cd06ffc379a08c1cb35c00925f14defe89 Mon Sep 17 00:00:00 2001 From: Joel Hegg Date: Fri, 20 Oct 2017 02:24:04 -0400 Subject: [PATCH 08/79] Temporarily stage the 1.5.1 .js files --- .../staging/actions-on-google.js | 38 + .../staging/actions-sdk-app.js | 877 +++++++ .../staging/assistant-app.js | 2262 +++++++++++++++++ .../staging/dialogflow-app.js | 1105 ++++++++ .../staging/response-builder.js | 800 ++++++ .../actions-on-google/staging/transactions.js | 1439 +++++++++++ 6 files changed, 6521 insertions(+) create mode 100644 types/actions-on-google/staging/actions-on-google.js create mode 100644 types/actions-on-google/staging/actions-sdk-app.js create mode 100644 types/actions-on-google/staging/assistant-app.js create mode 100644 types/actions-on-google/staging/dialogflow-app.js create mode 100644 types/actions-on-google/staging/response-builder.js create mode 100644 types/actions-on-google/staging/transactions.js diff --git a/types/actions-on-google/staging/actions-on-google.js b/types/actions-on-google/staging/actions-on-google.js new file mode 100644 index 0000000000..81c21dde03 --- /dev/null +++ b/types/actions-on-google/staging/actions-on-google.js @@ -0,0 +1,38 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Actions on Google client library. + * https://developers.google.com/actions/ + */ + +'use strict'; + +const AssistantApp = require('./assistant-app'); + +module.exports = { + AssistantApp: AssistantApp.AssistantApp, + State: AssistantApp.State, + ActionsSdkApp: require('./actions-sdk-app'), + DialogflowApp: require('./dialogflow-app'), + Transactions: require('./transactions'), + Responses: require('./response-builder'), + // Backwards compatibility + Assistant: AssistantApp.AssistantApp, + ActionsSdkAssistant: require('./actions-sdk-app'), + ApiAiAssistant: require('./dialogflow-app'), + ApiAiApp: require('./dialogflow-app') +}; diff --git a/types/actions-on-google/staging/actions-sdk-app.js b/types/actions-on-google/staging/actions-sdk-app.js new file mode 100644 index 0000000000..f5ef131b24 --- /dev/null +++ b/types/actions-on-google/staging/actions-sdk-app.js @@ -0,0 +1,877 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Debug = require('debug'); +const debug = Debug('actions-on-google:debug'); +const error = Debug('actions-on-google:error'); +const app = require('./assistant-app'); +const AssistantApp = app.AssistantApp; +const State = app.State; +const transformToCamelCase = require('./utils/transform').transformToCamelCase; + +// Constants +const CONVERSATION_API_AGENT_VERSION_HEADER = 'Agent-Version-Label'; +const RESPONSE_CODE_OK = 200; +const INPUTS_MAX = 3; +const CONVERSATION_API_SIGNATURE_HEADER = 'authorization'; + +// Configure logging for hosting platforms that only support console.log and console.error +debug.log = console.log.bind(console); +error.log = console.error.bind(console); + +// --------------------------------------------------------------------------- +// Actions SDK support +// --------------------------------------------------------------------------- + +/** + * This is the class that handles the conversation API directly from Assistant, + * providing implementation for all the methods available in the API. + */ +class ActionsSdkApp extends AssistantApp { + /** + * Constructor for ActionsSdkApp object. + * To be used in the Actions SDK HTTP endpoint logic. + * + * @example + * const ActionsSdkApp = require('actions-on-google').ActionsSdkApp; + * const app = new ActionsSdkApp({request: request, response: response, + * sessionStarted:sessionStarted}); + * + * @param {Object} options JSON configuration. + * @param {Object} options.request Express HTTP request object. + * @param {Object} options.response Express HTTP response object. + * @param {Function=} options.sessionStarted Function callback when session starts. + * @actionssdk + */ + constructor (options) { + debug('ActionsSdkApp constructor'); + super(options, () => this.body_); + + // If request is from AoG and in Proto2 format, convert to Proto3. + if (this.body_ && !this.isNotApiVersionOne_()) { + this.body_ = transformToCamelCase(this.body_); + } + + if (this.body_ && + this.body_.conversation && + this.body_.conversation.type && + this.body_.conversation.type === this.ConversationStages.NEW && + this.sessionStarted_ && typeof this.sessionStarted_ === 'function') { + this.sessionStarted_(); + } else if (this.sessionStarted_ && typeof this.sessionStarted_ !== 'function') { + this.handleError_('options.sessionStarted must be a Function'); + } + } + + /** + * Validates whether request is from Assistant through signature verification. + * Uses Google-Auth-Library to verify authorization token against given + * Google Cloud Project ID. Auth token is given in request header with key, + * "Authorization". + * + * @example + * const app = new ActionsSdkApp({request, response}); + * app.isRequestFromAssistant('nodejs-cloud-test-project-1234') + * .then(() => { + * app.ask('Hey there, thanks for stopping by!'); + * }) + * .catch(err => { + * response.status(400).send(); + * }); + * + * @param {string} projectId Google Cloud Project ID for the Assistant app. + * @return {Promise} Promise resolving with google-auth-library LoginTicket + * if request is from a valid source, otherwise rejects with the error reason + * for an invalid token. + * @actionssdk + */ + isRequestFromAssistant (projectId) { + debug('isRequestFromAssistant: projectId=%s', projectId); + const googleAuthClient = require('./utils/auth').googleAuthClient; + const jwtToken = this.request_.get(CONVERSATION_API_SIGNATURE_HEADER); + + return new Promise((resolve, reject) => { + if (!jwtToken) { + const errorMsg = 'No incoming API Signature JWT token'; + error(errorMsg); + reject(errorMsg); + } + googleAuthClient.verifyIdToken(jwtToken, projectId, (err, login) => { + if (err) { + error('ID token verification Failed: ' + err); + reject(err); + } else { + resolve(login); + } + }); + }); + } + + /** + * Gets the request Conversation API version. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * const apiVersion = app.getApiVersion(); + * + * @return {string} Version value or null if no value. + * @actionssdk + */ + getApiVersion () { + debug('getApiVersion'); + return this.apiVersion_ || this.actionsApiVersion_; + } + + /** + * Gets the user's raw input query. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * app.tell('You said ' + app.getRawInput()); + * + * @return {string} User's raw query or null if no value. + * @actionssdk + */ + getRawInput () { + debug('getRawInput'); + const input = this.getTopInput_(); + if (!input) { + error('Failed to get top Input.'); + return null; + } + if (!input.rawInputs || input.rawInputs.length === 0) { + error('Missing user raw input'); + return null; + } + const rawInput = input.rawInputs[0]; + if (!rawInput.query) { + error('Missing query for user raw input'); + return null; + } + return rawInput.query; + } + + /** + * Gets previous JSON dialog state that the app sent to Assistant. + * Alternatively, use the app.data field to store JSON values between requests. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * const dialogState = app.getDialogState(); + * + * @return {Object} JSON object provided to the Assistant in the previous + * user turn or {} if no value. + * @actionssdk + */ + getDialogState () { + debug('getDialogState'); + if (this.body_.conversation && this.body_.conversation.conversationToken) { + return JSON.parse(this.body_.conversation.conversationToken); + } + return {}; + } + + /** + * Gets the "versionLabel" specified inside the Action Package. + * Used by app to do version control. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * const actionVersionLabel = app.getActionVersionLabel(); + * + * @return {string} The specified version label or null if unspecified. + * @actionssdk + */ + getActionVersionLabel () { + debug('getActionVersionLabel'); + const versionLabel = this.request_.get(CONVERSATION_API_AGENT_VERSION_HEADER); + if (versionLabel) { + return versionLabel; + } else { + return null; + } + } + + /** + * Gets the unique conversation ID. It's a new ID for the initial query, + * and stays the same until the end of the conversation. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * const conversationId = app.getConversationId(); + * + * @return {string} Conversation ID or null if no value. + * @actionssdk + */ + getConversationId () { + debug('getConversationId'); + if (!this.body_.conversation || !this.body_.conversation.conversationId) { + error('No conversation ID'); + return null; + } + return this.body_.conversation.conversationId; + } + + /** + * Get the current intent. Alternatively, using a handler Map with + * {@link AssistantApp#handleRequest|handleRequest}, the client library will + * automatically handle the incoming intents. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * + * function responseHandler (app) { + * const intent = app.getIntent(); + * switch (intent) { + * case app.StandardIntents.MAIN: + * const inputPrompt = app.buildInputPrompt(false, 'Welcome to action snippets! Say anything.'); + * app.ask(inputPrompt); + * break; + * + * case app.StandardIntents.TEXT: + * app.tell('You said ' + app.getRawInput()); + * break; + * } + * } + * + * app.handleRequest(responseHandler); + * + * @return {string} Intent id or null if no value. + * @actionssdk + */ + getIntent () { + debug('getIntent'); + const input = this.getTopInput_(); + if (!input) { + error('Missing intent from request body'); + return null; + } + return input.intent; + } + + /** + * Get the argument value by name from the current intent. If the argument + * is not a text argument, the entire argument object is returned. + * + * Note: If incoming request is using an API version under 2 (e.g. 'v1'), + * the argument object will be in Proto2 format (snake_case, etc). + * + * @param {string} argName Name of the argument. + * @return {string} Argument value matching argName + * or null if no matching argument. + * @actionssdk + */ + getArgument (argName) { + return this.getArgumentCommon(argName); + } + + /** + * Returns the option key user chose from options response. + * + * @example + * const app = new App({request: req, response: res}); + * + * function pickOption (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askWithCarousel('Which of these looks good?', + * app.buildCarousel().addItems( + * app.buildOptionItem('another_choice', ['Another choice']). + * setTitle('Another choice').setDescription('Choose me!'))); + * } else { + * app.ask('What would you like?'); + * } + * } + * + * function optionPicked (app) { + * app.ask('You picked ' + app.getSelectedOption()); + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.TEXT, pickOption); + * actionMap.set(app.StandardIntents.OPTION, optionPicked); + * + * app.handleRequest(actionMap); + * + * @return {string} Option key of selected item. Null if no option selected or + * if current intent is not OPTION intent. + * @actionssdk + */ + getSelectedOption () { + debug('getSelectedOption'); + if (this.getArgument(this.BuiltInArgNames.OPTION)) { + return this.getArgument(this.BuiltInArgNames.OPTION); + } + debug('Failed to get selected option'); + return null; + } + + /** + * Asks to collect user's input; all user's queries need to be sent to + * the app. + * {@link https://developers.google.com/actions/policies/general-policies#user_experience|The guidelines when prompting the user for a response must be followed at all times}. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * + * function mainIntent (app) { + * const inputPrompt = app.buildInputPrompt(true, 'Hi! ' + + * 'I can read out an ordinal like ' + + * '123. Say a number.', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * + * function rawInput (app) { + * if (app.getRawInput() === 'bye') { + * app.tell('Goodbye!'); + * } else { + * const inputPrompt = app.buildInputPrompt(true, 'You said, ' + + * app.getRawInput() + '', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, mainIntent); + * actionMap.set(app.StandardIntents.TEXT, rawInput); + * + * app.handleRequest(actionMap); + * + * @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and + * no-input prompts. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by App. + * @return {(Object|null)} The response that is sent to Assistant to ask user to provide input. + * @actionssdk + */ + ask (inputPrompt, dialogState) { + debug('ask: inputPrompt=%s, dialogState=%s', + JSON.stringify(inputPrompt), JSON.stringify(dialogState)); + const expectedIntent = this.buildExpectedIntent_(this.StandardIntents.TEXT, []); + if (!expectedIntent) { + error('Error in building expected intent'); + return null; + } + return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState); + } + + /** + * Asks to collect user's input with a list. + * + * @example + * const app = new ActionsSdkApp({request, response}); + * + * function welcomeIntent (app) { + * app.askWithlist('Which of these looks good?', + * app.buildList('List title') + * .addItems([ + * app.buildOptionItem(SELECTION_KEY_ONE, + * ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2']) + * .setTitle('Number one'), + * app.buildOptionItem(SELECTION_KEY_TWO, + * ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2']) + * .setTitle('Number two'), + * ])); + * } + * + * function optionIntent (app) { + * if (app.getSelectedOption() === SELECTION_KEY_ONE) { + * app.tell('Number one is a great choice!'); + * } else { + * app.tell('Number two is a great choice!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.TEXT, welcomeIntent); + * actionMap.set(app.StandardIntents.OPTION, optionIntent); + * app.handleRequest(actionMap); + * + * @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and + * no-input prompts. Cannot contain basic card. + * @param {List} list List built with {@link AssistantApp#buildList|buildList}. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {(Object|null)} The response that is sent to Assistant to ask user to provide input. + * @actionssdk + */ + askWithList (inputPrompt, list, dialogState) { + debug('askWithList: inputPrompt=%s, list=%s, dialogState=%s', + JSON.stringify(inputPrompt), JSON.stringify(list), JSON.stringify(dialogState)); + if (!list || typeof list !== 'object') { + this.handleError_('Invalid list'); + return null; + } + if (list.items.length < 2) { + this.handleError_('List requires at least 2 items'); + return null; + } + const expectedIntent = this.buildExpectedIntent_(this.StandardIntents.OPTION, []); + if (!expectedIntent) { + error('Error in building expected intent'); + return null; + } + if (this.isNotApiVersionOne_()) { + expectedIntent.inputValueData = Object.assign({ + [this.ANY_TYPE_PROPERTY_]: this.InputValueDataTypes_.OPTION + }, { + listSelect: list + }); + } else { + expectedIntent.inputValueSpec = { + optionValueSpec: { + listSelect: list + } + }; + } + return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState); + } + + /** + * Asks to collect user's input with a carousel. + * + * @example + * const app = new ActionsSdkApp({request, response}); + * + * function welcomeIntent (app) { + * app.askWithCarousel('Which of these looks good?', + * app.buildCarousel() + * .addItems([ + * app.buildOptionItem(SELECTION_KEY_ONE, + * ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2']) + * .setTitle('Number one'), + * app.buildOptionItem(SELECTION_KEY_TWO, + * ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2']) + * .setTitle('Number two'), + * ])); + * } + * + * function optionIntent (app) { + * if (app.getSelectedOption() === SELECTION_KEY_ONE) { + * app.tell('Number one is a great choice!'); + * } else { + * app.tell('Number two is a great choice!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.TEXT, welcomeIntent); + * actionMap.set(app.StandardIntents.OPTION, optionIntent); + * app.handleRequest(actionMap); + * + * @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and + * no-input prompts. Cannot contain basic card. + * @param {Carousel} carousel Carousel built with + * {@link AssistantApp#buildCarousel|buildCarousel}. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {(Object|null)} The response that is sent to Assistant to ask user to provide input. + * @actionssdk + */ + askWithCarousel (inputPrompt, carousel, dialogState) { + debug('askWithCarousel: inputPrompt=%s, carousel=%s, dialogState=%s', + JSON.stringify(inputPrompt), JSON.stringify(carousel), JSON.stringify(dialogState)); + if (!carousel || typeof carousel !== 'object') { + this.handleError_('Invalid carousel'); + return null; + } + if (carousel.items.length < 2) { + this.handleError_('Carousel requires at least 2 items'); + return null; + } + const expectedIntent = this.buildExpectedIntent_(this.StandardIntents.OPTION, []); + if (!expectedIntent) { + error('Error in building expected intent'); + return null; + } + if (this.isNotApiVersionOne_()) { + expectedIntent.inputValueData = Object.assign({ + [this.ANY_TYPE_PROPERTY_]: this.InputValueDataTypes_.OPTION + }, { + carouselSelect: carousel + }); + } else { + expectedIntent.inputValueSpec = { + optionValueSpec: { + carouselSelect: carousel + } + }; + } + return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState); + } + + /** + * Tells Assistant to render the speech response and close the mic. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * + * function mainIntent (app) { + * const inputPrompt = app.buildInputPrompt(true, 'Hi! ' + + * 'I can read out an ordinal like ' + + * '123. Say a number.', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * + * function rawInput (app) { + * if (app.getRawInput() === 'bye') { + * app.tell('Goodbye!'); + * } else { + * const inputPrompt = app.buildInputPrompt(true, 'You said, ' + + * app.getRawInput() + '', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, mainIntent); + * actionMap.set(app.StandardIntents.TEXT, rawInput); + * + * app.handleRequest(actionMap); + * + * @param {string|SimpleResponse|RichResponse} textToSpeech Final response. + * Spoken response can be SSML. + * @return {(Object|null)} The HTTP response that is sent back to Assistant. + * @actionssdk + */ + tell (textToSpeech) { + debug('tell: textToSpeech=%s', textToSpeech); + if (!textToSpeech) { + this.handleError_('Invalid speech response'); + return null; + } + const finalResponse = {}; + if (typeof textToSpeech === 'string') { + if (this.isSsml_(textToSpeech)) { + finalResponse.speechResponse = { + ssml: textToSpeech + }; + } else { + finalResponse.speechResponse = { + textToSpeech: textToSpeech + }; + } + } else { + if (textToSpeech.items) { + finalResponse.richResponse = textToSpeech; + } else if (textToSpeech.speech) { + finalResponse.richResponse = this.buildRichResponse() + .addSimpleResponse(textToSpeech); + } else { + this.handleError_('Invalid speech response. Must be string, ' + + 'RichResponse or SimpleResponse.'); + return null; + } + } + const response = this.buildResponseHelper_(null, false, null, finalResponse); + return this.doResponse_(response, RESPONSE_CODE_OK); + } + + /** + * Builds the {@link https://developers.google.com/actions/reference/conversation#InputPrompt|InputPrompt object} + * from initial prompt and no-input prompts. + * + * The App needs one initial prompt to start the conversation. If there is no user response, + * the App re-opens the mic and renders the no-input prompts three times + * (one for each no-input prompt that was configured) to help the user + * provide the right response. + * + * Note: we highly recommend app to provide all the prompts required here in order to ensure a + * good user experience. + * + * @example + * const inputPrompt = app.buildInputPrompt(false, 'Welcome to action snippets! Say a number.', + * ['Say any number', 'Pick a number', 'What is the number?']); + * app.ask(inputPrompt); + * + * @param {boolean} isSsml Indicates whether the text to speech is SSML or not. + * @param {string} initialPrompt The initial prompt the App asks the user. + * @param {Array=} noInputs Array of re-prompts when the user does not respond (max 3). + * @return {Object} An {@link https://developers.google.com/actions/reference/conversation#InputPrompt|InputPrompt object}. + * @actionssdk + */ + buildInputPrompt (isSsml, initialPrompt, noInputs) { + debug('buildInputPrompt: isSsml=%s, initialPrompt=%s, noInputs=%s', + isSsml, initialPrompt, noInputs); + const initials = []; + + if (noInputs) { + if (noInputs.length > INPUTS_MAX) { + error('Invalid number of no inputs'); + return null; + } + } else { + noInputs = []; + } + + this.maybeAddItemToArray_(initialPrompt, initials); + if (isSsml) { + return { + initialPrompts: this.buildPromptsFromSsmlHelper_(initials), + noInputPrompts: this.buildPromptsFromSsmlHelper_(noInputs) + }; + } else { + return { + initialPrompts: this.buildPromptsFromPlainTextHelper_(initials), + noInputPrompts: this.buildPromptsFromPlainTextHelper_(noInputs) + }; + } + } + +// --------------------------------------------------------------------------- +// Private Helpers +// --------------------------------------------------------------------------- + + /** + * Get the top most Input object. + * + * @return {Object} Input object. + * @private + * @actionssdk + */ + getTopInput_ () { + debug('getTopInput_'); + if (!this.body_.inputs || this.body_.inputs.length === 0) { + error('Missing inputs from request body'); + return null; + } + return this.body_.inputs[0]; + } + + /** + * Builds the response to send back to Assistant. + * + * @param {string} conversationToken The dialog state. + * @param {boolean} expectUserResponse The expected user response. + * @param {Object} expectedInput The expected response. + * @param {boolean} finalResponse The final response. + * @return {Object} Final response returned to server. + * @private + * @actionssdk + */ + buildResponseHelper_ (conversationToken, expectUserResponse, expectedInput, finalResponse) { + debug('buildResponseHelper_: conversationToken=%s, expectUserResponse=%s, ' + + 'expectedInput=%s, finalResponse=%s', + conversationToken, expectUserResponse, JSON.stringify(expectedInput), + JSON.stringify(finalResponse)); + const response = {}; + if (conversationToken) { + response.conversationToken = conversationToken; + } + response.expectUserResponse = expectUserResponse; + if (expectedInput) { + response.expectedInputs = expectedInput; + } + if (!expectUserResponse && finalResponse) { + response.finalResponse = finalResponse; + } + return response; + } + + /** + * Helper to add item to an array. + * + * @param {*} item Item to add to the array. + * @param {Array} array Target array. + * @return {undefined} + * @private + * @actionssdk + */ + maybeAddItemToArray_ (item, array) { + debug('maybeAddItemToArray_: item=%s, array=%s', item, array); + if (!array) { + error('Invalid array'); + return; + } + if (!item) { + // ignore add + return; + } + array.push(item); + } + + /** + * Extract session data from the incoming JSON request. + * + * @return {undefined} + * @private + * @actionssdk + */ + extractData_ () { + debug('extractData_'); + if (this.body_.conversation && + this.body_.conversation.conversationToken) { + const json = JSON.parse(this.body_.conversation.conversationToken); + this.data = json.data; + this.state = json.state; + } else { + this.data = {}; + } + } + + /** + * Uses a PermissionsValueSpec object to construct and send a + * permissions request to user. + * + * @param {Object} permissionsSpec PermissionsValueSpec object containing + * the permissions prefix and the permissions requested. + * @param {Object} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {Object} HTTP response object. + * @private + * @actionssdk + */ + fulfillPermissionsRequest_ (permissionsSpec, dialogState) { + debug('fulfillPermissionsRequest_: permissionsSpec=%s, dialogState=%s', + JSON.stringify(permissionsSpec), JSON.stringify(dialogState)); + if (this.isNotApiVersionOne_()) { + return this.fulfillSystemIntent_(this.StandardIntents.PERMISSION, + this.InputValueDataTypes_.PERMISSION, permissionsSpec, + 'PLACEHOLDER_FOR_PERMISSION', dialogState); + } else { + // Build an Expected Intent object. + const expectedIntent = { + intent: this.StandardIntents.PERMISSION + }; + expectedIntent.inputValueSpec = { + permissionValueSpec: permissionsSpec + }; + const inputPrompt = this.buildInputPrompt(false, + 'PLACEHOLDER_FOR_PERMISSION'); + return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState); + } + } + + /** + * Uses a given intent spec to construct and send a non-TEXT intent response + * to Google. + * + * @param {string} intent Name of the intent to fulfill. One of + * {@link AssistantApp#StandardIntents|StandardIntents}. + * @param {string} specType Type of the related intent spec. One of + * {@link AssistantApp#InputValueDataTypes_|InputValueDataTypes_}. + * @param {Object} intentSpec Intent Spec object. Pass null to leave empty. + * @param {string=} promptPlaceholder Some placeholder text for the response + * prompt. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {Object} HTTP response. + * @private + * @actionssdk + */ + fulfillSystemIntent_ (intent, specType, intentSpec, promptPlaceholder, + dialogState) { + debug('fulfillSystemIntent_: intent=%s, specType=%s, intentSpec=%s, ' + + 'promptPlaceholder=%s dialogState=%s', intent, specType, + JSON.stringify(intentSpec), promptPlaceholder, JSON.stringify(dialogState)); + // Build an Expected Intent object. + const expectedIntent = this.buildExpectedIntent_(intent); + if (!expectedIntent) { + error('Error in building expected intent'); + return null; + } + expectedIntent.inputValueData = {}; + if (intentSpec) { + expectedIntent.inputValueData = Object.assign({ + [this.ANY_TYPE_PROPERTY_]: specType + }, intentSpec); + } + // Send an Ask request to Assistant. + const inputPrompt = this.buildInputPrompt(false, promptPlaceholder || + 'PLACEHOLDER_FOR_INTENT'); + return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState); + } + + /** + * Builds the ask response to send back to Assistant. + * + * @param {Object} inputPrompt Holding initial and no-input prompts. + * @param {Array} possibleIntents Array of ExpectedIntents. + * @param {Object} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {(Object|null)} The response that is sent to Assistant to ask user to provide input. + * @private + * @actionssdk + */ + buildAskHelper_ (inputPrompt, possibleIntents, dialogState) { + debug('buildAskHelper_: inputPrompt=%s, possibleIntents=%s, dialogState=%s', + inputPrompt, possibleIntents, JSON.stringify(dialogState)); + if (!inputPrompt) { + error('Invalid input prompt'); + return null; + } + if (typeof inputPrompt === 'string') { + inputPrompt = this.buildInputPrompt(this.isSsml_(inputPrompt), inputPrompt); + } else { + if (inputPrompt.speech) { + inputPrompt = { richInitialPrompt: this.buildRichResponse() + .addSimpleResponse(inputPrompt) }; + } else if (inputPrompt.items) { + inputPrompt = { richInitialPrompt: inputPrompt }; + } + } + if (!dialogState) { + dialogState = { + 'state': (this.state instanceof State ? this.state.getName() : this.state), + 'data': this.data + }; + } else if (Array.isArray(dialogState)) { + error('Invalid dialog state'); + return null; + } + const expectedInputs = [{ + inputPrompt: inputPrompt, + possibleIntents: possibleIntents + }]; + const response = this.buildResponseHelper_( + JSON.stringify(dialogState), + true, // expectedUserResponse + expectedInputs, + null // finalResponse is null b/c dialog is active + ); + return this.doResponse_(response, RESPONSE_CODE_OK); + } + + /** + * Builds an ExpectedIntent object. Refer to {@link ActionsSdkApp#newRuntimeEntity} to create the list + * of runtime entities required by this method. Runtime entities need to be defined in + * the Action Package. + * + * @param {string} intent Developer specified in-dialog intent inside the Action + * Package or an App built-in intent like + * 'assistant.intent.action.TEXT'. + * @return {Object} An {@link https://developers.google.com/actions/reference/conversation#ExpectedIntent|ExpectedIntent object} + encapsulating the intent and the runtime entities. + * @private + * @actionssdk + */ + buildExpectedIntent_ (intent) { + debug('buildExpectedIntent_: intent=%s', intent); + if (!intent || intent === '') { + error('Invalid intent'); + return null; + } + const expectedIntent = { intent }; + return expectedIntent; + } +} + +module.exports = ActionsSdkApp; diff --git a/types/actions-on-google/staging/assistant-app.js b/types/actions-on-google/staging/assistant-app.js new file mode 100644 index 0000000000..0237c0c921 --- /dev/null +++ b/types/actions-on-google/staging/assistant-app.js @@ -0,0 +1,2262 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Debug = require('debug'); +const debug = Debug('actions-on-google:debug'); +const error = Debug('actions-on-google:error'); + +// Response Builder classes +const RichResponse = require('./response-builder').RichResponse; +const BasicCard = require('./response-builder').BasicCard; +const List = require('./response-builder').List; +const Carousel = require('./response-builder').Carousel; +const OptionItem = require('./response-builder').OptionItem; +const isSsml = require('./response-builder').isSsml; + +// Transaction classes +const TransactionValues = require('./transactions').TransactionValues; +const Order = require('./transactions').Order; +const Cart = require('./transactions').Cart; +const LineItem = require('./transactions').LineItem; +const OrderUpdate = require('./transactions').OrderUpdate; + +const transformToSnakeCase = require('./utils/transform').transformToSnakeCase; + +// Constants +const ERROR_MESSAGE = 'Sorry, I am unable to process your request.'; +const API_ERROR_MESSAGE_PREFIX = 'Action Error: '; +const CONVERSATION_API_VERSION_HEADER = 'Google-Assistant-API-Version'; +const ACTIONS_CONVERSATION_API_VERSION_HEADER = 'Google-Actions-API-Version'; +const ACTIONS_CONVERSATION_API_VERSION_TWO = 2; +const RESPONSE_CODE_OK = 200; +const RESPONSE_CODE_BAD_REQUEST = 400; +const HTTP_CONTENT_TYPE_HEADER = 'Content-Type'; +const HTTP_CONTENT_TYPE_JSON = 'application/json'; + +// Configure logging for hosting platforms that only support console.log and console.error +debug.log = console.log.bind(console); +error.log = console.error.bind(console); + +/** + * The Actions on Google client library AssistantApp base class. + * + * This class contains the methods that are shared between platforms to support the conversation API + * protocol from Assistant. It also exports the 'State' class as a helper to represent states by + * name. + */ +class AssistantApp { + /** + * Constructor for AssistantApp object. + * Should not be instantiated; rather instantiate one of the subclasses + * {@link ActionsSdkApp} or {@link DialogflowApp}. + * + * @param {Object} options JSON configuration. + * @param {Object} options.request Express HTTP request object. + * @param {Object} options.response Express HTTP response object. + * @param {Function=} options.sessionStarted Function callback when session starts. + * @param {function(): *} requestData Function that returns the + * request data object to be processed. + */ + constructor (options, requestData) { + debug('AssistantApp constructor'); + + if (!options) { + // ignore for JavaScript inheritance to work + + // As a workaround for pre-existing sample code which incorrectly + // initializes this class without an options object. + this.StandardIntents = { + MAIN: 'assistant.intent.action.MAIN', + TEXT: 'assistant.intent.action.TEXT', + PERMISSION: 'assistant.intent.action.PERMISSION' + }; + return; + } + if (!options.request) { + this.handleError_('Request can NOT be empty.'); + return; + } + if (!options.response) { + this.handleError_('Response can NOT be empty.'); + return; + } + + /** + * The Express HTTP request that the endpoint receives from the Assistant. + * @private + * @type {Object} + */ + this.request_ = options.request; + + /** + * The Express HTTP response the endpoint will return to Assistant. + * @private + * @type {Object} + */ + this.response_ = options.response; + + /** + * 'sessionStarted' callback (optional). + * @private + * @type {Function} + */ + this.sessionStarted_ = options.sessionStarted; + + debug('Request from Assistant: %s', JSON.stringify(this.request_.body)); + + /** + * The request body contains query JSON and previous session variables. + * Assignment using JSON parse/stringify ensures manipulation of this.body_ + * does not affect passed in request body structure. + * @private + * @type {Object} + */ + this.body_ = JSON.parse(JSON.stringify(this.request_.body)); + + /** + * API version describes version of the Actions API request. + * @private + * @type {string} + */ + this.actionsApiVersion_ = null; + // Populates API version from either request header or Dialogflow orig request. + if (this.request_.get(ACTIONS_CONVERSATION_API_VERSION_HEADER)) { + this.actionsApiVersion_ = this.request_.get(ACTIONS_CONVERSATION_API_VERSION_HEADER); + debug('Actions API version from header: ' + this.actionsApiVersion_); + } + if (this.body_.originalRequest && + this.body_.originalRequest.version) { + this.actionsApiVersion_ = this.body_.originalRequest.version; + debug('Actions API version from Dialogflow: ' + this.actionsApiVersion_); + } + + /** + * Intent handling data structure. + * @private + * @type {Object} + */ + this.handler_ = null; + + /** + * Intent mapping data structure. + * @private + * @type {Object} + */ + this.intentMap_ = null; + + /** + * Intent state data structure. + * @private + * @type {Object} + */ + this.stateMap_ = null; + + /** + * The session state. + * @public + * @type {string} + */ + this.state = null; + + /** + * The session data in JSON format. + * @public + * @type {Object} + */ + this.data = {}; + + /** + * The Dialogflow context. + * @private + * @type {Object} + */ + this.contexts_ = {}; + + /** + * The last error message. + * @private + * @type {string} + */ + this.lastErrorMessage_ = null; + + /** + * Track if an HTTP response has been sent already. + * @private + * @type {boolean} + */ + this.responded_ = false; + + /** + * List of standard intents that the app provides. + * @readonly + * @enum {string} + * @actionssdk + * @dialogflow + */ + this.StandardIntents = { + /** App fires MAIN intent for queries like [talk to $app]. */ + MAIN: this.isNotApiVersionOne_() ? 'actions.intent.MAIN' : 'assistant.intent.action.MAIN', + /** App fires TEXT intent when action issues ask intent. */ + TEXT: this.isNotApiVersionOne_() ? 'actions.intent.TEXT' : 'assistant.intent.action.TEXT', + /** App fires PERMISSION intent when action invokes askForPermission. */ + PERMISSION: this.isNotApiVersionOne_() ? 'actions.intent.PERMISSION' : 'assistant.intent.action.PERMISSION', + /** App fires OPTION intent when user chooses from options provided. */ + OPTION: 'actions.intent.OPTION', + /** App fires TRANSACTION_REQUIREMENTS_CHECK intent when action sets up transaction. */ + TRANSACTION_REQUIREMENTS_CHECK: 'actions.intent.TRANSACTION_REQUIREMENTS_CHECK', + /** App fires DELIVERY_ADDRESS intent when action asks for delivery address. */ + DELIVERY_ADDRESS: 'actions.intent.DELIVERY_ADDRESS', + /** App fires TRANSACTION_DECISION intent when action asks for transaction decision. */ + TRANSACTION_DECISION: 'actions.intent.TRANSACTION_DECISION', + /** App fires CONFIRMATION intent when requesting affirmation from user. */ + CONFIRMATION: 'actions.intent.CONFIRMATION', + /** App fires DATETIME intent when requesting date/time from user. */ + DATETIME: 'actions.intent.DATETIME', + /** App fires SIGN_IN intent when requesting sign-in from user. */ + SIGN_IN: 'actions.intent.SIGN_IN', + /** App fires NO_INPUT intent when user doesn't provide input. */ + NO_INPUT: 'actions.intent.NO_INPUT', + /** App fires CANCEL intent when user exits app mid-dialog. */ + CANCEL: 'actions.intent.CANCEL', + /** App fires NEW_SURFACE intent when requesting handoff to a new surface from user. */ + NEW_SURFACE: 'actions.intent.NEW_SURFACE' + }; + + /** + * List of supported permissions the app supports. + * @readonly + * @enum {string} + * @actionssdk + * @dialogflow + */ + this.SupportedPermissions = { + /** + * The user's name as defined in the + * {@link https://developers.google.com/actions/reference/conversation#UserProfile|UserProfile object} + */ + NAME: 'NAME', + /** + * The location of the user's current device, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Location|Location object}. + */ + DEVICE_PRECISE_LOCATION: 'DEVICE_PRECISE_LOCATION', + /** + * City and zipcode corresponding to the location of the user's current device, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Location|Location object}. + */ + DEVICE_COARSE_LOCATION: 'DEVICE_COARSE_LOCATION' + }; + + /** + * List of built-in argument names. + * @readonly + * @enum {string} + * @actionssdk + * @dialogflow + */ + this.BuiltInArgNames = { + /** Permission granted argument. */ + PERMISSION_GRANTED: this.isNotApiVersionOne_() ? 'PERMISSION' : 'permission_granted', + /** Option selected argument. */ + OPTION: 'OPTION', + /** Transaction requirements check result argument. */ + TRANSACTION_REQ_CHECK_RESULT: 'TRANSACTION_REQUIREMENTS_CHECK_RESULT', + /** Delivery address value argument. */ + DELIVERY_ADDRESS_VALUE: 'DELIVERY_ADDRESS_VALUE', + /** Transactions decision argument. */ + TRANSACTION_DECISION_VALUE: 'TRANSACTION_DECISION_VALUE', + /** Confirmation argument. */ + CONFIRMATION: 'CONFIRMATION', + /** DateTime argument. */ + DATETIME: 'DATETIME', + /** Sign in status argument. */ + SIGN_IN: 'SIGN_IN', + /** Reprompt count for consecutive NO_INPUT intents. */ + REPROMPT_COUNT: 'REPROMPT_COUNT', + /** Flag representing finality of NO_INPUT intent. */ + IS_FINAL_REPROMPT: 'IS_FINAL_REPROMPT', + /** New surface value argument. */ + NEW_SURFACE: 'NEW_SURFACE' + }; + + /** + * The property name used when specifying an input value data spec. + * @readonly + * @type {string} + * @actionssdk + * @dialogflow + */ + this.ANY_TYPE_PROPERTY_ = '@type'; + + /** + * List of built-in value type names. + * @private + * @readonly + * @enum {string} + * @actionssdk + * @dialogflow + */ + this.InputValueDataTypes_ = { + /** Permission Value Spec. */ + PERMISSION: 'type.googleapis.com/google.actions.v2.PermissionValueSpec', + /** Option Value Spec. */ + OPTION: 'type.googleapis.com/google.actions.v2.OptionValueSpec', + /** Transaction Requirements Check Value Spec. */ + TRANSACTION_REQ_CHECK: 'type.googleapis.com/google.actions.v2.TransactionRequirementsCheckSpec', + /** Delivery Address Value Spec. */ + DELIVERY_ADDRESS: 'type.googleapis.com/google.actions.v2.DeliveryAddressValueSpec', + /** Transaction Decision Value Spec. */ + TRANSACTION_DECISION: 'type.googleapis.com/google.actions.v2.TransactionDecisionValueSpec', + /** Confirmation Value Spec. */ + CONFIRMATION: 'type.googleapis.com/google.actions.v2.ConfirmationValueSpec', + /** DateTime Value Spec. */ + DATETIME: 'type.googleapis.com/google.actions.v2.DateTimeValueSpec', + /** New Surface Value Spec. */ + NEW_SURFACE: 'type.googleapis.com/google.actions.v2.NewSurfaceValueSpec' + }; + + /** + * List of possible conversation stages, as defined in the + * {@link https://developers.google.com/actions/reference/conversation#Conversation|Conversation object}. + * @readonly + * @enum {number} + * @actionssdk + * @dialogflow + */ + this.ConversationStages = { + /** + * Unspecified conversation state. + */ + UNSPECIFIED: this.isNotApiVersionOne_() ? 'UNSPECIFIED' : 0, + /** + * A new conversation. + */ + NEW: this.isNotApiVersionOne_() ? 'NEW' : 1, + /** + * An active (ongoing) conversation. + */ + ACTIVE: this.isNotApiVersionOne_() ? 'ACTIVE' : 2 + }; + + /** + * List of surface capabilities supported by the app. + * @readonly + * @enum {string} + * @actionssdk + * @dialogflow + */ + this.SurfaceCapabilities = { + /** + * The ability to output audio. + */ + AUDIO_OUTPUT: 'actions.capability.AUDIO_OUTPUT', + /** + * The ability to output on a screen + */ + SCREEN_OUTPUT: 'actions.capability.SCREEN_OUTPUT' + }; + + /** + * List of possible user input types. + * @readonly + * @enum {number} + * @actionssdk + * @dialogflow + */ + this.InputTypes = { + /** + * Unspecified. + */ + UNSPECIFIED: this.isNotApiVersionOne_() ? 'UNSPECIFIED' : 0, + /** + * Input given by touch. + */ + TOUCH: this.isNotApiVersionOne_() ? 'TOUCH' : 1, + /** + * Input given by voice (spoken). + */ + VOICE: this.isNotApiVersionOne_() ? 'VOICE' : 2, + /** + * Input given by keyboard (typed). + */ + KEYBOARD: this.isNotApiVersionOne_() ? 'KEYBOARD' : 3 + }; + + /** + * List of possible sign in result status values. + * @readonly + * @enum {string} + * @actionssdk + * @dialogflow + */ + this.SignInStatus = { + // Unknown status. + UNSPECIFIED: 'SIGN_IN_STATUS_UNSPECIFIED', + // User successfully completed the account linking. + OK: 'OK', + // Cancelled or dismissed account linking. + CANCELLED: 'CANCELLED', + // System or network error. + ERROR: 'ERROR' + }; + + /** + * API version describes version of the Assistant request. + * @deprecated + * @private + * @type {string} + */ + this.apiVersion_ = null; + // Populates API version. + if (this.request_.get(CONVERSATION_API_VERSION_HEADER)) { + this.apiVersion_ = this.request_.get(CONVERSATION_API_VERSION_HEADER); + debug('Assistant API version: ' + this.apiVersion_); + } + + /** + * Values related to supporting {@link Transactions}. + * @readonly + * @type {object} + */ + this.Transactions = TransactionValues; + + this.requestData = requestData; + + // Extracts the data from the request + this.extractData_(); + } + + // --------------------------------------------------------------------------- + // Public APIs + // --------------------------------------------------------------------------- + + /** + * Handles the incoming Assistant request using a handler or Map of handlers. + * Each handler can be a function callback or Promise. + * + * @example + * // Actions SDK + * const app = new ActionsSdkApp({request: request, response: response}); + * + * function mainIntent (app) { + * const inputPrompt = app.buildInputPrompt(true, 'Hi! ' + + * 'I can read out an ordinal like ' + + * '123. Say a number.', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * + * function rawInput (app) { + * if (app.getRawInput() === 'bye') { + * app.tell('Goodbye!'); + * } else { + * const inputPrompt = app.buildInputPrompt(true, 'You said, ' + + * app.getRawInput() + '', + * ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']); + * app.ask(inputPrompt); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, mainIntent); + * actionMap.set(app.StandardIntents.TEXT, rawInput); + * + * app.handleRequest(actionMap); + * + * // Dialogflow + * const app = new DialogflowApp({request: req, response: res}); + * const NAME_ACTION = 'make_name'; + * const COLOR_ARGUMENT = 'color'; + * const NUMBER_ARGUMENT = 'number'; + * + * function makeName (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * const color = app.getArgument(COLOR_ARGUMENT); + * app.tell('Alright, your silly name is ' + + * color + ' ' + number + + * '! I hope you like it. See you next time.'); + * } + * + * const actionMap = new Map(); + * actionMap.set(NAME_ACTION, makeName); + * app.handleRequest(actionMap); + * + * @param {(Function|Map)} handler The handler (or Map of handlers) for the request. + * @return {undefined} + * @actionssdk + * @dialogflow + */ + handleRequest (handler) { + debug('handleRequest: handler=%s', handler); + if (!handler) { + this.handleError_('request handler can NOT be empty.'); + return; + } + if (typeof handler === 'function') { + debug('handleRequest: function'); + // simple function handler + this.handler_ = handler; + const promise = handler(this); + if (promise instanceof Promise) { + promise.then( + (result) => { + debug(result); + }) + .catch( + (reason) => { + this.handleError_('function failed: %s', reason.message); + this.tell(!reason.message ? ERROR_MESSAGE : reason.message); + }); + } else { + // Handle functions + return; + } + return; + } else if (handler instanceof Map) { + debug('handleRequest: map'); + const intent = this.getIntent(); + const result = this.invokeIntentHandler_(handler, intent); + if (!result) { + this.tell(!this.lastErrorMessage_ ? ERROR_MESSAGE : this.lastErrorMessage_); + } + return; + } + // Could not handle intent + this.handleError_('invalid intent handler type: ' + (typeof handler)); + this.tell(ERROR_MESSAGE); + } + + /** + * Equivalent to {@link AssistantApp#askForPermission|askForPermission}, + * but allows you to prompt the user for more than one permission at once. + * + * Notes: + * + * * The order in which you specify the permission prompts does not matter - + * it is controlled by the Assistant to provide a consistent user experience. + * * The user will be able to either accept all permissions at once, or none. + * If you wish to allow them to selectively accept one or other, make several + * dialog turns asking for each permission independently with askForPermission. + * * Asking for DEVICE_COARSE_LOCATION and DEVICE_PRECISE_LOCATION at once is + * equivalent to just asking for DEVICE_PRECISE_LOCATION + * + * @example + * const app = new DialogflowApp({request: req, response: res}); + * const REQUEST_PERMISSION_ACTION = 'request_permission'; + * const GET_RIDE_ACTION = 'get_ride'; + * + * function requestPermission (app) { + * const permission = [ + * app.SupportedPermissions.NAME, + * app.SupportedPermissions.DEVICE_PRECISE_LOCATION + * ]; + * app.askForPermissions('To pick you up', permissions); + * } + * + * function sendRide (app) { + * if (app.isPermissionGranted()) { + * const displayName = app.getUserName().displayName; + * const address = app.getDeviceLocation().address; + * app.tell('I will tell your driver to pick up ' + displayName + + * ' at ' + address); + * } else { + * // Response shows that user did not grant permission + * app.tell('Sorry, I could not figure out where to pick you up.'); + * } + * } + * const actionMap = new Map(); + * actionMap.set(REQUEST_PERMISSION_ACTION, requestPermission); + * actionMap.set(GET_RIDE_ACTION, sendRide); + * app.handleRequest(actionMap); + * + * @param {string} context Context why the permission is being asked; it's the TTS + * prompt prefix (action phrase) we ask the user. + * @param {Array} permissions Array of permissions App supports, each of + * which comes from AssistantApp.SupportedPermissions. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return {(Object|null)} A response is sent to Assistant to ask for the user's permission; for any + * invalid input, we return null. + * @actionssdk + * @dialogflow + */ + askForPermissions (context, permissions, dialogState) { + debug('askForPermissions: context=%s, permissions=%s, dialogState=%s', + context, permissions, JSON.stringify(dialogState)); + if (!context || context === '') { + this.handleError_('Assistant context can NOT be empty.'); + return null; + } + if (!permissions || permissions.length === 0) { + this.handleError_('At least one permission needed.'); + return null; + } + for (let i = 0; i < permissions.length; i++) { + const permission = permissions[i]; + if (permission !== this.SupportedPermissions.NAME && + permission !== this.SupportedPermissions.DEVICE_PRECISE_LOCATION && + permission !== this.SupportedPermissions.DEVICE_COARSE_LOCATION) { + this.handleError_('Assistant permission must be one of ' + + '[NAME, DEVICE_PRECISE_LOCATION, DEVICE_COARSE_LOCATION]'); + return null; + } + } + if (!dialogState) { + dialogState = { + 'state': (this.state instanceof State ? this.state.getName() : this.state), + 'data': this.data + }; + } + return this.fulfillPermissionsRequest_({ + optContext: context, + permissions: permissions + }, dialogState); + } + + /** + * Checks whether user is in transactable state. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const TXN_REQ_COMPLETE = 'txn.req.complete'; + * + * let transactionConfig = { + * deliveryAddressRequired: false, + * type: app.Transactions.PaymentType.BANK, + * displayName: 'Checking-1234' + * }; + * function welcomeIntent (app) { + * app.askForTransactionRequirements(transactionConfig); + * } + * + * function txnReqCheck (app) { + * if (app.getTransactionRequirementsResult() === app.Transactions.ResultType.OK) { + * // continue cart building flow + * } else { + * // don't continue cart building + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(TXN_REQ_COMPLETE, txnReqCheck); + * app.handleRequest(actionMap); + * + * @param {ActionPaymentTransactionConfig|GooglePaymentTransactionConfig=} + * transactionConfig Configuration for the transaction. Includes payment + * options and order options. Optional if order has no payment or + * delivery. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return {(Object|null)} HTTP response. + * @actionssdk + * @dialogflow + */ + askForTransactionRequirements (transactionConfig, dialogState) { + debug('checkForTransactionRequirements: transactionConfig=%s,' + + ' dialogState=%s', + JSON.stringify(transactionConfig), JSON.stringify(dialogState)); + if (transactionConfig && transactionConfig.type && + transactionConfig.cardNetworks) { + this.handleError_('Invalid transaction configuration. Must be of type' + + 'ActionPaymentTransactionConfig or GooglePaymentTransactionConfig'); + return null; + } + const transactionRequirementsCheckSpec = {}; + if (transactionConfig && transactionConfig.deliveryAddressRequired) { + transactionRequirementsCheckSpec.orderOptions = { + requestDeliveryAddress: transactionConfig.deliveryAddressRequired + }; + } + if (transactionConfig && (transactionConfig.type || + transactionConfig.cardNetworks)) { + transactionRequirementsCheckSpec.paymentOptions = + this.buildPaymentOptions_(transactionConfig); + } + return this.fulfillSystemIntent_(this.StandardIntents.TRANSACTION_REQUIREMENTS_CHECK, + this.InputValueDataTypes_.TRANSACTION_REQ_CHECK, transactionRequirementsCheckSpec, + 'PLACEHOLDER_FOR_TXN_REQUIREMENTS', dialogState); + } + + /** + * Asks user to confirm transaction information. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const TXN_COMPLETE = 'txn.complete'; + * + * let transactionConfig = { + * deliveryAddressRequired: false, + * type: app.Transactions.PaymentType.BANK, + * displayName: 'Checking-1234' + * }; + * + * let order = app.buildOrder(); + * // fill order cart + * + * function welcomeIntent (app) { + * app.askForTransaction(order, transactionConfig); + * } + * + * function txnComplete (app) { + * // respond with order update + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(TXN_COMPLETE, txnComplete); + * app.handleRequest(actionMap); + * + * @param {Object} order Order built with buildOrder(). + * @param {ActionPaymentTransactionConfig|GooglePaymentTransactionConfig} + * transactionConfig Configuration for the transaction. Includes payment + * options and order options. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return {(Object|null)} HTTP response + * @dialogflow + */ + askForTransactionDecision (order, transactionConfig, dialogState) { + debug('askForTransactionDecision: order=%s, transactionConfig=%s,' + + ' dialogState=%s', JSON.stringify(order), + JSON.stringify(transactionConfig), JSON.stringify(dialogState)); + if (!order) { + this.handleError_('Invalid order'); + return null; + } + if (transactionConfig && transactionConfig.type && + transactionConfig.cardNetworks) { + this.handleError_('Invalid transaction configuration. Must be of type' + + 'ActionPaymentTransactionConfig or GooglePaymentTransactionConfig'); + return null; + } + const transactionDecisionValueSpec = { + proposedOrder: order + }; + if (transactionConfig && transactionConfig.deliveryAddressRequired) { + transactionDecisionValueSpec.orderOptions = { + requestDeliveryAddress: transactionConfig.deliveryAddressRequired + }; + } + if (transactionConfig && (transactionConfig.type || + transactionConfig.cardNetworks)) { + transactionDecisionValueSpec.paymentOptions = + this.buildPaymentOptions_(transactionConfig); + } + if (transactionConfig && transactionConfig.customerInfoOptions) { + if (!transactionDecisionValueSpec.orderOptions) { + transactionDecisionValueSpec.orderOptions = {}; + } + transactionDecisionValueSpec.orderOptions.customerInfoOptions = + transactionConfig.customerInfoOptions; + } + return this.fulfillSystemIntent_(this.StandardIntents.TRANSACTION_DECISION, + this.InputValueDataTypes_.TRANSACTION_DECISION, transactionDecisionValueSpec, + 'PLACEHOLDER_FOR_TXN_DECISION', dialogState); + } + + /** + * Asks the Assistant to guide the user to grant a permission. For example, + * if you want your app to get access to the user's name, you would invoke + * the askForPermission method with a context containing the reason for the request, + * and the AssistantApp.SupportedPermissions.NAME permission. With this, the Assistant will ask + * the user, in your agent's voice, the following: '[Context with reason for the request], + * I'll just need to get your name from Google, is that OK?'. + * + * Once the user accepts or denies the request, the Assistant will fire another intent: + * assistant.intent.action.PERMISSION with a boolean argument: AssistantApp.BuiltInArgNames.PERMISSION_GRANTED + * and, if granted, the information that you requested. + * + * Read more: + * + * * {@link https://developers.google.com/actions/reference/conversation#ExpectedIntent|Supported Permissions} + * * Check if the permission has been granted with {@link AssistantApp#isPermissionGranted|isPermissionsGranted} + * * {@link AssistantApp#getDeviceLocation|getDeviceLocation} + * * {@link AssistantApp#getUserName|getUserName} + * + * @example + * const app = new DialogflowApp({request: req, response: res}); + * const REQUEST_PERMISSION_ACTION = 'request_permission'; + * const GET_RIDE_ACTION = 'get_ride'; + * + * function requestPermission (app) { + * const permission = app.SupportedPermissions.NAME; + * app.askForPermission('To pick you up', permission); + * } + * + * function sendRide (app) { + * if (app.isPermissionGranted()) { + * const displayName = app.getUserName().displayName; + * app.tell('I will tell your driver to pick up ' + displayName); + * } else { + * // Response shows that user did not grant permission + * app.tell('Sorry, I could not figure out who to pick up.'); + * } + * } + * const actionMap = new Map(); + * actionMap.set(REQUEST_PERMISSION_ACTION, requestPermission); + * actionMap.set(GET_RIDE_ACTION, sendRide); + * app.handleRequest(actionMap); + * + * @param {string} context Context why permission is asked; it's the TTS + * prompt prefix (action phrase) we ask the user. + * @param {string} permission One of the permissions Assistant supports, each of + * which comes from AssistantApp.SupportedPermissions. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {(Object|null)} A response is sent to the Assistant to ask for the user's permission; + * for any invalid input, we return null. + * @actionssdk + * @dialogflow + */ + askForPermission (context, permission, dialogState) { + debug('askForPermission: context=%s, permission=%s, dialogState=%s', + context, permission, JSON.stringify(dialogState)); + return this.askForPermissions(context, [permission], dialogState); + } + + /** + * Returns true if the request follows a previous request asking for + * permission from the user and the user granted the permission(s). Otherwise, + * false. Use with {@link AssistantApp#askForPermissions|askForPermissions}. + * + * @example + * const app = new ActionsSdkApp({request: request, response: response}); + * // or + * const app = new DialogflowApp({request: request, response: response}); + * app.askForPermissions("To get you a ride", [ + * app.SupportedPermissions.NAME, + * app.SupportedPermissions.DEVICE_PRECISE_LOCATION + * ]); + * // ... + * // In response handler for subsequent intent: + * if (app.isPermissionGranted()) { + * // Use the requested permission(s) to get the user a ride + * } + * + * @return {boolean} true if permissions granted. + * @dialogflow + * @actionssdk + */ + isPermissionGranted () { + debug('isPermissionGranted'); + return this.getArgumentCommon(this.BuiltInArgNames.PERMISSION_GRANTED) === 'true'; + } + + /** + * Asks user for delivery address. + * + * @example + * // For DialogflowApp: + * const app = new DialogflowApp({request, response}); + * const WELCOME_INTENT = 'input.welcome'; + * const DELIVERY_INTENT = 'delivery.address'; + * + * function welcomeIntent (app) { + * app.askForDeliveryAddress('To make sure I can deliver to you'); + * } + * + * function addressIntent (app) { + * const postalCode = app.getDeliveryAddress().postalAddress.postalCode; + * if (isInDeliveryZone(postalCode)) { + * app.tell('Great looks like you\'re in our delivery area!'); + * } else { + * app.tell('I\'m sorry it looks like we can\'t deliver to you.'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(DELIVERY_INTENT, addressIntent); + * app.handleRequest(actionMap); + * + * // For ActionsSdkApp: + * const app = new ActionsSdkApp({request, response}); + * const WELCOME_INTENT = app.StandardIntents.MAIN; + * const DELIVERY_INTENT = app.StandardIntents.DELIVERY_ADDRESS; + * + * function welcomeIntent (app) { + * app.askForDeliveryAddress('To make sure I can deliver to you'); + * } + * + * function addressIntent (app) { + * const postalCode = app.getDeliveryAddress().postalAddress.postalCode; + * if (isInDeliveryZone(postalCode)) { + * app.tell('Great looks like you\'re in our delivery area!'); + * } else { + * app.tell('I\'m sorry it looks like we can\'t deliver to you.'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(DELIVERY_INTENT, addressIntent); + * app.handleRequest(actionMap); + * + * @param {string} reason Reason given to user for asking delivery address. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {Object} HTTP response. + * @actionssdk + * @dialogflow + */ + askForDeliveryAddress (reason, dialogState) { + debug('askForDeliveryAddress: reason=%s, dialogState=%s', reason, dialogState); + if (!reason) { + this.handleError_('reason cannot be empty'); + return null; + } + const deliveryValueSpec = { + addressOptions: { + reason: reason + } + }; + return this.fulfillSystemIntent_(this.StandardIntents.DELIVERY_ADDRESS, + this.InputValueDataTypes_.DELIVERY_ADDRESS, deliveryValueSpec, + 'PLACEHOLDER_FOR_DELIVERY_ADDRESS', dialogState); + } + + /** + * Asks user for a confirmation. + * + * @example + * const app = new DialogflowApp({ request, response }); + * const WELCOME_INTENT = 'input.welcome'; + * const CONFIRMATION = 'confirmation'; + * + * function welcomeIntent (app) { + * app.askForConfirmation('Are you sure you want to do that?'); + * } + * + * function confirmation (app) { + * if (app.getUserConfirmation()) { + * app.tell('Great! I\'m glad you want to do it!'); + * } else { + * app.tell('That\'s okay. Let\'s not do it now.'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(CONFIRMATION, confirmation); + * app.handleRequest(actionMap); + * + * @param {string=} prompt The confirmation prompt presented to the user to + * query for an affirmative or negative response. If undefined or null, + * Google will use a generic yes/no prompt. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return {(Object|null)} HTTP response. + * @actionssdk + * @dialogflow + */ + askForConfirmation (prompt, dialogState) { + debug('askForConfirmation: prompt=%s, dialogState=%s', prompt, + JSON.stringify(dialogState)); + let confirmationValueSpec = {}; + if (prompt) { + confirmationValueSpec.dialogSpec = { + requestConfirmationText: prompt + }; + } + return this.fulfillSystemIntent_(this.StandardIntents.CONFIRMATION, + this.InputValueDataTypes_.CONFIRMATION, confirmationValueSpec, + 'PLACEHOLDER_FOR_CONFIRMATION', dialogState); + } + + /** + * Asks user for a timezone-agnostic date and time. + * + * @example + * const app = new DialogflowApp({ request, response }); + * const WELCOME_INTENT = 'input.welcome'; + * const DATETIME = 'datetime'; + * + * function welcomeIntent (app) { + * app.askForDateTime('When do you want to come in?', + * 'Which date works best for you?', + * 'What time of day works best for you?'); + * } + * + * function datetime (app) { + * app.tell({speech: 'Great see you at your appointment!', + * displayText: 'Great, we will see you on ' + * + app.getDateTime().date.month + * + '/' + app.getDateTime().date.day + * + ' at ' + app.getDateTime().time.hours + * + (app.getDateTime().time.minutes || '')}); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(DATETIME, datetime); + * app.handleRequest(actionMap); + * + * @param {string=} initialPrompt The initial prompt used to ask for a + * date and time. If undefined or null, Google will use a generic + * prompt. + * @param {string=} datePrompt The prompt used to specifically ask for the + * date if not provided by user. If undefined or null, Google will use a + * generic prompt. + * @param {string=} timePrompt The prompt used to specifically ask for the + * time if not provided by user. If undefined or null, Google will use a + * generic prompt. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return {(Object|null)} HTTP response. + * @actionssdk + * @dialogflow + */ + askForDateTime (initialPrompt, datePrompt, timePrompt, dialogState) { + debug('askForDateTime: initialPrompt=%s, datePrompt=%s, ' + + 'timePrompt=%s, dialogState=%s', initialPrompt, datePrompt, timePrompt, + JSON.stringify(dialogState)); + let dateTimeValueSpec = {}; + if (initialPrompt || datePrompt || timePrompt) { + dateTimeValueSpec.dialogSpec = { + requestDatetimeText: initialPrompt || undefined, + requestDateText: datePrompt || undefined, + requestTimeText: timePrompt || undefined + }; + } + return this.fulfillSystemIntent_(this.StandardIntents.DATETIME, + this.InputValueDataTypes_.DATETIME, dateTimeValueSpec, + 'PLACEHOLDER_FOR_DATETIME', dialogState); + } + + /** + * Hands the user off to a web sign in flow. App sign in and OAuth credentials + * are set in the {@link https://console.actions.google.com|Actions Console}. + * Retrieve the access token in subsequent intents using + * app.getUser().accessToken. + * + * Note: Currently this API requires enabling the app for Transactions APIs. + * To do this, fill out the App Info section of the Actions Console project + * and check the box indicating the use of Transactions under "Privacy and + * consent". + * + * @example + * const app = new DialogflowApp({ request, response }); + * const WELCOME_INTENT = 'input.welcome'; + * const SIGN_IN = 'sign.in'; + * + * function welcomeIntent (app) { + * app.askForSignIn(); + * } + * + * function signIn (app) { + * if (app.getSignInStatus() === app.SignInstatus.OK) { + * let accessToken = app.getUser().accessToken; + * app.ask('Great, thanks for signing in!'); + * } else { + * app.ask('I won\'t be able to save your data, but let\'s continue!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(SIGN_IN, signIn); + * app.handleRequest(actionMap); + * + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return {(Object|null)} HTTP response. + * @actionssdk + * @dialogflow + */ + askForSignIn (dialogState) { + debug('askForSignIn: dialogState=%s', JSON.stringify(dialogState)); + return this.fulfillSystemIntent_(this.StandardIntents.SIGN_IN, + this.InputValueDataTypes_.SIGN_IN, null, + 'PLACEHOLDER_FOR_SIGN_IN', dialogState); + } + + /** + * Requests the user to switch to another surface during the conversation. + * + * @example + * const app = new DialogflowApp({ request, response }); + * const WELCOME_INTENT = 'input.welcome'; + * const SHOW_IMAGE = 'show.image'; + * + * function welcomeIntent (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * showPicture(app); + * } else if (app.hasAvailableSurfaceCapabilities(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askForNewSurface('To show you an image', + * 'Check out this image', + * [app.SurfaceCapabilities.SCREEN_OUTPUT] + * ); + * } else { + * app.tell('This part of the app only works on screen devices. Sorry about that'); + * } + * } + * + * function showImage (app) { + * if (!app.isNewSurface()) { + * app.tell('Ok, I understand. You don't want to see pictures. Bye'); + * } else { + * showPicture(app, pictureType); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(SHOW_IMAGE, showImage); + * app.handleRequest(actionMap); + * + * @param {string} context Context why new surface is requested; it's the TTS + * prompt prefix (action phrase) we ask the user. + * @param {string} notificationTitle Title of the notification appearing on + * new surface device. + * @param {Array} capabilities The list of capabilities required in + * the surface. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. Used in {@link ActionsSdkAssistant}. + * @return {(Object|null)} HTTP response. + * @dialogflow + * @actionssdk + */ + askForNewSurface (context, notificationTitle, capabilities, dialogState) { + debug('askForNewSurface: context=%s, notificationTitle=%s, ' + + 'capabilities=%s, dialogState=%s', context, notificationTitle, + JSON.stringify(capabilities), dialogState); + let newSurfaceValueSpec = { context, notificationTitle, capabilities }; + return this.fulfillSystemIntent_(this.StandardIntents.NEW_SURFACE, + this.InputValueDataTypes_.NEW_SURFACE, newSurfaceValueSpec, + 'PLACEHOLDER_FOR_NEW_SURFACE', dialogState); + } + + /** + * User provided date/time info. + * @typedef {Object} DateTime + * @property {Object} date + * @property {number} date.year + * @property {number} date.month + * @property {number} date.day + * @property {Object} time + * @property {number} time.hours + * @property {number} time.minutes + * @property {number} time.seconds + * @property {number} time.nanos + */ + /** + * User's permissioned name info. + * @typedef {Object} UserName + * @property {string} displayName - User's display name. + * @property {string} givenName - User's given name. + * @property {string} familyName - User's family name. + */ + + /** + * User's permissioned device location. + * @typedef {Object} DeviceLocation + * @property {Object} coordinates - {latitude, longitude}. Requested with + * SupportedPermissions.DEVICE_PRECISE_LOCATION. + * @property {string} address - Full, formatted street address. Requested with + * SupportedPermissions.DEVICE_PRECISE_LOCATION. + * @property {string} zipCode - Zip code. Requested with + * SupportedPermissions.DEVICE_COARSE_LOCATION. + * @property {string} city - Device city. Requested with + * SupportedPermissions.DEVICE_COARSE_LOCATION. + */ + + /** + * User object. + * @typedef {Object} User + * @property {string} userId - Random string ID for Google user. + * @property {UserName} userName - User name information. Null if not + * requested with {@link AssistantApp#askForPermission|askForPermission(SupportedPermissions.NAME)}. + * @property {string} accessToken - Unique Oauth2 token. Only available with + * account linking. + */ + + /** + * Actions on Google Surface. + * @typedef {Object} Surface + * @property {Array} capabilities - Capabilities of the surface. + */ + + /** + * Surface capability. + * @typedef {Object} Capability + * @property {string} name - Name of the capability. + */ + + /** + * Gets the {@link User} object. + * The user object contains information about the user, including + * a string identifier and personal information (requires requesting permissions, + * see {@link AssistantApp#askForPermissions|askForPermissions}). + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * // or + * const app = new ActionsSdkApp({request: request, response: response}); + * const userId = app.getUser().userId; + * + * @return {User} Null if no value. + * @actionssdk + * @dialogflow + */ + getUser () { + debug('getUser'); + const data = this.requestData(); + + if (!data || !data.user) { + error('No user object'); + return null; + } + + const requestUser = data.user; + + // User object includes original API properties + const user = Object.assign({}, requestUser); + + // Backwards compatibility + user.user_id = user.userId; + user.access_token = user.accessToken; + + const profile = user.profile; + user.userName = profile ? Object.assign({}, profile) : null; + + return user; + } + + /** + * If granted permission to user's name in previous intent, returns user's + * display name, family name, and given name. If name info is unavailable, + * returns null. + * + * @example + * const app = new DialogflowApp({request: req, response: res}); + * const REQUEST_PERMISSION_ACTION = 'request_permission'; + * const SAY_NAME_ACTION = 'get_name'; + * + * function requestPermission (app) { + * const permission = app.SupportedPermissions.NAME; + * app.askForPermission('To know who you are', permission); + * } + * + * function sayName (app) { + * if (app.isPermissionGranted()) { + * app.tell('Your name is ' + app.getUserName().displayName)); + * } else { + * // Response shows that user did not grant permission + * app.tell('Sorry, I could not get your name.'); + * } + * } + * const actionMap = new Map(); + * actionMap.set(REQUEST_PERMISSION_ACTION, requestPermission); + * actionMap.set(SAY_NAME_ACTION, sayName); + * app.handleRequest(actionMap); + * @return {UserName} Null if name permission is not granted. + * @actionssdk + * @dialogflow + */ + getUserName () { + debug('getUserName'); + return this.getUser() && this.getUser().userName + ? this.getUser().userName : null; + } + + /** + * Gets the user locale. Returned string represents the regional language + * information of the user set in their Assistant settings. + * For example, 'en-US' represents US English. + * + * @example + * const app = new DialogflowApp({request, response}); + * const locale = app.getUserLocale(); + * + * @return {string} User's locale, e.g. 'en-US'. Null if no locale given. + * @actionssdk + * @dialogflow + */ + getUserLocale () { + debug('getUserLocale'); + return this.getUser() && this.getUser().locale + ? this.getUser().locale : null; + } + + /** + * If granted permission to device's location in previous intent, returns device's + * location (see {@link AssistantApp#askForPermissions|askForPermissions}). + * If device info is unavailable, returns null. + * + * @example + * const app = new DialogflowApp({request: req, response: res}); + * // or + * const app = new ActionsSdkApp({request: req, response: res}); + * app.askForPermission("To get you a ride", + * app.SupportedPermissions.DEVICE_PRECISE_LOCATION); + * // ... + * // In response handler for permissions fallback intent: + * if (app.isPermissionGranted()) { + * sendCarTo(app.getDeviceLocation().coordinates); + * } + * + * @return {DeviceLocation} Null if location permission is not granted. + * @actionssdk + * @dialogflow + */ + getDeviceLocation () { + debug('getDeviceLocation'); + const data = this.requestData(); + if (!data || !data.device || !data.device.location) { + return null; + } + const deviceLocation = Object.assign({}, data.device.location); + deviceLocation.address = deviceLocation.formattedAddress; + return deviceLocation; + } + + /** + * Gets type of input used for this request. + * + * @return {number} One of AssistantApp.InputTypes. + * Null if no input type given. + * @dialogflow + * @actionssdk + */ + getInputType () { + debug('getInputType'); + const data = this.requestData(); + if (data && data.inputs) { + for (const input of data.inputs) { + if (input.rawInputs) { + for (const rawInput of input.rawInputs) { + if (rawInput.inputType) { + return rawInput.inputType; + } + } + } + } + } + error('No input type in incoming request'); + return null; + } + + /** + * Get the argument value by name from the current intent. + * If the argument is included in originalRequest, and is not a text argument, + * the entire argument object is returned. + * + * Note: If incoming request is using an API version under 2 (e.g. 'v1'), + * the argument object will be in Proto2 format (snake_case, etc). + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * + * function welcomeIntent (app) { + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} argName Name of the argument. + * @return {Object} Argument value matching argName + * or null if no matching argument. + * @dialogflow + * @actionssdk + */ + getArgumentCommon (argName) { + debug('getArgument: argName=%s', argName); + if (!argName) { + error('Invalid argument name'); + return null; + } + const argument = this.findArgument_(argName); + if (!argument) { + debug('Failed to get argument value: %s', argName); + return null; + } else if (argument.textValue) { + return argument.textValue; + } else { + if (!this.isNotApiVersionOne_()) { + return transformToSnakeCase(argument); + } else { + return argument; + } + } + } + + /** + * Gets transactability of user. Only use after calling + * askForTransactionRequirements. Null if no result given. + * + * @return {string} One of Transactions.ResultType. + * @dialogflow + * @actionssdk + */ + getTransactionRequirementsResult () { + debug('getTransactionRequirementsResult'); + const argument = this.findArgument_(this.BuiltInArgNames.TRANSACTION_REQ_CHECK_RESULT); + if (argument && argument.extension && argument.extension.resultType) { + return argument.extension.resultType; + } + debug('Failed to get transaction requirements result'); + return null; + } + + /** + * Gets order delivery address. Only use after calling askForDeliveryAddress. + * + * @return {DeliveryAddress} Delivery address information. Null if user + * denies permission, or no address given. + * @dialogflow + * @actionssdk + */ + getDeliveryAddress () { + debug('getDeliveryAddress'); + const { + DELIVERY_ADDRESS_VALUE, + TRANSACTION_DECISION_VALUE + } = this.BuiltInArgNames; + const argument = this.findArgument_(DELIVERY_ADDRESS_VALUE, TRANSACTION_DECISION_VALUE); + if (argument && argument.extension) { + if (argument.extension.userDecision === this.Transactions.DeliveryAddressDecision.ACCEPTED) { + const { location } = argument.extension; + if (!location.postalAddress) { + debug('User accepted, but may not have configured address in app'); + return null; + } + return location; + } else { + debug('User rejected giving delivery address'); + return null; + } + } + debug('Failed to get order delivery address'); + return null; + } + + /** + * Gets transaction decision information. Only use after calling + * askForTransactionDecision. + * + * @return {TransactionDecision} Transaction decision data. Returns object with + * userDecision only if user declines. userDecision will be one of + * Transactions.ConfirmationDecision. Null if no decision given. + * @dialogflow + * @actionssdk + */ + getTransactionDecision () { + debug('getTransactionDecision'); + const argument = this.findArgument_(this.BuiltInArgNames.TRANSACTION_DECISION_VALUE); + if (argument && argument.extension) { + return argument.extension; + } + debug('Failed to get order decision information'); + return null; + } + + /** + * Gets confirmation decision. Use after askForConfirmation. + * + * @return {(boolean|null)} False if user replied with negative response. Null if no user + * confirmation decision given. + * @dialogflow + * @actionssdk + */ + getUserConfirmation () { + debug('getUserConfirmation'); + const argument = this.findArgument_(this.BuiltInArgNames.CONFIRMATION); + if (argument) { + return argument.boolValue; + } + debug('Failed to get confirmation decision information'); + return null; + } + + /** + * Gets user provided date and time. Use after askForDateTime. + * + * @return {DateTime} Date and time given by the user. Null if no user + * date and time given. + * @dialogflow + * @actionssdk + */ + getDateTime () { + debug('getDateTime'); + const argument = this.findArgument_(this.BuiltInArgNames.DATETIME); + if (argument) { + return argument.datetimeValue; + } + debug('Failed to get date/time information'); + return null; + } + + /** + * Gets status of user sign in request. + * + * @return {string} Result of user sign in request. One of + * DialogflowApp.SignInStatus or ActionsSdkApp.SignInStatus + * Null if no sign in status. + * @dialogflow + * @actionssdk + */ + getSignInStatus () { + debug('getSignInStatus'); + const argument = this.findArgument_(this.BuiltInArgNames.SIGN_IN); + if (argument && argument.extension && argument.extension.status) { + return argument.extension.status; + } + debug('Failed to get sign in status'); + return null; + } + + /** + * Returns true if user device has a given surface capability. + * + * @param {string} requestedCapability Must be one of {@link SurfaceCapabilities}. + * @return {boolean} True if user device has the given capability. + * + * @example + * const app = new DialogflowApp({request: req, response: res}); + * const DESCRIBE_SOMETHING = 'DESCRIBE_SOMETHING'; + * + * function describe (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.tell(richResponseWithBasicCard); + * } else { + * app.tell('Let me tell you about ...'); + * } + * } + * const actionMap = new Map(); + * actionMap.set(DESCRIBE_SOMETHING, describe); + * app.handleRequest(actionMap); + * + * @dialogflow + * @actionssdk + */ + hasSurfaceCapability (requestedCapability) { + debug('hasSurfaceCapability: requestedCapability=%s', requestedCapability); + const capabilities = this.getSurfaceCapabilities(); + if (!capabilities) { + error('No incoming capabilities to search ' + + 'for request capability: %s', requestedCapability); + return false; + } + return capabilities.includes(requestedCapability); + } + + /** + * Gets surface capabilities of user device. + * + * @return {Array} Supported surface capabilities, as defined in + * AssistantApp.SurfaceCapabilities. + * @dialogflow + * @actionssdk + */ + getSurfaceCapabilities () { + debug('getSurfaceCapabilities'); + const data = this.requestData(); + if (!data || !data.surface || !data.surface.capabilities) { + error('No surface capabilities in incoming request'); + return null; + } + if (data && data.surface && data.surface.capabilities) { + return data.surface.capabilities.map(capability => capability.name); + } else { + error('No surface capabilities in incoming request'); + return null; + } + } + + /** + * Returns the set of other available surfaces for the user. + * + * @return {Array} Empty if no available surfaces. + * @actionssdk + * @dialogflow + */ + getAvailableSurfaces () { + debug('getAvailableSurfaces'); + return this.requestData().availableSurfaces || []; + } + + /** + * Returns true if user has an available surface which includes all given + * capabilities. Available surfaces capabilities may exist on surfaces other + * than that used for an ongoing conversation. + * + * @param {string|Array} capabilities Must be one of + * {@link SurfaceCapabilities}. + * @return {boolean} True if user has a capability available on some surface. + * + * @dialogflow + * @actionssdk + */ + hasAvailableSurfaceCapabilities (capabilities) { + debug('hasAvailableSurfaceCapabilities: capabilities=%s', capabilities); + const capabilitiesArray = Array.isArray(capabilities) ? capabilities + : [capabilities]; + const availableSurfaces = this.requestData().availableSurfaces; + if (availableSurfaces) { + for (let surface of availableSurfaces) { + const availableCapabilities = surface.capabilities.map(capability => capability.name); + const unavailableCapabilities = capabilitiesArray + .filter(capability => !availableCapabilities.includes(capability)); + if (!unavailableCapabilities.length) { + return true; + } + } + } + return false; + } + + /** + * Returns the result of the AskForNewSurface helper. + * + * @return {boolean} True if user has triggered conversation on a new device + * following the NEW_SURFACE intent. + * @actionssdk + * @dialogflow + */ + isNewSurface () { + debug('isNewSurface'); + const argument = this.findArgument_(this.BuiltInArgNames.NEW_SURFACE); + return argument && argument.extension && argument.extension.status && + argument.extension.status === 'OK'; + } + + /** + * Returns true if the app is being tested in sandbox mode. Enable sandbox + * mode in the (Actions console)[console.actions.google.com] to test + * transactions. + * + * @return {boolean} True if app is being used in Sandbox mode. + * @dialogflow + * @actionssdk + */ + isInSandbox () { + debug('isInSandbox'); + const data = this.requestData(); + return data && data.isInSandbox; + } + + /** + * Returns the number of subsequent reprompts related to silent input from the + * user. This should be used along with the NO_INPUT intent to reprompt the + * user for input in cases where the Google Assistant could not pick up any + * speech. + * + * @example + * const app = new ActionsSdkApp({request, response}); + * + * function welcome (app) { + * app.ask('Welcome to your app!'); + * } + * + * function noInput (app) { + * if (app.getRepromptCount() === 0) { + * app.ask(`What was that?`); + * } else if (app.getRepromptCount() === 1) { + * app.ask(`Sorry I didn't catch that. Could you repeat yourself?`); + * } else if (app.isFinalReprompt()) { + * app.tell(`Okay let's try this again later.`); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, welcome); + * actionMap.set(app.StandardIntents.NO_INPUT, noInput); + * app.handleRequest(actionMap); + * + * @return {number} The current reprompt count. Null if no reprompt count + * available (e.g. not in the NO_INPUT intent). + * @dialogflow + * @actionssdk + */ + getRepromptCount () { + debug('getRepromptCount'); + const repromptCount = this.getArgumentCommon(this.BuiltInArgNames.REPROMPT_COUNT); + return repromptCount !== null ? parseInt(repromptCount, 10) : null; + } + + /** + * Returns true if it is the final reprompt related to silent input from the + * user. This should be used along with the NO_INPUT intent to give the final + * response to the user after multiple silences and should be an app.tell + * which ends the conversation. + * + * @example + * const app = new ActionsSdkApp({request, response}); + * + * function welcome (app) { + * app.ask('Welcome to your app!'); + * } + * + * function noInput (app) { + * if (app.getRepromptCount() === 0) { + * app.ask(`What was that?`); + * } else if (app.getRepromptCount() === 1) { + * app.ask(`Sorry I didn't catch that. Could you repeat yourself?`); + * } else if (app.isFinalReprompt()) { + * app.tell(`Okay let's try this again later.`); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(app.StandardIntents.MAIN, welcome); + * actionMap.set(app.StandardIntents.NO_INPUT, noInput); + * app.handleRequest(actionMap); + * + * @return {boolean} True if in a NO_INPUT intent and this is the final turn + * of dialog. + * @dialogflow + * @actionssdk + */ + isFinalReprompt () { + debug('isFinalReprompt'); + const finalReprompt = this.getArgumentCommon(this.BuiltInArgNames.IS_FINAL_REPROMPT); + return finalReprompt === '1'; + } + + // --------------------------------------------------------------------------- + // Response Builders + // --------------------------------------------------------------------------- + + /** + * Constructs RichResponse with chainable property setters. + * + * @param {RichResponse=} richResponse RichResponse to clone. + * @return {RichResponse} Constructed RichResponse. + */ + buildRichResponse (richResponse) { + return new RichResponse(richResponse); + } + + /** + * Constructs BasicCard with chainable property setters. + * + * @param {string=} bodyText Body text of the card. Can be set using setTitle + * instead. + * @return {BasicCard} Constructed BasicCard. + */ + buildBasicCard (bodyText) { + const card = new BasicCard(); + if (bodyText) { + card.setBodyText(bodyText); + } + return card; + } + + /** + * Constructs List with chainable property setters. + * + * @param {string=} title A title to set for a new List. + * @return {List} Constructed List. + */ + buildList (title) { + return new List(title); + } + + /** + * Constructs Carousel with chainable property setters. + * + * @return {Carousel} Constructed Carousel. + */ + buildCarousel () { + return new Carousel(); + } + + /** + * Constructs OptionItem with chainable property setters. + * + * @param {string=} key A unique key to identify this option. This key will + * be returned as an argument in the resulting actions.intent.OPTION + * intent. + * @param {string|Array=} synonyms A list of synonyms which the user may + * use to identify this option instead of the option key. + * @return {OptionItem} Constructed OptionItem. + */ + buildOptionItem (key, synonyms) { + let optionItem = new OptionItem(); + if (key) { + optionItem.setKey(key); + } + if (synonyms) { + optionItem.addSynonyms(synonyms); + } + return optionItem; + } + + // --------------------------------------------------------------------------- + // Transaction Builders + // --------------------------------------------------------------------------- + + /** + * Constructs Order with chainable property setters. + * + * @param {string} orderId Unique identifier for the order. + * @return {Order} Constructed Order. + */ + buildOrder (orderId) { + return new Order(orderId); + } + + /** + * Constructs Cart with chainable property setters. + * + * @param {string=} cartId Unique identifier for the cart. + * @return {Cart} Constructed Cart. + */ + buildCart (cartId) { + return new Cart(cartId); + } + + /** + * Constructs LineItem with chainable property setters. + * Because of a previous bug, the parameters are swapped compared to + * the LineItem constructor to prevent a breaking change. + * + * @param {string} name Name of the line item. + * @param {string} id Unique identifier for the item. + * @return {LineItem} Constructed LineItem. + */ + buildLineItem (name, id) { + return new LineItem(id, name); + } + + /** + * Constructs OrderUpdate with chainable property setters. + * + * @param {string} orderId Unique identifier of the order. + * @param {boolean} isGoogleOrderId True if the order ID is provided by + * Google. False if the order ID is app provided. + * @return {OrderUpdate} Constructed OrderUpdate. + */ + buildOrderUpdate (orderId, isGoogleOrderId) { + return new OrderUpdate(orderId, isGoogleOrderId); + } + + // --------------------------------------------------------------------------- + // Private Helpers + // --------------------------------------------------------------------------- + + /** + * Utility function to invoke an intent handler. + * + * @param {Object} handler The handler for the request. + * @param {string} intent The intent to handle. + * @return {boolean} true if the handler was invoked. + * @private + */ + invokeIntentHandler_ (handler, intent) { + debug('invokeIntentHandler_: handler=%s, intent=%s', handler, intent); + this.lastErrorMessage_ = null; + // map of intents or states + for (let key of handler.keys()) { + const value = handler.get(key); + let name; + if (key instanceof Intent) { + debug('key is intent'); + name = key.getName(); + } else if (key instanceof State) { + debug('key is state'); + name = key.getName(); + } else { + debug('key is id'); + // String id + name = key; + } + debug('name=' + name); + if (value instanceof Map) { + debug('state=' + (this.state instanceof State ? this.state.getName() : this.state)); + // map of states + if (!this.state && name === null) { + debug('undefined state'); + return this.invokeIntentHandler_(value, intent); + } else if (this.state instanceof State && name === this.state.getName()) { + return this.invokeIntentHandler_(value, intent); + } else if (name === this.state) { + return this.invokeIntentHandler_(value, intent); + } + } + // else map of intents + if (name === intent) { + debug('map of intents'); + const promise = value(this); + if (promise instanceof Promise) { + promise.then( + (result) => { + // No-op + }) + .catch( + (reason) => { + error(reason.message); + this.handleError_('intent handler failed: %s', reason.message); + this.lastErrorMessage_ = reason.message; + return false; + }); + } else { + // Handle functions + return true; + } + return true; + } + } + this.handleError_('no matching intent handler for: ' + intent); + return false; + } + + /** + * Find argument with requirements + * @param {Array} targets Argument to find + * @return {*} The argument + */ + findArgument_ (...targets) { + const data = this.requestData(); + if (data && data.inputs) { + for (const input of data.inputs) { + if (input.arguments) { + for (const argument of input.arguments) { + for (const target of targets) { + if (argument.name === target) { + return argument; + } + } + } + } + } + } + return null; + } + + /** + * Utility function to detect SSML markup. + * + * @param {string} text The text to be checked. + * @return {boolean} true if text is SSML markup. + * @private + */ + isSsml_ (text) { + debug('isSsml_: text=%s', text); + if (!text) { + error('Text can NOT be empty'); + return false; + } + return isSsml(text); + } + + /** + * Utility function to detect incoming request format. + * + * @return {boolean} true if request is not Action API Version 1. + * @private + */ + isNotApiVersionOne_ () { + debug('isNotApiVersionOne_'); + return this.actionsApiVersion_ !== null && + parseInt(this.actionsApiVersion_, 10) >= ACTIONS_CONVERSATION_API_VERSION_TWO; + } + + /** + * Utility function to handle error messages. + * + * @param {string} text The error message. + * @return {undefined} + * @private + */ + handleError_ (text) { + debug('handleError_: text=%s', text); + if (!text) { + error('Missing text'); + return; + } + // Log error + error.apply(text, Array.prototype.slice.call(arguments, 1)); + // Tell app to say error + if (this.responded_) { + return; + } + if (this.response_) { + // Don't call other methods; just do directly + this.response_.status(RESPONSE_CODE_BAD_REQUEST).send(API_ERROR_MESSAGE_PREFIX + text); + this.responded_ = true; + } + } + + /** + * Utility method to send an HTTP response. + * + * @param {string} response The JSON response. + * @param {string} responseCode The HTTP response code. + * @return {(Object|null)} HTTP response. + * @private + */ + doResponse_ (response, responseCode) { + debug('doResponse_: response=%s, responseCode=%d', JSON.stringify(response), responseCode); + if (this.responded_) { + return; + } + if (!response) { + this.handleError_('Response can NOT be empty.'); + return null; + } + let code = RESPONSE_CODE_OK; + if (responseCode) { + code = responseCode; + } + if (this.apiVersion_ !== null) { + this.response_.append(CONVERSATION_API_VERSION_HEADER, this.apiVersion_); + } + this.response_.append(HTTP_CONTENT_TYPE_HEADER, HTTP_CONTENT_TYPE_JSON); + // If request was in Proto2 format, convert response to Proto2 + if (!this.isNotApiVersionOne_()) { + if (response.data) { + response.data = transformToSnakeCase(response.data); + } else { + response = transformToSnakeCase(response); + } + } + debug('Response %s', JSON.stringify(response)); + const httpResponse = this.response_.status(code).send(response); + this.responded_ = true; + return httpResponse; + } + + /** + * Extract session data from the incoming JSON request. + * + * Used in subclasses for Actions SDK and Dialogflow. + * @return {undefined} + * @private + */ + extractData_ () { + debug('extractData_'); + this.data = {}; + } + + /** + * Uses a PermissionsValueSpec object to construct and send a + * permissions request to user. + * + * Used in subclasses for Actions SDK and Dialogflow. + * @return {Object} HTTP response. + * @private + */ + fulfillPermissionsRequest_ () { + debug('fulfillPermissionsRequest_'); + return {}; + } + + /** + * Uses a ConfirmationValueSpec object to construct and send a + * confirmation request to user. + * + * Used in subclasses for Actions SDK and Dialogflow. + * @return {Object} HTTP response. + * @private + */ + fulfillConfirmationRequest_ () { + debug('fulfillConfirmationRequest_'); + return {}; + } + + /** + * Uses a DateTimeValueSpec object to construct and send a + * date time request to user. + * + * Used in subclasses for Actions SDK and Dialogflow. + * @return {Object} HTTP response. + * @private + */ + fulfillDateTimeRequest_ () { + debug('fulfillDateTimeRequest_'); + return {}; + } + + /** + * Construct and send a sign in request to user. + * + * Used in subclasses for Actions SDK and Dialogflow. + * @return {Object} HTTP response. + * @private + */ + fulfillSignInRequest_ () { + debug('fulfillSignInRequest_'); + return {}; + } + + /** + * Uses a TransactionRequirementsCheckValueSpec object to construct and send a + * transaction requirements request to user. + * + * Used in subclasses for Actions SDK and Dialogflow. + * @return {Object} HTTP response. + * @private + */ + fulfillTransactionRequirementsCheck_ () { + debug('fulfillTransactionRequirementsCheck_'); + return {}; + } + + /** + * Uses a TransactionDecisionValueSpec object to construct and send a + * transaction confirmation request to user. + * + * Used in subclasses for Actions SDK and Dialogflow. + * @return {Object} HTTP response. + * @private + */ + fulfillTransactionDecision_ () { + debug('fulfillTransactionDecision_'); + return {}; + } + + /** + * Helper to build prompts from SSML's. + * + * @param {Array} ssmls Array of ssml. + * @return {Array} Array of SpeechResponse objects. + * @private + */ + buildPromptsFromSsmlHelper_ (ssmls) { + debug('buildPromptsFromSsmlHelper_: ssmls=%s', ssmls); + const prompts = []; + for (let i = 0; i < ssmls.length; i++) { + const prompt = { + ssml: ssmls[i] + }; + prompts.push(prompt); + } + return prompts; + } + + /** + * Helper to build prompts from plain texts. + * + * @param {Array} plainTexts Array of plain text to speech. + * @return {Array} Array of SpeechResponse objects. + * @private + */ + buildPromptsFromPlainTextHelper_ (plainTexts) { + debug('buildPromptsFromPlainTextHelper_: plainTexts=%s', plainTexts); + const prompts = []; + for (let i = 0; i < plainTexts.length; i++) { + const prompt = { + textToSpeech: plainTexts[i] + }; + prompts.push(prompt); + } + return prompts; + } + + /** + * Helper to process a transaction config and create a payment options object. + * + * @param {ActionPaymentTransactionConfig|GooglePaymentTransactionConfig} + * transactionConfig Configuration for the transaction. Includes payment + * options and order options. + * @return {Object} paymentOptions + * @private + */ + buildPaymentOptions_ (transactionConfig) { + debug('buildPromptsFromPlainTextHelper_: transactionConfig=%s', + JSON.stringify(transactionConfig)); + let paymentOptions = {}; + if (transactionConfig.type) { // Action payment + paymentOptions.actionProvidedOptions = { + paymentType: transactionConfig.type, + displayName: transactionConfig.displayName + }; + } else { // Google payment + paymentOptions.googleProvidedOptions = { + supportedCardNetworks: transactionConfig.cardNetworks, + prepaidCardDisallowed: transactionConfig.prepaidCardDisallowed + }; + if (transactionConfig.tokenizationParameters) { + paymentOptions.googleProvidedOptions.tokenizationParameters = { + tokenizationType: 'PAYMENT_GATEWAY', + parameters: transactionConfig.tokenizationParameters + }; + } + } + return paymentOptions; + } +} + +/** + * Utility class for representing intents by name. + * + * @private + */ +const Intent = class { + /** + * Constructor for Intent object. + * + * @param {string} name The name of the intent. + */ + constructor (name) { + this.name_ = name; + } + + /** + * Getter for the Intent name. + * + * @return {string} The name of the intent. + */ + getName () { + return this.name_; + } +}; + +/** + * Utility class for representing states by name. + * + * @private + */ +const State = class { + /** + * Constructor for State object. + * + * @param {string} name The name of the state. + */ + constructor (name) { + this.name_ = name; + } + + /** + * Getter for the State name. + * + * @return {string} The name of the state. + */ + getName () { + return this.name_; + } +}; + +module.exports = { + AssistantApp: AssistantApp, + State: State +}; diff --git a/types/actions-on-google/staging/dialogflow-app.js b/types/actions-on-google/staging/dialogflow-app.js new file mode 100644 index 0000000000..9c4336195e --- /dev/null +++ b/types/actions-on-google/staging/dialogflow-app.js @@ -0,0 +1,1105 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Debug = require('debug'); +const debug = Debug('actions-on-google:debug'); +const error = Debug('actions-on-google:error'); +const app = require('./assistant-app'); +const AssistantApp = app.AssistantApp; +const State = app.State; +const transformToCamelCase = require('./utils/transform').transformToCamelCase; + +// Constants +const RESPONSE_CODE_OK = 200; +const ACTIONS_DIALOGFLOW_CONTEXT = '_actions_on_google_'; +const MAX_LIFESPAN = 100; +const INPUTS_MAX = 3; +const ORIGINAL_SUFFIX = '.original'; +const SELECT_EVENT = 'actions_intent_option'; + +// Dialogflow Rich Response item types +const SIMPLE_RESPONSE = 'simple_response'; +const BASIC_CARD = 'basic_card'; +const LIST = 'list_card'; +const CAROUSEL = 'carousel_card'; +const SUGGESTIONS = 'suggestion_chips'; +const LINK_OUT_SUGGESTION = 'link_out_chip'; +const TYPE = 'type'; +const PLATFORM = 'platform'; + +// Configure logging for hosting platforms that only support console.log and console.error +debug.log = console.log.bind(console); +error.log = console.error.bind(console); + +// --------------------------------------------------------------------------- +// Dialogflow support +// --------------------------------------------------------------------------- + +/** + * This is the class that handles the communication with Dialogflow's fulfillment API. + */ +class DialogflowApp extends AssistantApp { + /** + * Constructor for DialogflowApp object. + * To be used in the Dialogflow fulfillment webhook logic. + * + * @example + * const DialogflowApp = require('actions-on-google').DialogflowApp; + * const app = new DialogflowApp({request: request, response: response, + * sessionStarted:sessionStarted}); + * + * @param {Object} options JSON configuration. + * @param {Object} options.request Express HTTP request object. + * @param {Object} options.response Express HTTP response object. + * @param {Function=} options.sessionStarted Function callback when session starts. + * Only called if webhook is enabled for welcome/triggering intents, and + * called from Web Simulator or Google Home device (i.e., not Dialogflow simulator). + * @dialogflow + */ + constructor (options) { + debug('DialogflowApp constructor'); + super(options, () => { + const originalRequest = this.body_.originalRequest; + if (!(originalRequest && originalRequest.data)) { + return null; + } + return originalRequest.data; + }); + + // If request contains originalRequest, convert to Proto3. + if (this.body_ && this.body_.originalRequest && !this.isNotApiVersionOne_()) { + this.body_.originalRequest = transformToCamelCase(this.body_.originalRequest); + } + + if (this.body_ && + this.body_.originalRequest && + this.body_.originalRequest.data && + this.body_.originalRequest.data.conversation) { + if (this.body_.originalRequest.data.conversation.type === + this.ConversationStages.NEW && this.sessionStarted_ && + typeof this.sessionStarted_ === 'function') { + this.sessionStarted_(); + } else if (this.sessionStarted_ && typeof this.sessionStarted_ !== 'function') { + this.handleError_('options.sessionStarted must be a Function'); + } + } + } + + /** + * @deprecated + * Verifies whether the request comes from Dialogflow. + * + * @param {string} key The header key specified by the developer in the + * Dialogflow Fulfillment settings of the app. + * @param {string} value The private value specified by the developer inside the + * fulfillment header. + * @return {boolean} True if the request comes from Dialogflow. + * @dialogflow + */ + isRequestFromApiAi (key, value) { + debug('isRequestFromApiAi: key=%s, value=%s', key, value); + console.log('isRequestFromApiAi is *DEPRECATED*, use isRequestFromDialogflow'); + return this.isRequestFromDialogflow(key, value); + } + + /** + * Verifies whether the request comes from Dialogflow. + * + * @param {string} key The header key specified by the developer in the + * Dialogflow Fulfillment settings of the app. + * @param {string} value The private value specified by the developer inside the + * fulfillment header. + * @return {boolean} True if the request comes from Dialogflow. + * @dialogflow + */ + isRequestFromDialogflow (key, value) { + debug('isRequestFromDialogflow: key=%s, value=%s', key, value); + if (!key || key === '') { + error('Key must be specified'); + return false; + } + if (!value || value === '') { + error('Value must be specified'); + return false; + } + return this.request_.get(key) === value; + } + + /** + * Get the current intent. Alternatively, using a handler Map with + * {@link AssistantApp#handleRequest|handleRequest}, + * the client library will automatically handle the incoming intents. + * 'Intent' in the Dialogflow context translates into the current action. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * + * function responseHandler (app) { + * const intent = app.getIntent(); + * switch (intent) { + * case WELCOME_INTENT: + * app.ask('Welcome to action snippets! Say a number.'); + * break; + * + * case NUMBER_INTENT: + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * break; + * } + * } + * + * app.handleRequest(responseHandler); + * + * @return {string} Intent id or null if no value (action name). + * @dialogflow + */ + getIntent () { + debug('getIntent'); + const intent = this.getIntent_(); + if (!intent) { + error('The current action name could not be found in request body'); + return null; + } + return intent; + } + + /** + * Get the argument value by name from the current intent. If the argument + * is included in originalRequest, and is not a text argument, the entire + * argument object is returned. + * + * Note: If incoming request is using an API version under 2 (e.g. 'v1'), + * the argument object will be in Proto2 format (snake_case, etc). + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * + * function welcomeIntent (app) { + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} argName Name of the argument. + * @return {Object} Argument value matching argName + * or null if no matching argument. + * @dialogflow + */ + getArgument (argName) { + debug('getArgument: argName=%s', argName); + if (!argName) { + error('Invalid argument name'); + return null; + } + const { parameters } = this.body_.result; + if (parameters && parameters[argName]) { + return parameters[argName]; + } + return this.getArgumentCommon(argName); + } + + /** + * Get the context argument value by name from the current intent. Context + * arguments include parameters collected in previous intents during the + * lifespan of the given context. If the context argument has an original + * value, usually representing the underlying entity value, that will be given + * as part of the return object. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * const OUT_CONTEXT = 'output_context'; + * const NUMBER_ARG = 'myNumberArg'; + * + * function welcomeIntent (app) { + * const parameters = {}; + * parameters[NUMBER_ARG] = '42'; + * app.setContext(OUT_CONTEXT, 1, parameters); + * app.ask('Welcome to action snippets! Ask me for your number.'); + * } + * + * function numberIntent (app) { + * const number = app.getContextArgument(OUT_CONTEXT, NUMBER_ARG); + * // number === { value: 42 } + * app.tell('Your number is ' + number.value); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} contextName Name of the context. + * @param {string} argName Name of the argument. + * @return {Object} Object containing value property and optional original + * property matching context argument. Null if no matching argument. + * @dialogflow + */ + getContextArgument (contextName, argName) { + debug('getContextArgument: contextName=%s, argName=%s', contextName, argName); + if (!contextName) { + error('Invalid context name'); + return null; + } + if (!argName) { + error('Invalid argument name'); + return null; + } + if (!this.body_.result || + !this.body_.result.contexts) { + error('No contexts included in request'); + return null; + } + for (let context of this.body_.result.contexts) { + if (context.name === contextName && context.parameters[argName]) { + let argument = { value: context.parameters[argName] }; + if (context.parameters[argName + ORIGINAL_SUFFIX]) { + argument.original = context.parameters[argName + ORIGINAL_SUFFIX]; + } + return argument; + } + } + debug('Failed to get context argument value: %s', argName); + return null; + } + + /** + * Returns the RichResponse constructed in Dialogflow response builder. + * + * @example + * const app = new App({request: req, response: res}); + * + * function tellFact (app) { + * let fact = 'Google was founded in 1998'; + * + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.ask(app.getIncomingRichResponse().addSimpleResponse('Here\'s a ' + + * 'fact for you. ' + fact + ' Which one do you want to hear about ' + + * 'next, Google\'s history or headquarters?')); + * } else { + * app.ask('Here\'s a fact for you. ' + fact + ' Which one ' + + * 'do you want to hear about next, Google\'s history or headquarters?'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set('tell.fact', tellFact); + * + * app.handleRequest(actionMap); + * + * @return {RichResponse} RichResponse created in Dialogflow. If no RichResponse was + * created, an empty RichResponse is returned. + * @dialogflow + */ + getIncomingRichResponse () { + debug('getIncomingRichResponse'); + let response = this.buildRichResponse(); + if (this.body_.result && + this.body_.result.fulfillment && + this.body_.result.fulfillment.messages) { + for (let message of this.body_.result.fulfillment.messages) { + if (!message.type) { + continue; + } + if (message.type === SIMPLE_RESPONSE) { + let item = { + simpleResponse: {} + }; + Object.assign(item.simpleResponse, message); + delete item.simpleResponse[TYPE]; + delete item.simpleResponse[PLATFORM]; + response.items.push(item); + } else if (message.type === BASIC_CARD) { + let item = { + basicCard: {} + }; + Object.assign(item.basicCard, message); + delete item.basicCard[TYPE]; + delete item.basicCard[PLATFORM]; + response.items.push(item); + } else if (message.type === SUGGESTIONS) { + response.suggestions = message.suggestions; + } else if (message.type === LINK_OUT_SUGGESTION) { + response.linkOutSuggestion = Object.assign({}, message); + delete response.linkOutSuggestion[TYPE]; + delete response.linkOutSuggestion[PLATFORM]; + } + } + } + return response; + } + + /** + * Returns the List constructed in Dialogflow response builder. + * + * @example + * const app = new App({request: req, response: res}); + * + * function pickOption (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askWithList('Which of these looks good?', + * app.getIncomingList().addItems( + * app.buildOptionItem('another_choice', ['Another choice']). + * setTitle('Another choice'))); + * } else { + * app.ask('What would you like?'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set('pick.option', pickOption); + * + * app.handleRequest(actionMap); + * + * @return {List} List created in Dialogflow. If no List was created, an empty + * List is returned. + * @dialogflow + */ + getIncomingList () { + debug('getIncomingList'); + let list = this.buildList(); + if (this.body_.result && + this.body_.result.fulfillment && + this.body_.result.fulfillment.messages) { + for (let message of this.body_.result.fulfillment.messages) { + if (!message.type) { + continue; + } + if (message.type === LIST) { + Object.assign(list, message); + delete list[TYPE]; + delete list[PLATFORM]; + } + } + } + return list; + } + + /** + * Returns the Carousel constructed in Dialogflow response builder. + * + * @example + * const app = new App({request: req, response: res}); + * + * function pickOption (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askWithCarousel('Which of these looks good?', + * app.getIncomingCarousel().addItems( + * app.buildOptionItem('another_choice', ['Another choice']). + * setTitle('Another choice').setDescription('Choose me!'))); + * } else { + * app.ask('What would you like?'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set('pick.option', pickOption); + * + * app.handleRequest(actionMap); + * + * @return {Carousel} Carousel created in Dialogflow. If no Carousel was created, + * an empty Carousel is returned. + * @dialogflow + */ + getIncomingCarousel () { + debug('getIncomingCarousel'); + let carousel = this.buildCarousel(); + if (this.body_.result && + this.body_.result.fulfillment && + this.body_.result.fulfillment.messages) { + for (let message of this.body_.result.fulfillment.messages) { + if (!message.type) { + continue; + } + if (message.type === CAROUSEL) { + Object.assign(carousel, message); + delete carousel[TYPE]; + delete carousel[PLATFORM]; + } + } + } + return carousel; + } + + /** + * Returns the option key user chose from options response. + * + * @example + * const app = new App({request: req, response: res}); + * + * function pickOption (app) { + * if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) { + * app.askWithCarousel('Which of these looks good?', + * app.getIncomingCarousel().addItems( + * app.buildOptionItem('another_choice', ['Another choice']). + * setTitle('Another choice').setDescription('Choose me!'))); + * } else { + * app.ask('What would you like?'); + * } + * } + * + * function optionPicked (app) { + * app.ask('You picked ' + app.getSelectedOption()); + * } + * + * const actionMap = new Map(); + * actionMap.set('pick.option', pickOption); + * actionMap.set('option.picked', optionPicked); + * + * app.handleRequest(actionMap); + * + * @return {string} Option key of selected item. Null if no option selected or + * if current intent is not OPTION intent. + * @dialogflow + */ + getSelectedOption () { + debug('getSelectedOption'); + if (this.getContextArgument(SELECT_EVENT, this.BuiltInArgNames.OPTION) && + this.getContextArgument(SELECT_EVENT, this.BuiltInArgNames.OPTION).value) { + return this.getContextArgument(SELECT_EVENT, this.BuiltInArgNames.OPTION).value; + } else if (this.getArgument(this.BuiltInArgNames.OPTION)) { + return this.getArgument(this.BuiltInArgNames.OPTION); + } + debug('Failed to get selected option'); + return null; + } + + /** + * Asks to collect the user's input. + * {@link https://developers.google.com/actions/policies/general-policies#user_experience|The guidelines when prompting the user for a response must be followed at all times}. + * + * NOTE: Due to a bug, if you specify the no-input prompts, + * the mic is closed after the 3rd prompt, so you should use the 3rd prompt + * for a bye message until the bug is fixed. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * + * function welcomeIntent (app) { + * app.ask('Welcome to action snippets! Say a number.', + * ['Say any number', 'Pick a number', 'We can stop here. See you soon.']); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string|SimpleResponse|RichResponse} inputPrompt The input prompt + * response. + * @param {Array=} noInputs Array of re-prompts when the user does not respond (max 3). + * @return {Object} HTTP response. + * @dialogflow + */ + ask (inputPrompt, noInputs) { + debug('ask: inputPrompt=%s, noInputs=%s', inputPrompt, noInputs); + if (!inputPrompt) { + this.handleError_('Invalid input prompt'); + return null; + } + const response = this.buildResponse_(inputPrompt, true, noInputs); + if (!response) { + this.handleError_('Error in building response'); + return null; + } + return this.doResponse_(response, RESPONSE_CODE_OK); + } + + /** + * Asks to collect the user's input with a list. + * + * @example + * const app = new DialogflowApp({request, response}); + * const WELCOME_INTENT = 'input.welcome'; + * const OPTION_INTENT = 'option.select'; + * + * function welcomeIntent (app) { + * app.askWithList('Which of these looks good?', + * app.buildList('List title') + * .addItems([ + * app.buildOptionItem(SELECTION_KEY_ONE, + * ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2']) + * .setTitle('Title of First List Item'), + * app.buildOptionItem(SELECTION_KEY_TWO, + * ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2']) + * .setTitle('Title of Second List Item'), + * ])); + * } + * + * function optionIntent (app) { + * if (app.getSelectedOption() === SELECTION_KEY_ONE) { + * app.tell('Number one is a great choice!'); + * } else { + * app.tell('Number two is a great choice!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(OPTION_INTENT, optionIntent); + * app.handleRequest(actionMap); + * + * @param {string|RichResponse|SimpleResponse} inputPrompt The input prompt + * response. + * @param {List} list List built with {@link AssistantApp#buildList|buildList}. + * @return {Object} HTTP response. + * @dialogflow + */ + askWithList (inputPrompt, list) { + debug('askWithList: inputPrompt=%s, list=%s', + inputPrompt, JSON.stringify(list)); + if (!inputPrompt) { + this.handleError_('Invalid input prompt'); + return null; + } + if (!list || typeof list !== 'object') { + this.handleError_('Invalid list'); + return null; + } + if (list.items.length < 2) { + this.handleError_('List requires at least 2 items'); + return null; + } + const response = this.buildResponse_(inputPrompt, true); + if (!response) { + error('Error in building response'); + return null; + } + response.data.google.systemIntent = { + intent: this.StandardIntents.OPTION + }; + if (this.isNotApiVersionOne_()) { + response.data.google.systemIntent.data = Object.assign({ + [this.ANY_TYPE_PROPERTY_]: this.InputValueDataTypes_.OPTION + }, { + listSelect: list + }); + } else { + response.data.google.systemIntent.spec = { + optionValueSpec: { + listSelect: list + } + }; + } + return this.doResponse_(response, RESPONSE_CODE_OK); + } + + /** + * Asks to collect the user's input with a carousel. + * + * @example + * const app = new DialogflowApp({request, response}); + * const WELCOME_INTENT = 'input.welcome'; + * const OPTION_INTENT = 'option.select'; + * + * function welcomeIntent (app) { + * app.askWithCarousel('Which of these looks good?', + * app.buildCarousel() + * .addItems([ + * app.buildOptionItem(SELECTION_KEY_ONE, + * ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2']) + * .setTitle('Number one'), + * app.buildOptionItem(SELECTION_KEY_TWO, + * ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2']) + * .setTitle('Number two'), + * ])); + * } + * + * function optionIntent (app) { + * if (app.getSelectedOption() === SELECTION_KEY_ONE) { + * app.tell('Number one is a great choice!'); + * } else { + * app.tell('Number two is a great choice!'); + * } + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(OPTION_INTENT, optionIntent); + * app.handleRequest(actionMap); + * + * @param {string|RichResponse|SimpleResponse} inputPrompt The input prompt + * response. + * @param {Carousel} carousel Carousel built with + * {@link AssistantApp#buildCarousel|buildCarousel}. + * @return {Object} HTTP response. + * @dialogflow + */ + askWithCarousel (inputPrompt, carousel) { + debug('askWithCarousel: inputPrompt=%s, carousel=%s', + inputPrompt, JSON.stringify(carousel)); + if (!inputPrompt) { + this.handleError_('Invalid input prompt'); + return null; + } + if (!carousel || typeof carousel !== 'object') { + this.handleError_('Invalid carousel'); + return null; + } + if (carousel.items.length < 2) { + this.handleError_('Carousel requires at least 2 items'); + return null; + } + const response = this.buildResponse_(inputPrompt, true); + if (!response) { + error('Error in building response'); + return null; + } + response.data.google.systemIntent = { + intent: this.StandardIntents.OPTION + }; + if (this.isNotApiVersionOne_()) { + response.data.google.systemIntent.data = Object.assign({ + [this.ANY_TYPE_PROPERTY_]: this.InputValueDataTypes_.OPTION + }, { + carouselSelect: carousel + }); + } else { + response.data.google.systemIntent.spec = { + optionValueSpec: { + carouselSelect: carousel + } + }; + } + return this.doResponse_(response, RESPONSE_CODE_OK); + } + + /** + * Tells the Assistant to render the speech response and close the mic. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const WELCOME_INTENT = 'input.welcome'; + * const NUMBER_INTENT = 'input.number'; + * + * function welcomeIntent (app) { + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string|SimpleResponse|RichResponse} speechResponse Final response. + * Spoken response can be SSML. + * @return {(Object|null)} The response that is sent back to Assistant. + * @dialogflow + */ + tell (speechResponse) { + debug('tell: speechResponse=%s', speechResponse); + if (!speechResponse) { + this.handleError_('Invalid speech response'); + return null; + } + const response = this.buildResponse_(speechResponse, false); + return this.doResponse_(response, RESPONSE_CODE_OK); + } + + /** + * Set a new context for the current intent. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const CONTEXT_NUMBER = 'number'; + * const NUMBER_ARGUMENT = 'myNumber'; + * + * function welcomeIntent (app) { + * app.setContext(CONTEXT_NUMBER); + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} name Name of the context. Dialogflow converts to lowercase. + * @param {int} [lifespan=1] Context lifespan. + * @param {Object=} parameters Context JSON parameters. + * @return {null|undefined} Null if the context name is not defined. + * @dialogflow + */ + setContext (name, lifespan, parameters) { + debug('setContext: context=%s, lifespan=%d, parameters=%s', name, lifespan, + JSON.stringify(parameters)); + if (!name) { + error('Empty context name'); + return null; + } + const newContext = { + name: name, + lifespan: 1 + }; + if (lifespan !== null && lifespan !== undefined) { + newContext.lifespan = lifespan; + } + if (parameters) { + newContext.parameters = parameters; + } + this.contexts_[name] = newContext; + } + + /** + * Dialogflow {@link https://dialogflow.com/docs/concept-contexts|Context}. + * @typedef {Object} Context + * @property {string} name - Full name of the context. + * @property {Object} parameters - Parameters carried within this context. + See {@link https://dialogflow.com/docs/concept-actions#section-extracting-values-from-contexts|here}. + * @property {number} lifespan - Remaining number of intents + */ + + /** + * Returns the incoming contexts for this intent. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const CONTEXT_NUMBER = 'number'; + * const NUMBER_ARGUMENT = 'myNumber'; + * + * function welcomeIntent (app) { + * app.setContext(CONTEXT_NUMBER); + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * let contexts = app.getContexts(); + * // contexts === [{ + * // name: 'number', + * // lifespan: 0, + * // parameters: { + * // myNumber: '23', + * // myNumber.original: '23' + * // } + * // }] + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @return {Context[]} Empty if no active contexts. + * @dialogflow + */ + getContexts () { + debug('getContexts'); + if (!this.body_.result || + !this.body_.result.contexts) { + error('No contexts included in request'); + return null; + } + return this.body_.result.contexts.filter((context) => { + return context.name !== ACTIONS_DIALOGFLOW_CONTEXT; + }); + } + + /** + * Returns the incoming context by name for this intent. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * const CONTEXT_NUMBER = 'number'; + * const NUMBER_ARGUMENT = 'myNumber'; + * + * function welcomeIntent (app) { + * app.setContext(CONTEXT_NUMBER); + * app.ask('Welcome to action snippets! Say a number.'); + * } + * + * function numberIntent (app) { + * let context = app.getContext(CONTEXT_NUMBER); + * // context === { + * // name: 'number', + * // lifespan: 0, + * // parameters: { + * // myNumber: '23', + * // myNumber.original: '23' + * // } + * // } + * const number = app.getArgument(NUMBER_ARGUMENT); + * app.tell('You said ' + number); + * } + * + * const actionMap = new Map(); + * actionMap.set(WELCOME_INTENT, welcomeIntent); + * actionMap.set(NUMBER_INTENT, numberIntent); + * app.handleRequest(actionMap); + * + * @param {string} name The name of the Context to retrieve. + * @return {Object} Context value matching name + * or null if no matching context. + * @dialogflow + */ + getContext (name) { + debug('getContext: name=%s', name); + if (!this.body_.result || + !this.body_.result.contexts) { + error('No contexts included in request'); + return null; + } + for (let context of this.body_.result.contexts) { + if (context.name === name) { + return context; + } + } + debug('Failed to get context: %s', name); + return null; + } + + /** + * Gets the user's raw input query. + * + * @example + * const app = new DialogflowApp({request: request, response: response}); + * app.tell('You said ' + app.getRawInput()); + * + * @return {string} User's raw query or null if no value. + * @dialogflow + */ + getRawInput () { + debug('getRawInput'); + if (!this.body_.result || + !this.body_.result.resolvedQuery) { + error('No raw input'); + return null; + } + return this.body_.result.resolvedQuery; + } + + // --------------------------------------------------------------------------- + // Private Helpers + // --------------------------------------------------------------------------- + + /** + * Get the current intent. + * + * @return {string} The intent id. + * @private + * @dialogflow + */ + getIntent_ () { + debug('getIntent_'); + if (this.body_.result) { + return this.body_.result.action; + } else { + error('Missing result from request body'); + return null; + } + } + + /** + * Builds a response for Dialogflow to send back to the Assistant. + * + * @param {string|RichResponse|SimpleResponse} textToSpeech TTS/response + * spoken/shown to end user. + * @param {boolean} expectUserResponse true if the user response is expected. + * @param {Array=} noInputs Array of re-prompts when the user does not respond (max 3). + * @return {Object} The final response returned to Assistant. + * @private + * @dialogflow + */ + buildResponse_ (textToSpeech, expectUserResponse, noInputs) { + debug('buildResponse_: textToSpeech=%s, expectUserResponse=%s, noInputs=%s', + textToSpeech, expectUserResponse, noInputs); + if (!textToSpeech === undefined || !textToSpeech) { + error('Empty text to speech'); + return null; + } + let isStringResponse = typeof textToSpeech === 'string'; + if (!isStringResponse) { + if (textToSpeech.speech) { + // Convert SimpleResponse to RichResponse + textToSpeech = this.buildRichResponse().addSimpleResponse(textToSpeech); + } else if (!(textToSpeech.items && + textToSpeech.items[0] && + textToSpeech.items[0].simpleResponse)) { + error('Invalid RichResponse. First item must be SimpleResponse'); + return null; + } + } + const dialogState = { + 'state': (this.state instanceof State ? this.state.getName() : this.state), + 'data': this.data + }; + if (noInputs) { + if (noInputs.length > INPUTS_MAX) { + error('Invalid number of no inputs'); + return null; + } + if (this.isSsml_(textToSpeech)) { + noInputs = this.buildPromptsFromSsmlHelper_(noInputs); + } else { + noInputs = this.buildPromptsFromPlainTextHelper_(noInputs); + } + } else { + noInputs = []; + } + const response = { + speech: isStringResponse ? textToSpeech + : textToSpeech.items[0].simpleResponse.textToSpeech, + contextOut: [] + }; + response.data = isStringResponse ? { + google: { + expectUserResponse: expectUserResponse, + isSsml: this.isSsml_(textToSpeech), + noInputPrompts: noInputs + } + } : { + google: { + expectUserResponse: expectUserResponse, + richResponse: textToSpeech, + noInputPrompts: noInputs + } + }; + if (expectUserResponse) { + response.contextOut.push({ + name: ACTIONS_DIALOGFLOW_CONTEXT, + lifespan: MAX_LIFESPAN, + parameters: dialogState.data + }); + } + for (let context of Object.keys(this.contexts_)) { + response.contextOut.push(this.contexts_[context]); + } + return response; + } + + /** + * Extract the session data from the incoming JSON request. + * + * @return {undefined} + * @private + * @dialogflow + */ + extractData_ () { + debug('extractData_'); + if (this.body_.result && this.body_.result.contexts.length > 0) { + for (let i = 0; i < this.body_.result.contexts.length; i++) { + if (this.body_.result.contexts[i].name === ACTIONS_DIALOGFLOW_CONTEXT) { + const parameters = this.body_.result.contexts[i].parameters; + if (parameters) { + this.data = parameters; + } else { + this.data = {}; + } + break; + } + } + } else { + this.data = {}; + } + } + + /** + * Uses a PermissionsValueSpec object to construct and send a + * permissions request to the user. + * + * @param {Object} permissionsSpec PermissionsValueSpec object containing + * the permissions prefix and permissions requested. + * @return {Object} The HTTP response. + * @private + * @dialogflow + */ + fulfillPermissionsRequest_ (permissionsSpec) { + debug('fulfillPermissionsRequest_: permissionsSpec=%s', + JSON.stringify(permissionsSpec)); + const inputPrompt = 'PLACEHOLDER_FOR_PERMISSION'; + if (this.isNotApiVersionOne_()) { + return this.fulfillSystemIntent_(this.StandardIntents.PERMISSION, + this.InputValueDataTypes_.PERMISSION, permissionsSpec, + inputPrompt); + } else { + const response = this.buildResponse_(inputPrompt, true); + response.data.google.systemIntent = { + intent: this.StandardIntents.PERMISSION + }; + response.data.google.systemIntent.spec = { + permissionValueSpec: permissionsSpec + }; + return this.doResponse_(response, RESPONSE_CODE_OK); + } + } + + /** + * Uses a given intent spec to construct and send a non-TEXT intent response + * to Google. + * + * @param {string} intent Name of the intent to fulfill. One of + * {@link AssistantApp#StandardIntents|StandardIntents}. + * @param {string} specType Type of the related intent spec. One of + * {@link AssistantApp#InputValueDataTypes_|InputValueDataTypes_}. + * @param {Object} intentSpec Intent Spec object. Pass {} to leave empty. + * @param {string=} promptPlaceholder Some placeholder text for the response + * prompt. Default is 'PLACEHOLDER_FOR_INTENT'. + * @param {Object=} dialogState JSON object the app uses to hold dialog state that + * will be circulated back by Assistant. + * @return {Object} HTTP response. + * @private + * @dialogflow + */ + fulfillSystemIntent_ (intent, specType, intentSpec, promptPlaceholder, + dialogState) { + debug('fulfillSystemIntent_: intent=%s, specType=%s, intentSpec=%s, ' + + 'promptPlaceholder=%s dialogState=%s', intent, specType, + JSON.stringify(intentSpec), promptPlaceholder, JSON.stringify(dialogState)); + const response = this.buildResponse_(promptPlaceholder || + 'PLACEHOLDER_FOR_INTENT', true); + response.data.google.systemIntent = { intent }; + response.data.google.systemIntent.data = {}; + if (intentSpec) { + response.data.google.systemIntent.data = Object.assign({ + [this.ANY_TYPE_PROPERTY_]: specType + }, intentSpec); + } + return this.doResponse_(response, RESPONSE_CODE_OK); + } +} + +module.exports = DialogflowApp; diff --git a/types/actions-on-google/staging/response-builder.js b/types/actions-on-google/staging/response-builder.js new file mode 100644 index 0000000000..6e6238eeee --- /dev/null +++ b/types/actions-on-google/staging/response-builder.js @@ -0,0 +1,800 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A collection of response builders. + */ + +'use strict'; + +const Debug = require('debug'); +const debug = Debug('actions-on-google:debug'); +const warn = Debug('actions-on-google:warn'); +const error = Debug('actions-on-google:error'); + +const LIST_ITEM_LIMIT = 30; +const CAROUSEL_ITEM_LIMIT = 10; + +/** + * Simple Response type. + * @typedef {Object} SimpleResponse + * @property {string} speech - Speech to be spoken to user. SSML allowed. + * @property {string} displayText - Optional text to be shown to user + */ + +/** + * Suggestions to show with response. + * @typedef {Object} Suggestion + * @property {string} title - Text of the suggestion. + */ + +/** + * Link Out Suggestion. Used in rich response as a suggestion chip which, when + * selected, links out to external URL. + * @typedef {Object} LinkOutSuggestion + * @property {string} title - Text shown on the suggestion chip. + * @property {string} url - String URL to open. + */ + +/** + * Image type shown on visual elements. + * @typedef {Object} Image + * @property {string} url - Image source URL. + * @property {string} accessibilityText - Text to replace for image for + * accessibility. + * @property {number} width - Width of the image. + * @property {number} height - Height of the image. + */ + +/** + * Basic Card Button. Shown below basic cards. Open a URL when selected. + * @typedef {Object} Button + * @property {string} title - Text shown on the button. + * @property {Object} openUrlAction - Action to take when selected. + * @property {string} openUrlAction.url - String URL to open. + */ + +/** + * Option item. Used in actions.intent.OPTION intent. + * @typedef {Object} OptionItem + * @property {OptionInfo} optionInfo - Option item identifier information. + * @property {string} title - Name of the item. + * @property {string} description - Optional text describing the item. + * @property {Image} image - Square image to show for this item. + */ + +/** + * Option info. Provides unique identifier for a given OptionItem. + * @typedef {Object} OptionInfo + * @property {string} key - Unique string ID for this option. + * @property {Array} synonyms - Synonyms that can be used by the user + * to indicate this option if they do not use the key. + */ + +/** + * Class for initializing and constructing Rich Responses with chainable interface. + */ +const RichResponse = class { + /** + * Constructor for RichResponse. Accepts optional RichResponse to clone. + * + * @param {RichResponse=} richResponse Optional RichResponse to clone. + */ + constructor (richResponse) { + /** + * Ordered list of either SimpleResponse objects or BasicCard objects. + * First item must be SimpleResponse. There can be at most one card. + * @type {Array} + */ + this.items = []; + + /** + * Ordered list of text suggestions to display. Optional. + * @type {Array} + */ + this.suggestions = []; + + /** + * Link Out Suggestion chip for this rich response. Optional. + * @type {LinkOutSuggestion} + */ + this.linkOutSuggestion = undefined; + + if (richResponse) { + if (richResponse.items) { + this.items = richResponse.items; + for (let item of this.items) { + if (item.basicCard) { + item.basicCard = new BasicCard(item.basicCard); + } + } + } + if (richResponse.suggestions) { + this.suggestions = richResponse.suggestions; + } + if (richResponse.linkOutSuggestion) { + this.linkOutSuggestion = richResponse.linkOutSuggestion; + } + } + } + + /** + * Adds a SimpleResponse to list of items. + * + * @param {string|SimpleResponse} simpleResponse Simple response to present to + * user. If just a string, display text will not be set. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addSimpleResponse (simpleResponse) { + if (!simpleResponse) { + error('Invalid simpleResponse'); + return this; + } + // Validate if RichResponse already contains two SimpleResponse objects + let simpleResponseCount = 0; + for (let item of this.items) { + if (item.simpleResponse) { + simpleResponseCount++; + } + if (simpleResponseCount >= 2) { + error('Cannot include >2 SimpleResponses in RichResponse'); + return this; + } + } + const simpleResponseObj = { + simpleResponse: this.buildSimpleResponseHelper_(simpleResponse) + }; + // Check first if needs to replace BasicCard at beginning of items list + if (this.items.length > 0 && (this.items[0].basicCard || + this.items[0].structuredResponse)) { + this.items.unshift(simpleResponseObj); + } else { + this.items.push(simpleResponseObj); + } + return this; + } + + /** + * Adds a BasicCard to list of items. + * + * @param {BasicCard} basicCard Basic card to include in response. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addBasicCard (basicCard) { + if (!basicCard) { + error('Invalid basicCard'); + return this; + } + // Validate if basic card is already present + for (let item of this.items) { + if (item.basicCard) { + error('Cannot include >1 BasicCard in RichResponse'); + return this; + } + } + this.items.push({ + basicCard: basicCard + }); + return this; + } + + /** + * Adds a single suggestion or list of suggestions to list of items. + * + * @param {string|Array} suggestions Either a single string suggestion + * or list of suggestions to add. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addSuggestions (suggestions) { + if (!suggestions) { + error('Invalid suggestions'); + return this; + } + if (Array.isArray(suggestions)) { + for (let suggestion of suggestions) { + if (this.isValidSuggestionText(suggestion)) { + this.suggestions.push({title: suggestion}); + } else { + warn('Suggestion text can\'t be longer than 25 characters: ' + suggestion + + '. This suggestion won\'t be added to the list.'); + } + } + } else { + if (this.isValidSuggestionText(suggestions)) { + this.suggestions.push({title: suggestions}); + } else { + warn('Suggestion text can\'t be longer than 25 characters: ' + suggestions + + '. This suggestion won\'t be added to the list.'); + } + } + return this; + } + + /** + * Returns true if the given suggestion text is valid to be added to the suggestion list. A valid + * text string is not longer than 25 characters. + * + * @param {string} suggestionText Text to validate as suggestion. + * @return {boolean} True if the text is valid, false otherwise.s + */ + isValidSuggestionText (suggestionText) { + return suggestionText && suggestionText.length && suggestionText.length <= 25; + } + + /** + * Sets the suggestion link for this rich response. + * + * @param {string} destinationName Name of the link out destination. + * @param {string} suggestionUrl - String URL to open when suggestion is used. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addSuggestionLink (destinationName, suggestionUrl) { + if (!destinationName) { + error('destinationName cannot be empty'); + return this; + } + if (!suggestionUrl) { + error('suggestionUrl cannot be empty'); + return this; + } + this.linkOutSuggestion = { + destinationName: destinationName, + url: suggestionUrl + }; + return this; + } + + /** + * Adds an order update to this response. Use after a successful transaction + * decision to confirm the order. + * + * @param {OrderUpdate} orderUpdate OrderUpdate object to add. + * @return {RichResponse} Returns current constructed RichResponse. + */ + addOrderUpdate (orderUpdate) { + if (!orderUpdate) { + error('Invalid orderUpdate'); + return this; + } + // Validate if RichResponse already contains StructuredResponse object + for (let item of this.items) { + if (item.structuredResponse) { + debug('Cannot include >1 StructuredResponses in RichResponse'); + return this; + } + } + this.items.push({ + structuredResponse: { + orderUpdate: orderUpdate + } + }); + return this; + } + + /** + * Helper to build SimpleResponse from speech and display text. + * + * @param {string|SimpleResponse} response String to speak, or SimpleResponse. + * SSML allowed. + * @param {string} response.speech If using SimpleResponse, speech to be spoken + * to user. + * @param {string=} response.displayText If using SimpleResponse, text to be shown + * to user. + * @return {Object} Appropriate SimpleResponse object. + * @private + */ + buildSimpleResponseHelper_ (response) { + if (!response) { + error('Invalid response'); + return null; + } + debug('buildSimpleResponseHelper_: response=%s', JSON.stringify(response)); + let simpleResponseObj = {}; + if (typeof response === 'string') { + simpleResponseObj = isSsml(response) + ? { ssml: response } : { textToSpeech: response }; + } else if (response.speech) { + simpleResponseObj = isSsml(response.speech) + ? { ssml: response.speech } : { textToSpeech: response.speech }; + simpleResponseObj.displayText = response.displayText; + } else { + error('SimpleResponse requires a speech parameter.'); + return null; + } + return simpleResponseObj; + } +}; + +/** + * Class for initializing and constructing Basic Cards with chainable interface. + */ +const BasicCard = class { + /** + * Constructor for BasicCard. Accepts optional BasicCard to clone. + * + * @param {BasicCard=} basicCard Optional BasicCard to clone. + */ + constructor (basicCard) { + /** + * Title of the card. Optional. + * @type {string} + */ + this.title = undefined; + + /** + * Body text to show on the card. Required, unless image is present. + * @type {string} + */ + this.formattedText = ''; + + /** + * Subtitle of the card. Optional. + * @type {string} + */ + this.subtitle = undefined; + + /** + * Image to show on the card. Optional. + * @type {Image} + */ + this.image = undefined; + + /** + * Ordered list of buttons to show below card. Optional. + * @type {Array