lib/component/sdk.js

  1. /* eslint-disable no-prototype-builtins */
  2. 'use strict';
  3. const { BaseContext } = require("./baseContext");
  4. const ComponentRequestSchemaFactory = require("./schema/componentRequestSchema");
  5. const { MessageModel } = require("../message/messageModel");
  6. // fix for MIECS-23476, we need to return "2.0" until all customers have migrated to 20.05 which
  7. // no longer checks whether version returned is a valid platform version
  8. // const sdkVersion = '2.0';
  9. const sdkVersion = require('../../package.json').version;
  10. // Response template
  11. const RESPONSE = {
  12. platformVersion: undefined,
  13. context: undefined,
  14. action: undefined,
  15. keepTurn: true,
  16. transition: false,
  17. error: false,
  18. modifyContext: false
  19. };
  20. // Variable types supported by the dialog engine
  21. const CONST = {
  22. SYSTEM_INVALID_USER_INPUT: 'system.invalidUserInput',
  23. };
  24. /**
  25. * The Bots Node SDK uses this class to receive bot requests, which is provided
  26. * to the custom component invocation.
  27. * <p>
  28. * It offers a comprehensive interface to reading context for the invocation
  29. * as well as changing variables and sending results back to the dialog engine.
  30. * </p>
  31. * @memberof module:Lib
  32. * @extends BaseContext
  33. * @alias CustomComponentContext
  34. * @example <caption>Context object used to invoke Custom Component</caption>
  35. *
  36. * const MyCustomComponent = {
  37. * metadata: () => ({name: 'hello'}),
  38. * invoke: async (context) => {
  39. * // use conversation instance methods to respond, set variables, etc.
  40. * context.reply('Hello!');
  41. * context.transition();
  42. * }
  43. * }
  44. */
  45. class CustomComponentContext extends BaseContext {
  46. /**
  47. * @param {object} request - The request body
  48. */
  49. constructor(request) {
  50. // Initilize the response, filling in platformVersion, context/vars
  51. // from the incoming request as needed.
  52. const response = Object.assign({}, RESPONSE, { platformVersion: request.platformVersion });
  53. super(request, response, ComponentRequestSchemaFactory);
  54. // Reset system.invalidUserInput variable if set to true. Requested by runtime to do this in sdk
  55. this._resetInvalidUserInput();
  56. }
  57. /**
  58. * Retrieves the request body.
  59. * @return {object} The request body.
  60. */
  61. request() {
  62. return super.getRequest();
  63. }
  64. /**
  65. * Retrieves the bot id.
  66. * @return {string} The bot id.
  67. */
  68. botId() {
  69. return this.request().botId;
  70. }
  71. /**
  72. * Retrieves the sdk version.
  73. * @return {string} The sdk version.
  74. */
  75. static sdkVersion() {
  76. return sdkVersion;
  77. }
  78. /**
  79. * Retrieves the platform version of the request.
  80. * @return {string} The platform version.
  81. */
  82. platformVersion() {
  83. return this.request().platformVersion;
  84. }
  85. /**
  86. * Retrieves the raw payload of the current input message.
  87. * @return {object} The raw payload.
  88. */
  89. rawPayload() {
  90. return this.request().message.payload;
  91. }
  92. /**
  93. * Retrieves the payload of the current input message in the common message format.
  94. * @return {object} The common message payload.
  95. */
  96. messagePayload() {
  97. return this.request().message.messagePayload;
  98. }
  99. /**
  100. * Retrieves the payload of the current input message. For backward compatibility purposes.
  101. * However, the payload returned may be in the new message format.
  102. * @return {object} The message payload.
  103. * @deprecated to be removed in favor of rawPayload() and messagePayload()
  104. * @private
  105. */
  106. payload() {
  107. this.logger().warn("conversation SDK payload() is deprecated in favor of messagePayload()");
  108. return this.rawPayload();
  109. }
  110. /**
  111. * Retrieves the channel type of the current input message.
  112. * @return {string} The channel type - facebook, webhook, test, etc.
  113. */
  114. channelType() {
  115. return this.request().message.channelConversation.type;
  116. }
  117. /**
  118. * Retrieves the channel Id of the current input message.
  119. * @return {string} The channel id.
  120. */
  121. channelId() {
  122. return this.request().message.channelConversation.channelId;
  123. }
  124. /**
  125. * Retrieves the userId for the current input message.
  126. * @return {string} The userId.
  127. */
  128. userId() {
  129. return this.request().message.channelConversation.userId;
  130. }
  131. /**
  132. * Retrieves the sessionId for the current input message.
  133. * @return {string} The sessionId.
  134. */
  135. sessionId() {
  136. return this.request().message.channelConversation.sessionId;
  137. }
  138. // retrieve v1.0 facebook postback
  139. _postback10() {
  140. const rawPayload = this.rawPayload();
  141. if (rawPayload && this.channelType() === 'facebook') {
  142. if (rawPayload.hasOwnProperty('postback') && rawPayload.postback.hasOwnProperty('payload')) {
  143. return rawPayload.postback.payload;
  144. }
  145. }
  146. return null;
  147. }
  148. /**
  149. * Retrieves the postback of the current input message.
  150. * If the input message is not a postback, this will return null.
  151. * @return {object} The postback payload.
  152. */
  153. postback() {
  154. let postback = null;
  155. const messagePayload = this.messagePayload();
  156. if (messagePayload && messagePayload.postback) {
  157. postback = messagePayload.postback;
  158. }
  159. if (!postback) {
  160. postback = this._postback10();
  161. }
  162. return postback;
  163. }
  164. // return v1.0 facebook text and quick_reply text
  165. _text10() {
  166. const rawPayload = this.rawPayload();
  167. if (rawPayload && this.channelType() === 'facebook') {
  168. if (rawPayload.hasOwnProperty('message')) {
  169. if (rawPayload.message.hasOwnProperty('quick_reply') && rawPayload.message.quick_reply.hasOwnProperty('payload')) {
  170. return rawPayload.message.quick_reply.payload;
  171. } else if (rawPayload.message.hasOwnProperty('text')) {
  172. return rawPayload.message.text;
  173. }
  174. }
  175. }
  176. return null;
  177. }
  178. /**
  179. * Retrieves the text of the current input message.
  180. * Eventually not all messages will have a text value, in which case
  181. * this will return null.
  182. * @return {string} The text of the input message.
  183. */
  184. text() {
  185. let text = null;
  186. const messagePayload = this.messagePayload();
  187. if (messagePayload) {
  188. if (messagePayload.text) {
  189. text = messagePayload.text;
  190. } else {
  191. const postback = this.postback();
  192. if (postback && typeof postback === 'string') {
  193. text = postback;
  194. }
  195. }
  196. }
  197. if (!text) {
  198. text = this._text10();
  199. }
  200. return text;
  201. }
  202. /**
  203. * Retrieves the attachment of the current input message.
  204. * If the input message is not an attachment, this will return null.
  205. * @return {object} The attachment.
  206. */
  207. attachment() {
  208. let attachment = null;
  209. const messagePayload = this.messagePayload();
  210. if (messagePayload && messagePayload.attachment) {
  211. attachment = messagePayload.attachment;
  212. }
  213. return attachment;
  214. }
  215. /**
  216. * Retrieves the location of the current input message.
  217. * If the input message does not contain a location, this will return null.
  218. * @return {object} The location.
  219. */
  220. location() {
  221. let location = null;
  222. const messagePayload = this.messagePayload();
  223. if (messagePayload && messagePayload.location) {
  224. location = messagePayload.location;
  225. }
  226. return location;
  227. }
  228. /**
  229. * Retrieves the properties defined for the current state.
  230. * @return {object} The properties
  231. */
  232. properties() {
  233. return this.request().properties || {};
  234. }
  235. /**
  236. * Returns the MessageModel class for creating or validating messages to or from bots.
  237. * @return {MessageModel} The MessageModel class
  238. * @deprecated Use getMessageFactory() instead
  239. */
  240. MessageModel() {
  241. return super.getMessageModel();
  242. }
  243. /**
  244. * Sets the action to return from this component, which will determine the
  245. * next state in the dialog.
  246. *
  247. * @param {string} a - action name
  248. * @deprecated to be removed in favor of transition(action)
  249. * @private
  250. */
  251. action(a) {
  252. this.logger().warn("conversation SDK action() is deprecated in favor of transition(action)");
  253. if (a === undefined) {
  254. return this.response().action;
  255. }
  256. this.response().action = a;
  257. return this;
  258. }
  259. /**
  260. * Call this method if the input is not understood, and this would allow the bots runtime to
  261. * handle the issue. The bots runtime may just display the message to the user and execute the same component again, or
  262. * it may try to interpret the input and process differently.
  263. * @param {object|string|MessageModel} [r] - optional payload to be sent to user. payload could also be a string for text response
  264. */
  265. invalidUserInput(r) {
  266. this.variable(CONST.SYSTEM_INVALID_USER_INPUT, true);
  267. this.reply(r||'Input not understood. Please try again');
  268. return this;
  269. }
  270. _resetInvalidUserInput() {
  271. if (this.variable(CONST.SYSTEM_INVALID_USER_INPUT) === true) {
  272. this.variable(CONST.SYSTEM_INVALID_USER_INPUT, false);
  273. }
  274. }
  275. /**
  276. * Set "exit" to true when your component has replies it wants to send to
  277. * the client.
  278. * <p>
  279. * The SDK's "reply" function automatically sets "exit" to true, but
  280. * if you manually modify the response to send replies then you will need
  281. * to set this explicitly.
  282. * </p>
  283. * @private
  284. * @deprecated to be removed in favor of keepTurn(boolean)
  285. */
  286. exit(e) {
  287. this.logger().warn("conversation SDK exit() is deprecated in favor of keepTurn(boolean)");
  288. this.response().keepTurn = !e;
  289. return this;
  290. }
  291. /**
  292. * "keepTurn" is used to indicate if the Bot/component should send the next replies, or
  293. * or if the Bot/component should wait for user input (keepTurn = false).
  294. * <p>
  295. * The SDK's "reply" function automatically sets "keepTurn" to false.
  296. * </p>
  297. * @param {boolean} [k] - whether to keep the turn for sending more replies
  298. */
  299. keepTurn(k) {
  300. this.response().keepTurn = (typeof k === "undefined" ? true : !!k);
  301. return this;
  302. }
  303. /**
  304. * "releaseTurn" is the shorthand for keepTurn(false)
  305. * @param {boolean} [k] - whether to keep the turn for sending more replies
  306. */
  307. releaseTurn(k) {
  308. this.response().keepTurn = (typeof k === "undefined" ? false : !k);
  309. return this;
  310. }
  311. /**
  312. * Set "done" to true when your component has completed its logic and
  313. * the dialog should transition to the next state.
  314. * <p>
  315. * This is only meaningful when you are sending replies (ie you have also
  316. * set "exit" to true). If you are not sending replies ("exit" is false,
  317. * the default) then "done" is ignored; the dialog always moves to the next
  318. * state.
  319. * </p>
  320. * If "exit" is true (replies are being sent), then leaving "done" as false
  321. * (the default) means the dialog will stay in this state after sending
  322. * the replies, and subsequent user input will come back to this component.
  323. * This allows a component to handle a series of interactions within itself,
  324. * however the component is responsible for keeping track of its own state
  325. * in such situations.
  326. * <p>
  327. * Setting "done" to true will transition to the next state/component after
  328. * sending the replies.
  329. * </p>
  330. * @private
  331. * @deprecated to be removed in favor of transition()
  332. */
  333. done(d) {
  334. this.logger().warn("conversation SDK done() is deprecated in favor of transition()");
  335. this.response().transition = !!d;
  336. return this;
  337. }
  338. /**
  339. * Call <code>transition()</code> when your component has completed its logic and
  340. * the dialog should transition to the next state, after replies (if any) are sent.
  341. * <p>
  342. * If <code>transition()</code> is not called, the dialog will stay in this state after sending
  343. * the replies (if any), and subsequent user input will come back to this component.
  344. * This allows a component to handle a series of interactions within itself,
  345. * however the component is responsible for keeping track of its own state
  346. * in such situations.
  347. * </p>
  348. * <code>transition()</code> will cause the dialog to transition to the next state.
  349. * transition(outcome) will set te outcome of the component that would be used to
  350. * determine the next state to transition to.
  351. * @param {string} [t] - outcome of component
  352. */
  353. transition(t) {
  354. this.response().transition = true;
  355. if (typeof t !== 'undefined') {
  356. this.response().action = t;
  357. }
  358. return this;
  359. }
  360. /**
  361. * Sets the error flag on the response.
  362. * @param {boolean} e - sets error if true
  363. */
  364. error(e) {
  365. this.response().error = !!e;
  366. return this;
  367. }
  368. /**
  369. * Adds a reply to be sent back to the user. May be called multiple times to send multiple replies in a given response.
  370. * Automatically sets the <code>keepTurn</code> as false.
  371. * </p>
  372. * @param {object} payload - can take a string message, a message created by the MessageFactory, or a message created by
  373. * the deprecated MessageModel.
  374. * @param {object} [channelConversation] - to override the default channelConversation from request
  375. * @returns the message payload in JSON format
  376. */
  377. reply(payload, channelConversation) {
  378. var response = {
  379. tenantId: this.request().message.tenantId,
  380. channelConversation: channelConversation || Object.assign({}, this.request().message.channelConversation)
  381. };
  382. var messagePayload = super.constructMessagePayload(payload);
  383. if (messagePayload) {
  384. response.messagePayload = messagePayload;
  385. } else {
  386. // is invalid raw message payload, keep for backwards compatibility
  387. var rawMessagePayload = MessageModel.rawConversationMessage(payload);
  388. var messageModel = new MessageModel(rawMessagePayload);
  389. response.payload = messageModel.rawPayload();
  390. }
  391. this.response().messages = this.response().messages || [];
  392. this.response().messages.push(response);
  393. // "keepTurn" false which signals to the engine to send replies and wait for user input
  394. this.keepTurn(false);
  395. return this;
  396. }
  397. // The HTTP response body
  398. response() {
  399. return super.getResponse();
  400. }
  401. // BUGBUG: workaround for https://jira.oraclecorp.com/jira/browse/MIECS-2748
  402. resolveVariable(variable) {
  403. return variable.startsWith('${') ? null : variable;
  404. }
  405. /**
  406. * When expecting an out of band conversation continuation, such as a
  407. * user following the OAuth flow, completing a form and hitting submit, or
  408. * a human support agent or other third party sending a message, issue a
  409. * limited use token to allow calling back into Bots via the generic callback
  410. * endpoint.
  411. * The provided token should be a UUID or other unique and random number. By setting it
  412. * here in the response the Bot will await a reply with that token and use it to
  413. * thread the message back into the current conversation with that user.
  414. * @param {string} callbackToken - token generated by you to allow reauthentication back
  415. * into this conversation. Should be unique, like userId + random. It is ok to reissue
  416. * the same token for the same conversation.
  417. * @private
  418. */
  419. setCallbackToken(callbackToken) {
  420. this.response().callbackToken = (typeof callbackToken === "undefined" ? null : callbackToken);
  421. return this;
  422. }
  423. }
  424. module.exports = {
  425. CustomComponentContext,
  426. }