UNPKG

iopa-bot

Version:

API-First Bot Framework for Internet of Things (IoT), based on Internet of Protocols Alliance (IOPA) specification

228 lines (192 loc) 6.54 kB
/* * Iopa Bot Framework * Copyright (c) 2016-2019 Internet of Protocols Alliance * * 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 * as Iopa from 'iopa' const { IOPA, SERVER } = Iopa.constants import { BOT } from '../constants' import type { ActionElement } from 'reactive-dialogs' interface Db { get<T>(path: string): Promise<T | null> put<T>(path: string, item: T): Promise<void> delete(path: string): Promise<void> } export interface Session { id: string updated: number [key: string]: any } export interface SessionDbCapability { /** return item from session storage */ get(id: string, timeout: number): Promise<Session> /** put item into session storage */ put(session: Partial<Session> & { id: string }) /** delete item from session storage */ delete(id: string) /** stop dialog manager and dispose resources */ dispose() } export interface SessionCurrentDialog { /* current step id */ id: string /** version of the IOPA dialogs manager */ iopaBotVersion: '2.0' /** sequence number of the directive last executed in the current dialog step */ lastDirective: number | null /** id of step rendered before this one (for return logic) */ previousId: string /** last set of actions prompted to participant */ lastPromptActions: ActionElement[] | null } /** Dialogs Session passed to every context record */ export interface ReactiveDialogsSession { 'id': string /** id of the dialog step being executed in the current skill */ 'bot:CurrentDialog': SessionCurrentDialog | null /** timestamp that the last dialog step ended */ 'bot:LastDialogEndedDate': number | null /** Flag indicating whether this intent is the first for this session */ 'bot:NewSession': boolean /** id of the current executing bot session */ 'bot:Skill': string /** V2 semversion of the current executing bot session; checked in case flow definition upgraded mid conversation */ 'bot:SkillVersion': string /** Skill data for current request */ 'bot:Slots': string /** property bag of all data collected in current skill session, including silent properties specifed on card actions */ 'bot:Variables': any /** was the last delivered item a multi-choice prompt */ 'isMultiChoicePrompt': boolean [key:string]: any } export const useBotSession = (context: Iopa.Context) => [ context[BOT.Session] as Partial<ReactiveDialogsSession>, (newState: Partial<ReactiveDialogsSession>) => { context[BOT.Session] = newState ? Object.assign(context[BOT.Session], newState) : { id: context[BOT.Session].id } return context[SERVER.Capabilities][BOT.CAPABILITIES.Session] .put(context[BOT.Session]) } ] as [Partial<ReactiveDialogsSession>, (newState: Partial<ReactiveDialogsSession>) => Promise<void>] export default class SessionMiddleware implements Iopa.Component { enabled: boolean app: Iopa.App | null db: Db | null constructor(app: Iopa.App) { if (app.properties[SERVER.Capabilities][BOT.CAPABILITIES.Session]) { // Already registered this.enabled = false return } this.enabled = true if ( !app.properties[SERVER.Capabilities]['urn:io.iopa.database:session'] && !app.properties[SERVER.Capabilities]['urn:io.iopa.database'] ) { throw new Error('Session Middleware requires database middleware') } this.app = app var db = app.properties[SERVER.Capabilities]['urn:io.iopa.database:session'] || app.properties[SERVER.Capabilities]['urn:io.iopa.database'] this.db = db app.properties[SERVER.Capabilities][BOT.CAPABILITIES.Session] = { /** return item from session storage */ get: async (id: string, timeout: number) => { if (!this.app) { return undefined } var dbpath = 'sessions/' + id let session: Session = await db.get(dbpath) if (!session) { session = { id: id, updated: new Date().getTime() } await db.put(dbpath, session) return session } else { session.id = id } if (timeout && timeout > 0) { var updated = new Date(session.updated) var expiration = new Date(new Date().getTime() - timeout) if (updated < expiration) { session = { id: id, updated: updated.getTime() } await db.put(dbpath, session) } } return session }, /** put item into session storage */ put: (session: Session) => { if (!this.app) { return } var dbpath = 'sessions/' + session.id session.updated = new Date().getTime() return db.put(dbpath, session) }, /** delete item from session storage */ delete: (id: string) => { if (!this.app) { return } var dbpath = 'sessions/' + id return db.delete(dbpath) }, dispose: () => { this.app = null this.db = null } } as SessionDbCapability app.properties[SERVER.Capabilities][BOT.CAPABILITIES.Session][ IOPA.Version ] = BOT.VERSION } async invoke(context, next) { if (!this.enabled) return next() if (!this.app) return Promise.resolve() const sessiondb = this.app.properties[SERVER.Capabilities][ BOT.CAPABILITIES.Session ] if ( !context[BOT.Session] && context[BOT.Address] && context[BOT.Address][BOT.User] ) { const session = await sessiondb.get(context[BOT.Address][BOT.User]) context[BOT.Session] = session || {} } else { context[BOT.Session] = {} } await next() if ( context.response[BOT.ShouldEndSession] || Object.keys(context[BOT.Session]).length == 1 ) { await sessiondb['delete'](context[BOT.Session].id) } else { await sessiondb.put(context[BOT.Session]) } } }