-
Notifications
You must be signed in to change notification settings - Fork 11k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Single Contact Identification (#32727)
Co-authored-by: Gustavo Reis Bauer <gustavoreisbauer@gmail.com> Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Co-authored-by: Marcos Spessatto Defendi <marcos.defendi@rocket.chat> Co-authored-by: Pierre <pierre.lehnen@rocket.chat> Co-authored-by: Rafael Tapia <rafael.tapia@rocket.chat> Co-authored-by: matheusbsilva137 <matheus_barbosa137@hotmail.com> Co-authored-by: Kevin Aleman <kaleman960@gmail.com> Co-authored-by: Tasso <tasso.evangelista@rocket.chat>
- Loading branch information
1 parent
6ba7372
commit 32d93a0
Showing
333 changed files
with
8,526 additions
and
2,545 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
--- | ||
'@rocket.chat/model-typings': minor | ||
'@rocket.chat/core-typings': minor | ||
'@rocket.chat/rest-typings': minor | ||
'@rocket.chat/apps-engine': minor | ||
'@rocket.chat/i18n': minor | ||
'@rocket.chat/meteor': minor | ||
--- | ||
|
||
These changes aims to add: | ||
- A brand-new omnichannel contact profile | ||
- The ability to communicate with known contacts only | ||
- Communicate with verified contacts only | ||
- Merge verified contacts across different channels | ||
- Block contact channels | ||
- Resolve conflicting contact information when registered via different channels | ||
- An advanced contact center filters |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
apps/meteor/app/api/server/lib/maybeMigrateLivechatRoom.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { isOmnichannelRoom, type IRoom } from '@rocket.chat/core-typings'; | ||
import { Rooms } from '@rocket.chat/models'; | ||
import type { FindOptions } from 'mongodb'; | ||
|
||
import { projectionAllowsAttribute } from './projectionAllowsAttribute'; | ||
import { migrateVisitorIfMissingContact } from '../../../livechat/server/lib/contacts/migrateVisitorIfMissingContact'; | ||
|
||
/** | ||
* If the room is a livechat room and it doesn't yet have a contact, trigger the migration for its visitor and source | ||
* The migration will create/use a contact and assign it to every room that matches this visitorId and source. | ||
**/ | ||
export async function maybeMigrateLivechatRoom(room: IRoom | null, options: FindOptions<IRoom> = {}): Promise<IRoom | null> { | ||
if (!room || !isOmnichannelRoom(room)) { | ||
return room; | ||
} | ||
|
||
// Already migrated | ||
if (room.contactId) { | ||
return room; | ||
} | ||
|
||
// If the query options specify that contactId is not needed, then do not trigger the migration | ||
if (!projectionAllowsAttribute('contactId', options)) { | ||
return room; | ||
} | ||
|
||
const contactId = await migrateVisitorIfMissingContact(room.v._id, room.source); | ||
|
||
// Did not migrate | ||
if (!contactId) { | ||
return room; | ||
} | ||
|
||
// Load the room again with the same options so it can be reloaded with the contactId in place | ||
return Rooms.findOneById(room._id, options); | ||
} |
29 changes: 29 additions & 0 deletions
29
apps/meteor/app/api/server/lib/projectionAllowsAttribute.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { expect } from 'chai'; | ||
|
||
import { projectionAllowsAttribute } from './projectionAllowsAttribute'; | ||
|
||
describe('projectionAllowsAttribute', () => { | ||
it('should return true if there are no options', () => { | ||
expect(projectionAllowsAttribute('attributeName')).to.be.equal(true); | ||
}); | ||
|
||
it('should return true if there is no projection', () => { | ||
expect(projectionAllowsAttribute('attributeName', {})).to.be.equal(true); | ||
}); | ||
|
||
it('should return true if the field is projected', () => { | ||
expect(projectionAllowsAttribute('attributeName', { projection: { attributeName: 1 } })).to.be.equal(true); | ||
}); | ||
|
||
it('should return false if the field is disallowed by projection', () => { | ||
expect(projectionAllowsAttribute('attributeName', { projection: { attributeName: 0 } })).to.be.equal(false); | ||
}); | ||
|
||
it('should return false if the field is not projected and others are', () => { | ||
expect(projectionAllowsAttribute('attributeName', { projection: { anotherAttribute: 1 } })).to.be.equal(false); | ||
}); | ||
|
||
it('should return true if the field is not projected and others are disallowed', () => { | ||
expect(projectionAllowsAttribute('attributeName', { projection: { anotherAttribute: 0 } })).to.be.equal(true); | ||
}); | ||
}); |
19 changes: 19 additions & 0 deletions
19
apps/meteor/app/api/server/lib/projectionAllowsAttribute.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { IRocketChatRecord } from '@rocket.chat/core-typings'; | ||
import type { FindOptions } from 'mongodb'; | ||
|
||
export function projectionAllowsAttribute(attributeName: string, options?: FindOptions<IRocketChatRecord>): boolean { | ||
if (!options?.projection) { | ||
return true; | ||
} | ||
|
||
if (attributeName in options.projection) { | ||
return Boolean(options.projection[attributeName]); | ||
} | ||
|
||
const projectingAllowedFields = Object.values(options.projection).some((value) => Boolean(value)); | ||
|
||
// If the attribute is not on the projection list, return the opposite of the values in the projection. aka: | ||
// if the projection is specifying blocked fields, then this field is allowed; | ||
// if the projection is specifying allowed fields, then this field is blocked; | ||
return !projectingAllowedFields; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import type { IAppServerOrchestrator } from '@rocket.chat/apps'; | ||
import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; | ||
import { ContactBridge } from '@rocket.chat/apps-engine/server/bridges'; | ||
|
||
import { addContactEmail } from '../../../livechat/server/lib/contacts/addContactEmail'; | ||
import { verifyContactChannel } from '../../../livechat/server/lib/contacts/verifyContactChannel'; | ||
|
||
export class AppContactBridge extends ContactBridge { | ||
constructor(private readonly orch: IAppServerOrchestrator) { | ||
super(); | ||
} | ||
|
||
async getById(contactId: ILivechatContact['_id'], appId: string): Promise<ILivechatContact | undefined> { | ||
this.orch.debugLog(`The app ${appId} is fetching a contact`); | ||
return this.orch.getConverters().get('contacts').convertById(contactId); | ||
} | ||
|
||
async verifyContact( | ||
verifyContactChannelParams: { | ||
contactId: string; | ||
field: string; | ||
value: string; | ||
visitorId: string; | ||
roomId: string; | ||
}, | ||
appId: string, | ||
): Promise<void> { | ||
this.orch.debugLog(`The app ${appId} is verifing a contact`); | ||
// Note: If there is more than one app installed, whe should validate the app that called this method to be same one | ||
// selected in the setting. | ||
await verifyContactChannel(verifyContactChannelParams); | ||
} | ||
|
||
protected async addContactEmail(contactId: ILivechatContact['_id'], email: string, appId: string): Promise<ILivechatContact> { | ||
this.orch.debugLog(`The app ${appId} is adding a new email to the contact`); | ||
const contact = await addContactEmail(contactId, email); | ||
return this.orch.getConverters().get('contacts').convertContact(contact); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import type { IAppContactsConverter, IAppsLivechatContact } from '@rocket.chat/apps'; | ||
import type { ILivechatContact } from '@rocket.chat/core-typings'; | ||
import { LivechatContacts } from '@rocket.chat/models'; | ||
|
||
import { transformMappedData } from './transformMappedData'; | ||
|
||
export class AppContactsConverter implements IAppContactsConverter { | ||
async convertById(contactId: ILivechatContact['_id']): Promise<IAppsLivechatContact | undefined> { | ||
const contact = await LivechatContacts.findOneById(contactId); | ||
if (!contact) { | ||
return; | ||
} | ||
|
||
return this.convertContact(contact); | ||
} | ||
|
||
async convertContact(contact: undefined | null): Promise<undefined>; | ||
|
||
async convertContact(contact: ILivechatContact): Promise<IAppsLivechatContact>; | ||
|
||
async convertContact(contact: ILivechatContact | undefined | null): Promise<IAppsLivechatContact | undefined> { | ||
if (!contact) { | ||
return; | ||
} | ||
|
||
return structuredClone(contact); | ||
} | ||
|
||
convertAppContact(contact: undefined | null): Promise<undefined>; | ||
|
||
convertAppContact(contact: IAppsLivechatContact): Promise<ILivechatContact>; | ||
|
||
async convertAppContact(contact: IAppsLivechatContact | undefined | null): Promise<ILivechatContact | undefined> { | ||
if (!contact) { | ||
return; | ||
} | ||
|
||
// Map every attribute individually to ensure there are no extra data coming from the app and leaking into anything else. | ||
const map = { | ||
_id: '_id', | ||
_updatedAt: '_updatedAt', | ||
name: 'name', | ||
phones: { | ||
from: 'phones', | ||
list: true, | ||
map: { | ||
phoneNumber: 'phoneNumber', | ||
}, | ||
}, | ||
emails: { | ||
from: 'emails', | ||
list: true, | ||
map: { | ||
address: 'address', | ||
}, | ||
}, | ||
contactManager: 'contactManager', | ||
unknown: 'unknown', | ||
conflictingFields: { | ||
from: 'conflictingFields', | ||
list: true, | ||
map: { | ||
field: 'field', | ||
value: 'value', | ||
}, | ||
}, | ||
customFields: 'customFields', | ||
channels: { | ||
from: 'channels', | ||
list: true, | ||
map: { | ||
name: 'name', | ||
verified: 'verified', | ||
visitor: { | ||
from: 'visitor', | ||
map: { | ||
visitorId: 'visitorId', | ||
source: { | ||
from: 'source', | ||
map: { | ||
type: 'type', | ||
id: 'id', | ||
}, | ||
}, | ||
}, | ||
}, | ||
blocked: 'blocked', | ||
field: 'field', | ||
value: 'value', | ||
verifiedAt: 'verifiedAt', | ||
details: { | ||
from: 'details', | ||
map: { | ||
type: 'type', | ||
id: 'id', | ||
alias: 'alias', | ||
label: 'label', | ||
sidebarIcon: 'sidebarIcon', | ||
defaultIcon: 'defaultIcon', | ||
destination: 'destination', | ||
}, | ||
}, | ||
lastChat: { | ||
from: 'lastChat', | ||
map: { | ||
_id: '_id', | ||
ts: 'ts', | ||
}, | ||
}, | ||
}, | ||
}, | ||
createdAt: 'createdAt', | ||
lastChat: { | ||
from: 'lastChat', | ||
map: { | ||
_id: '_id', | ||
ts: 'ts', | ||
}, | ||
}, | ||
importIds: 'importIds', | ||
}; | ||
|
||
return transformMappedData(contact, map); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.