Skip to content

Commit

Permalink
feat: Action Menu protocol (Aries RFC 0509) implementation (#974)
Browse files Browse the repository at this point in the history
Signed-off-by: Ariel Gentile <[email protected]>
  • Loading branch information
genaris authored Sep 1, 2022
1 parent 4b90e87 commit 60a8091
Show file tree
Hide file tree
Showing 34 changed files with 2,376 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/agent/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CacheRepository } from '../cache'
import { InjectionSymbols } from '../constants'
import { JwsService } from '../crypto/JwsService'
import { AriesFrameworkError } from '../error'
import { ActionMenuModule } from '../modules/action-menu'
import { BasicMessagesModule } from '../modules/basic-messages/BasicMessagesModule'
import { ConnectionsModule } from '../modules/connections/ConnectionsModule'
import { CredentialsModule } from '../modules/credentials/CredentialsModule'
Expand Down Expand Up @@ -68,6 +69,7 @@ export class Agent {
public readonly genericRecords: GenericRecordsModule
public readonly ledger: LedgerModule
public readonly questionAnswer!: QuestionAnswerModule
public readonly actionMenu!: ActionMenuModule
public readonly credentials: CredentialsModule
public readonly mediationRecipient: RecipientModule
public readonly mediator: MediatorModule
Expand Down Expand Up @@ -122,6 +124,7 @@ export class Agent {
this.mediationRecipient = this.dependencyManager.resolve(RecipientModule)
this.basicMessages = this.dependencyManager.resolve(BasicMessagesModule)
this.questionAnswer = this.dependencyManager.resolve(QuestionAnswerModule)
this.actionMenu = this.dependencyManager.resolve(ActionMenuModule)
this.genericRecords = this.dependencyManager.resolve(GenericRecordsModule)
this.ledger = this.dependencyManager.resolve(LedgerModule)
this.discovery = this.dependencyManager.resolve(DiscoverFeaturesModule)
Expand Down Expand Up @@ -342,6 +345,7 @@ export class Agent {
RecipientModule,
BasicMessagesModule,
QuestionAnswerModule,
ActionMenuModule,
GenericRecordsModule,
LedgerModule,
DiscoverFeaturesModule,
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/modules/action-menu/ActionMenuEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { BaseEvent } from '../../agent/Events'
import type { ActionMenuState } from './ActionMenuState'
import type { ActionMenuRecord } from './repository'

export enum ActionMenuEventTypes {
ActionMenuStateChanged = 'ActionMenuStateChanged',
}
export interface ActionMenuStateChangedEvent extends BaseEvent {
type: typeof ActionMenuEventTypes.ActionMenuStateChanged
payload: {
actionMenuRecord: ActionMenuRecord
previousState: ActionMenuState | null
}
}
159 changes: 159 additions & 0 deletions packages/core/src/modules/action-menu/ActionMenuModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import type { DependencyManager } from '../../plugins'
import type {
ClearActiveMenuOptions,
FindActiveMenuOptions,
PerformActionOptions,
RequestMenuOptions,
SendMenuOptions,
} from './ActionMenuModuleOptions'

import { Dispatcher } from '../../agent/Dispatcher'
import { MessageSender } from '../../agent/MessageSender'
import { createOutboundMessage } from '../../agent/helpers'
import { AriesFrameworkError } from '../../error'
import { injectable, module } from '../../plugins'
import { ConnectionService } from '../connections/services'

import { ActionMenuRole } from './ActionMenuRole'
import {
ActionMenuProblemReportHandler,
MenuMessageHandler,
MenuRequestMessageHandler,
PerformMessageHandler,
} from './handlers'
import { ActionMenuService } from './services'

@module()
@injectable()
export class ActionMenuModule {
private connectionService: ConnectionService
private messageSender: MessageSender
private actionMenuService: ActionMenuService

public constructor(
dispatcher: Dispatcher,
connectionService: ConnectionService,
messageSender: MessageSender,
actionMenuService: ActionMenuService
) {
this.connectionService = connectionService
this.messageSender = messageSender
this.actionMenuService = actionMenuService
this.registerHandlers(dispatcher)
}

/**
* Start Action Menu protocol as requester, asking for root menu. Any active menu will be cleared.
*
* @param options options for requesting menu
* @returns Action Menu record associated to this new request
*/
public async requestMenu(options: RequestMenuOptions) {
const connection = await this.connectionService.getById(options.connectionId)

const { message, record } = await this.actionMenuService.createRequest({
connection,
})

const outboundMessage = createOutboundMessage(connection, message)
await this.messageSender.sendMessage(outboundMessage)

return record
}

/**
* Send a new Action Menu as responder. This menu will be sent as response if there is an
* existing menu thread.
*
* @param options options for sending menu
* @returns Action Menu record associated to this action
*/
public async sendMenu(options: SendMenuOptions) {
const connection = await this.connectionService.getById(options.connectionId)

const { message, record } = await this.actionMenuService.createMenu({
connection,
menu: options.menu,
})

const outboundMessage = createOutboundMessage(connection, message)
await this.messageSender.sendMessage(outboundMessage)

return record
}

/**
* Perform action in active Action Menu, as a requester. The related
* menu will be closed.
*
* @param options options for requesting menu
* @returns Action Menu record associated to this selection
*/
public async performAction(options: PerformActionOptions) {
const connection = await this.connectionService.getById(options.connectionId)

const actionMenuRecord = await this.actionMenuService.find({
connectionId: connection.id,
role: ActionMenuRole.Requester,
})
if (!actionMenuRecord) {
throw new AriesFrameworkError(`No active menu found for connection id ${options.connectionId}`)
}

const { message, record } = await this.actionMenuService.createPerform({
actionMenuRecord,
performedAction: options.performedAction,
})

const outboundMessage = createOutboundMessage(connection, message)
await this.messageSender.sendMessage(outboundMessage)

return record
}

/**
* Find the current active menu for a given connection and the specified role.
*
* @param options options for requesting active menu
* @returns Active Action Menu record, or null if no active menu found
*/
public async findActiveMenu(options: FindActiveMenuOptions) {
return this.actionMenuService.find({
connectionId: options.connectionId,
role: options.role,
})
}

/**
* Clears the current active menu for a given connection and the specified role.
*
* @param options options for clearing active menu
* @returns Active Action Menu record, or null if no active menu record found
*/
public async clearActiveMenu(options: ClearActiveMenuOptions) {
const actionMenuRecord = await this.actionMenuService.find({
connectionId: options.connectionId,
role: options.role,
})

return actionMenuRecord ? await this.actionMenuService.clearMenu({ actionMenuRecord }) : null
}

private registerHandlers(dispatcher: Dispatcher) {
dispatcher.registerHandler(new ActionMenuProblemReportHandler(this.actionMenuService))
dispatcher.registerHandler(new MenuMessageHandler(this.actionMenuService))
dispatcher.registerHandler(new MenuRequestMessageHandler(this.actionMenuService))
dispatcher.registerHandler(new PerformMessageHandler(this.actionMenuService))
}

/**
* Registers the dependencies of the discover features module on the dependency manager.
*/
public static register(dependencyManager: DependencyManager) {
// Api
dependencyManager.registerContextScoped(ActionMenuModule)

// Services
dependencyManager.registerSingleton(ActionMenuService)
}
}
27 changes: 27 additions & 0 deletions packages/core/src/modules/action-menu/ActionMenuModuleOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ActionMenuRole } from './ActionMenuRole'
import type { ActionMenu } from './models/ActionMenu'
import type { ActionMenuSelection } from './models/ActionMenuSelection'

export interface FindActiveMenuOptions {
connectionId: string
role: ActionMenuRole
}

export interface ClearActiveMenuOptions {
connectionId: string
role: ActionMenuRole
}

export interface RequestMenuOptions {
connectionId: string
}

export interface SendMenuOptions {
connectionId: string
menu: ActionMenu
}

export interface PerformActionOptions {
connectionId: string
performedAction: ActionMenuSelection
}
9 changes: 9 additions & 0 deletions packages/core/src/modules/action-menu/ActionMenuRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Action Menu roles based on the flow defined in RFC 0509.
*
* @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0509-action-menu#roles
*/
export enum ActionMenuRole {
Requester = 'requester',
Responder = 'responder',
}
13 changes: 13 additions & 0 deletions packages/core/src/modules/action-menu/ActionMenuState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Action Menu states based on the flow defined in RFC 0509.
*
* @see https://github.com/hyperledger/aries-rfcs/tree/main/features/0509-action-menu#states
*/
export enum ActionMenuState {
Null = 'null',
AwaitingRootMenu = 'awaiting-root-menu',
PreparingRootMenu = 'preparing-root-menu',
PreparingSelection = 'preparing-selection',
AwaitingSelection = 'awaiting-selection',
Done = 'done',
}
Loading

0 comments on commit 60a8091

Please sign in to comment.