Skip to content

Commit

Permalink
feat!: allow sending custom profiles (#1)
Browse files Browse the repository at this point in the history
Signed-off-by: Ariel Gentile <gentilester@gmail.com>
  • Loading branch information
genaris committed Sep 14, 2023
1 parent 7befa54 commit 9847c7d
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 36 deletions.
44 changes: 33 additions & 11 deletions src/UserProfileApi.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import {
injectable,
MessageSender,
AgentContext,
ConnectionRecord,
OutboundMessageContext,
} from '@aries-framework/core'
import { injectable, MessageSender, AgentContext, OutboundMessageContext, ConnectionsApi } from '@aries-framework/core'
import { ProfileHandler, RequestProfileHandler } from './handlers'
import { UserProfileData } from './repository'
import { UserProfileService } from './services'
import { UserProfileData } from './model'

@injectable()
export class UserProfileApi {
Expand All @@ -26,18 +20,41 @@ export class UserProfileApi {
])
}

public async requestUserProfile(connection: ConnectionRecord) {
/**
* Request the user profile for a given connection. It will store received UserProfileData into ConnectionRecord metadata
* (`UserProfile` key).
*
* @param options
*/
public async requestUserProfile(options: { connectionId: string }) {
const connection = await this.agentContext.dependencyManager.resolve(ConnectionsApi).getById(options.connectionId)

const message = await this.userProfileService.createRequestProfileMessage({})

await this.messageSender.sendMessage(
new OutboundMessageContext(message, { agentContext: this.agentContext, connection })
)
}

public async sendUserProfile(connection: ConnectionRecord, sendBackYours?: boolean) {
/**
* Sends User Profile to a given connection. It will send our own stored profile data if `profileData` is not specified.
*
* Note: to specify a profileData here does not mean that it will persist and be used in further profile data sharing. It
* is meant in case we want to send diferent profiles to each connection or update it according to the context.
*
* @param options
*/
public async sendUserProfile(options: {
connectionId: string
profileData?: Partial<UserProfileData>
sendBackYours?: boolean
}) {
const { connectionId, profileData, sendBackYours } = options
const connection = await this.agentContext.dependencyManager.resolve(ConnectionsApi).getById(connectionId)

const myProfile = await this.userProfileService.getUserProfile(this.agentContext)
const message = await this.userProfileService.createProfileMessage({
profile: {
profile: profileData ?? {
displayName: myProfile.displayName,
displayPicture: myProfile.displayPicture,
description: myProfile.description,
Expand All @@ -62,6 +79,11 @@ export class UserProfileApi {
return await this.getUserProfileData()
}

/**
* Retrieve our User Profile Data from storage.
*
* @returns our own UserProfileData
*/
public async getUserProfileData(): Promise<UserProfileData> {
const userProfile = await this.userProfileService.getUserProfile(this.agentContext)
return {
Expand Down
18 changes: 16 additions & 2 deletions src/messages/ProfileMessage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { AgentMessage, Attachment, IsValidMessageType, parseMessageType } from '@aries-framework/core'
import { Expose } from 'class-transformer'
import { IsBoolean, IsOptional } from 'class-validator'
import { UserProfile } from '../model'
import { UserProfileData } from '../repository'
import { UserProfile, UserProfileData } from '../model'

export interface ProfileMessageOptions {
id?: string
Expand All @@ -23,6 +22,7 @@ export class ProfileMessage extends AgentMessage {
this.profile = {
...options.profile,
displayPicture: options.profile.displayPicture ? '#displayPicture' : undefined,
displayIcon: options.profile.displayIcon ? '#displayIcon' : undefined,
}

if (options.profile.displayPicture) {
Expand All @@ -39,6 +39,20 @@ export class ProfileMessage extends AgentMessage {
)
}

if (options.profile.displayIcon) {
// If there is a display icon, we need to add an attachment including picture data
this.addAppendedAttachment(
new Attachment({
id: 'displayIcon',
mimeType: options.profile.displayIcon.mimeType,
data: {
base64: options.profile.displayIcon.base64,
links: options.profile.displayIcon.links,
},
})
)
}

this.sendBackYours = options.sendBackYours ?? false
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/model/ConnectionMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ConnectionRecord } from '@aries-framework/core'
import { UserProfileData } from '../repository'
import { UserProfileData } from '../model'

export const getConnectionProfile = (record: ConnectionRecord) => record.metadata.get('UserProfile') as UserProfileData
export const getConnectionProfile = (record: ConnectionRecord) => record.metadata.get('UserProfile') as UserProfileData | null

export const setConnectionProfile = (record: ConnectionRecord, metadata: UserProfileData) =>
record.metadata.add('UserProfile', metadata)
13 changes: 12 additions & 1 deletion src/model/UserProfile.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
export interface DisplayPictureData {
export interface PictureData {
mimeType?: string
links?: string[]
base64?: string
}

export interface UserProfileData {
displayName?: string
displayPicture?: PictureData
displayIcon?: PictureData
description?: string
organizationDid?: string
organizationName?: string
registrarDid?: string
registrarName?: string
}

export class UserProfile {
public type?: string
public category?: string
Expand Down
11 changes: 3 additions & 8 deletions src/repository/UserProfileRecord.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { BaseRecord } from '@aries-framework/core'
import { DisplayPictureData } from '../model'
import { UserProfileData, PictureData } from '../model'
import { v4 as uuid } from 'uuid'

export interface UserProfileData {
displayName?: string
displayPicture?: DisplayPictureData
description?: string
}

export interface UserProfileStorageProps extends UserProfileData {
id?: string
createdAt?: Date
}

// TODO: Store more data than display name, display picture and description
export class UserProfileRecord extends BaseRecord implements UserProfileStorageProps {
public displayName?: string
public displayPicture?: DisplayPictureData
public displayPicture?: PictureData
public description?: string

public static readonly type = 'UserProfileRecord'
Expand Down
3 changes: 2 additions & 1 deletion src/services/UserProfileEvents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BaseEvent, ConnectionRecord } from '@aries-framework/core'
import { ConnectionProfileKey } from '../messages'
import { UserProfileData, UserProfileRecord } from '../repository'
import { UserProfileData } from '../model'
import { UserProfileRecord } from '../repository'

export enum ProfileEventTypes {
UserProfileUpdated = 'UserProfileUpdated',
Expand Down
27 changes: 20 additions & 7 deletions src/services/UserProfileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
EventEmitter,
InboundMessageContext,
} from '@aries-framework/core'
import { UserProfileRepository } from '../repository/UserProfileRepository'
import { UserProfileRecord, UserProfileData } from '../repository/UserProfileRecord'
import { UserProfileRepository, UserProfileRecord } from '../repository'
import { UserProfileData } from '../model'
import {
ConnectionProfileUpdatedEvent,
ProfileEventTypes,
Expand Down Expand Up @@ -97,14 +97,27 @@ export class UserProfileService {
? messageContext.message.getAppendedAttachmentById('displayPicture')
: undefined

const displayIconData = receivedProfile.displayIcon
? messageContext.message.getAppendedAttachmentById('displayIcon')
: undefined

// TODO: use composed objects
const newProfile: UserProfileData = {
...receivedProfile,
displayPicture: {
mimeType: displayPictureData?.mimeType,
base64: displayPictureData?.data.base64,
links: displayPictureData?.data.links,
},
displayPicture: displayPictureData
? {
mimeType: displayPictureData?.mimeType,
base64: displayPictureData?.data.base64,
links: displayPictureData?.data.links,
}
: currentProfile?.displayPicture,
displayIcon: displayIconData
? {
mimeType: displayIconData?.mimeType,
base64: displayIconData?.data.base64,
links: displayIconData?.data.links,
}
: currentProfile?.displayIcon,
}
if (currentProfile) {
Object.assign(currentProfile, newProfile)
Expand Down
30 changes: 26 additions & 4 deletions test/profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const logger = new ConsoleLogger(LogLevel.info)

export type SubjectMessage = { message: EncryptedMessage; replySubject?: Subject<SubjectMessage> }

describe('receipts test', () => {
describe('profile test', () => {
let aliceAgent: Agent<{ askar: AskarModule; profile: UserProfileModule }>
let bobAgent: Agent<{ askar: AskarModule; profile: UserProfileModule }>
let aliceWalletId: string
Expand Down Expand Up @@ -115,7 +115,7 @@ describe('receipts test', () => {
}
})

test('Send profile', async () => {
test('Send stored profile', async () => {
const profileReceivedPromise = firstValueFrom(
aliceAgent.events.observable<ConnectionProfileUpdatedEvent>(ProfileEventTypes.ConnectionProfileUpdated).pipe(
filter((event: ConnectionProfileUpdatedEvent) => event.payload.connection.id === aliceConnectionRecord.id),
Expand All @@ -129,7 +129,7 @@ describe('receipts test', () => {
displayName: 'Bob',
displayPicture: { mimeType: 'image/png', links: ['http://download'] },
})
await bobAgent.modules.profile.sendUserProfile(bobConnectionRecord!, false)
await bobAgent.modules.profile.sendUserProfile({ connectionId: bobConnectionRecord!.id, sendBackYours: false })

const profile = await profileReceivedPromise

Expand All @@ -142,6 +142,28 @@ describe('receipts test', () => {
)
})

test('Send custom profile', async () => {
const profileReceivedPromise = firstValueFrom(
aliceAgent.events.observable<ConnectionProfileUpdatedEvent>(ProfileEventTypes.ConnectionProfileUpdated).pipe(
filter((event: ConnectionProfileUpdatedEvent) => event.payload.connection.id === aliceConnectionRecord.id),
map((event: ConnectionProfileUpdatedEvent) => event.payload.profile),
timeout(5000)
)
)

await bobAgent.modules.profile.sendUserProfile({ connectionId: bobConnectionRecord!.id, profileData: {
displayIcon: { base64: 'base64' }, organizationDid: 'orgDid' }, sendBackYours: false })

const profile = await profileReceivedPromise

expect(profile).toEqual(
expect.objectContaining({
organizationDid: 'orgDid',
displayIcon: { base64: 'base64' },
})
)
})

test('Request profile', async () => {
const profileRequestedPromise = firstValueFrom(
aliceAgent.events.observable<UserProfileRequestedEvent>(ProfileEventTypes.UserProfileRequested).pipe(
Expand All @@ -151,7 +173,7 @@ describe('receipts test', () => {
)
)

await bobAgent.modules.profile.requestUserProfile(bobConnectionRecord)
await bobAgent.modules.profile.requestUserProfile({ connectionId: bobConnectionRecord.id })

const profileRequestQuery = await profileRequestedPromise

Expand Down

0 comments on commit 9847c7d

Please sign in to comment.