From 377ecbf36f46d9d6544898476783f24da7e87dea Mon Sep 17 00:00:00 2001 From: Ariel Gentile Date: Wed, 3 Apr 2024 20:15:02 -0300 Subject: [PATCH] feat: connection termination Signed-off-by: Ariel Gentile --- doc/service-agent-api.md | 14 ++++++++++++- src/controllers/message/MessageController.ts | 20 +++++++++++++++--- src/events/ConnectionEvents.ts | 21 +++++++++++++++++-- .../messages/TerminateConnectionMessage.ts | 19 +++++++++++++++++ src/model/messages/index.ts | 1 + 5 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/model/messages/TerminateConnectionMessage.ts diff --git a/doc/service-agent-api.md b/doc/service-agent-api.md index 9858113..cc2120f 100644 --- a/doc/service-agent-api.md +++ b/doc/service-agent-api.md @@ -31,6 +31,7 @@ In addition, it supports a notification mechanism to subscribe to any event the - [Menu Selection](#menu-selection) - [Invitation](#invitation) - [Profile](#profile) + - [Terminate Connection](#terminate-connection) - [Identity Proof Item types](#identity-proof-item-types) - [Verifiable Credential](#verifiable-credential) - [Request value](#request-value) @@ -99,10 +100,10 @@ Currently, the following messages can be submitted and received: - Receipts (`receipts`) - Invitation (`invitation`) - Profile (`profile`) +- Terminate Connection (`terminate-connection`) > **TODO**: Messages for: > -> - Open browsing session > - System messages in topics > - Message signaling (typing) @@ -405,6 +406,17 @@ Sends User Profile to a particular connection. An Agent may have its default pro } ``` +#### Terminate Connection + +Terminates a particular connection, notifying the other party through a 'Hangup' message. No further messages will be allowed after this action. + +```json +{ + ... + "type": "terminate-connection", +} +``` + ### Identity Proof Item types When a Credential Issuance is requested, the issuer might require the recipient to present certain identity proofing elements. diff --git a/src/controllers/message/MessageController.ts b/src/controllers/message/MessageController.ts index bbb437d..dfec88a 100644 --- a/src/controllers/message/MessageController.ts +++ b/src/controllers/message/MessageController.ts @@ -9,6 +9,7 @@ import { OutboundMessageContext, OutOfBandRepository, OutOfBandInvitation, + DidExchangeState, } from '@credo-ts/core' import { QuestionAnswerRepository, ValidResponse } from '@credo-ts/question-answer' import { Body, Controller, HttpException, HttpStatus, Logger, Post } from '@nestjs/common' @@ -27,6 +28,7 @@ import { IBaseMessage, didcommReceiptFromServiceAgentReceipt, IdentityProofResultMessage, + TerminateConnectionMessage, } from '../../model' import { VerifiableCredentialRequestedProofItem } from '../../model/messages/proofs/vc/VerifiableCredentialRequestedProofItem' import { AgentService } from '../../services/AgentService' @@ -78,6 +80,14 @@ export class MessageController { const messageType = message.type this.logger.debug!(`Message submitted. ${JSON.stringify(message)}`) + const connection = await agent.connections.findById(message.connectionId) + + if (!connection) throw new Error(`Connection with id ${message.connectionId} not found`) + + if (connection.state === DidExchangeState.Completed && (!connection.did || !connection.theirDid)) { + throw new Error(`This connection has been terminated. No further messages are possible`) + } + if (messageType === TextMessage.type) { const textMsg = JsonTransformer.fromJSON(message, TextMessage) const record = await agent.basicMessages.sendMessage(textMsg.connectionId, textMsg.content) @@ -258,7 +268,6 @@ export class MessageController { const msg = JsonTransformer.fromJSON(message, InvitationMessage) const { label, imageUrl, did } = msg - const connection = await agent.connections.getById(msg.connectionId) const messageSender = agent.context.dependencyManager.resolve(MessageSender) if (did) { @@ -310,8 +319,6 @@ export class MessageController { const msg = JsonTransformer.fromJSON(message, ProfileMessage) const { displayImageUrl, displayName, displayIconUrl } = msg - const connection = await agent.connections.getById(msg.connectionId) - await agent.modules.userProfile.sendUserProfile({ connectionId: connection.id, profileData: { @@ -321,8 +328,15 @@ export class MessageController { }, }) + // FIXME: No message id is returned here + } else if (messageType === TerminateConnectionMessage.type) { + JsonTransformer.fromJSON(message, TerminateConnectionMessage) + + await agent.connections.hangup({ connectionId: connection.id }) + // FIXME: No message id is returned here } + this.logger.debug!(`messageId: ${messageId}`) return { id: messageId ?? utils.uuid() } // TODO: persistant mapping between AFJ records and Service Agent flows. Support external message id setting } catch (error) { diff --git a/src/events/ConnectionEvents.ts b/src/events/ConnectionEvents.ts index 7cd4031..ffe93ef 100644 --- a/src/events/ConnectionEvents.ts +++ b/src/events/ConnectionEvents.ts @@ -1,7 +1,7 @@ import type { ServerConfig } from '../utils/ServerConfig' -import type { ConnectionStateChangedEvent } from '@credo-ts/core' +import type { AgentMessageProcessedEvent, ConnectionStateChangedEvent } from '@credo-ts/core' -import { ConnectionEventTypes, ConnectionRepository } from '@credo-ts/core' +import { AgentEventTypes, ConnectionEventTypes, ConnectionRepository, HangupMessage } from '@credo-ts/core' import { ServiceAgent } from '../utils/ServiceAgent' @@ -34,4 +34,21 @@ export const connectionEvents = async (agent: ServiceAgent, config: ServerConfig await sendWebhookEvent(config.webhookUrl + '/connection-state-updated', body, config.logger) }, ) + + // When a hangup message is received for a given connection, it will be effectively terminated. Service Agent controller + // will be notified about this 'termination' status + agent.events.on(AgentEventTypes.AgentMessageProcessed, async ({ payload }: AgentMessageProcessedEvent) => { + const { message, connection } = payload + + if (message.type === HangupMessage.type.messageTypeUri && connection) { + const body = { + type: 'connection-state-updated', + connectionId: connection.id, + invitationId: connection.outOfBandId, + state: 'terminated', + } + + await sendWebhookEvent(config.webhookUrl + '/connection-state-updated', body, config.logger) + } + }) } diff --git a/src/model/messages/TerminateConnectionMessage.ts b/src/model/messages/TerminateConnectionMessage.ts new file mode 100644 index 0000000..17de279 --- /dev/null +++ b/src/model/messages/TerminateConnectionMessage.ts @@ -0,0 +1,19 @@ +import { BaseMessage, BaseMessageOptions } from './BaseMessage' + +export interface TerminateConnectionMessageOptions extends BaseMessageOptions {} + +export class TerminateConnectionMessage extends BaseMessage { + public constructor(options: TerminateConnectionMessageOptions) { + super() + + if (options) { + this.id = options.id ?? this.generateId() + this.threadId = options.threadId + this.timestamp = options.timestamp ?? new Date() + this.connectionId = options.connectionId + } + } + + public readonly type = TerminateConnectionMessage.type + public static readonly type = 'terminate-connection' +} diff --git a/src/model/messages/index.ts b/src/model/messages/index.ts index b13a1ef..5ad6e56 100644 --- a/src/model/messages/index.ts +++ b/src/model/messages/index.ts @@ -14,4 +14,5 @@ export * from './MenuDisplayMessage' export * from './MenuSelectMessage' export * from './ProfileMessage' export * from './ReceiptsMessage' +export * from './TerminateConnectionMessage' export * from './TextMessage'