diff --git a/packages/restapi/src/lib/payloads/helpers.ts b/packages/restapi/src/lib/payloads/helpers.ts index 6b58987bd..01f0445b4 100644 --- a/packages/restapi/src/lib/payloads/helpers.ts +++ b/packages/restapi/src/lib/payloads/helpers.ts @@ -80,6 +80,65 @@ export function getPayloadForAPIInput( return null; } +/** + * This function will map the Input options passed to the SDK to the "payload" structure + * needed by the API input + * + * We need notificationPayload only for identityType + * - DIRECT_PAYLOAD + * - MINIMAL + */ +export function getPayloadForAPIInputV2( + inputOptions: ISendNotificationInputOptions, + recipients: any +): INotificationPayload | null { + if (inputOptions?.notification && inputOptions?.payload) { + return { + notification: { + title: inputOptions?.notification?.title, + body: inputOptions?.notification?.body, + }, + data: { + acta: inputOptions?.payload?.cta || '', + aimg: inputOptions?.payload?.img || '', + amsg: inputOptions?.payload?.body || '', + asub: inputOptions?.payload?.title || '', + type: inputOptions?.type?.toString() || '', + //deprecated + ...(inputOptions?.expiry && { etime: inputOptions?.expiry }), + ...(inputOptions?.payload?.etime && { + etime: inputOptions?.payload?.etime, + }), + //deprecated + ...(inputOptions?.hidden && { hidden: inputOptions?.hidden }), + ...(inputOptions?.payload?.hidden && { + hidden: inputOptions?.payload?.hidden, + }), + ...(inputOptions?.payload?.silent && { + silent: inputOptions?.payload?.silent, + }), + ...(inputOptions?.payload?.sectype && { + sectype: inputOptions?.payload?.sectype, + }), + //deprecated + ...(inputOptions?.payload?.metadata && { + metadata: inputOptions?.payload?.metadata, + }), + ...(inputOptions?.payload?.additionalMeta && { + additionalMeta: inputOptions?.payload?.additionalMeta, + }), + ...(inputOptions?.payload?.index && { + index: inputOptions?.payload?.index, + }), + timestamp: Math.floor(Date.now() / 1000), + }, + recipients: recipients, + }; + } + + return null; +} + /** * This function returns the recipient format accepted by the API for different notification types */ @@ -285,6 +344,110 @@ export async function getVerificationProof({ return verificationProof; } +export async function getVerificationProofV2({ + senderType, + signer, + chainId, + notificationType, + identityType, + verifyingContract, + payload, + ipfsHash, + graph = {}, + uuid, + chatId, + wallet, + pgpPrivateKey, + env, + rules, +}: { + senderType: 0 | 1; + signer: any; + chainId: number; + notificationType: NOTIFICATION_TYPE; + identityType: IDENTITY_TYPE; + verifyingContract: string; + payload: any; + ipfsHash?: string; + graph?: any; + uuid: string; + // for notifications which have additionalMeta in payload + chatId?: string; + wallet?: walletType; + pgpPrivateKey?: string; + env?: ENV; + rules?: VideoNotificationRules; +}) { + let message = null; + let verificationProof = null; + + switch (identityType) { + case IDENTITY_TYPE.MINIMAL: { + message = { + data: `${identityType}+${notificationType}+${payload.notification.title}+${payload.notification.body}`, + }; + break; + } + case IDENTITY_TYPE.IPFS: { + message = { + data: `1+${ipfsHash}`, + }; + break; + } + case IDENTITY_TYPE.DIRECT_PAYLOAD: { + const payloadJSON = JSON.stringify(payload); + message = { + data: `2+${payloadJSON}`, + }; + break; + } + case IDENTITY_TYPE.SUBGRAPH: { + message = { + data: `3+graph:${graph?.id}+${graph?.counter}`, + }; + break; + } + default: { + throw new Error('Invalid IdentityType'); + } + } + + switch (senderType) { + case 0: { + const type = { + Data: [{ name: 'data', type: 'string' }], + }; + const domain = { + name: 'EPNS COMM V1', + chainId: chainId, + verifyingContract: verifyingContract, + }; + const pushSigner = new Signer(signer); + const signature = await pushSigner.signTypedData( + domain, + type, + message, + 'Data' + ); + verificationProof = `eip712v3:${signature}::uid::${uuid}`; + break; + } + case 1: { + const hash = CryptoJS.SHA256(JSON.stringify(message)).toString(); + const signature = await sign({ + message: hash, + signingKey: pgpPrivateKey!, + }); + verificationProof = `pgpv2:${signature}:meta:${chatId}::uid::${uuid}`; + break; + } + default: { + throw new Error('Invalid SenderType'); + } + } + return verificationProof; +} + export function getPayloadIdentity({ identityType, payload, diff --git a/packages/restapi/src/lib/payloads/index.ts b/packages/restapi/src/lib/payloads/index.ts index 22436ff9a..de06ef72c 100644 --- a/packages/restapi/src/lib/payloads/index.ts +++ b/packages/restapi/src/lib/payloads/index.ts @@ -1,4 +1,5 @@ export * from './sendNotifications'; +export {sendNotificationV2} from './sendNotificationsV2'; export { NOTIFICATION_TYPE, IDENTITY_TYPE, diff --git a/packages/restapi/src/lib/payloads/sendNotificationsV2.ts b/packages/restapi/src/lib/payloads/sendNotificationsV2.ts new file mode 100644 index 000000000..e29946872 --- /dev/null +++ b/packages/restapi/src/lib/payloads/sendNotificationsV2.ts @@ -0,0 +1,257 @@ +import { ISendNotificationInputOptions } from '../types'; +import { + getPayloadIdentity, + getRecipients, + getRecipientFieldForAPIPayload, + getSource, + getUUID, + getPayloadForAPIInputV2, + getVerificationProofV2, +} from './helpers'; +import { + getAPIBaseUrls, + getCAIPAddress, + getCAIPDetails, + getConfig, + isValidNFTCAIP, + isValidPushCAIP, +} from '../helpers'; +import { + IDENTITY_TYPE, + DEFAULT_DOMAIN, + NOTIFICATION_TYPE, + SOURCE_TYPES, + VIDEO_CALL_TYPE, + VIDEO_NOTIFICATION_ACCESS_TYPE, +} from './constants'; +import { ENV } from '../constants'; +import { axiosPost } from '../utils/axiosUtil'; +/** + * Validate options for some scenarios + */ +function validateOptions(options: ISendNotificationInputOptions) { + if (!options?.channel) { + throw '[Push SDK] - Error - sendNotificationV2() - "channel" is mandatory!'; + } + if (!isValidPushCAIP(options.channel)) { + throw '[Push SDK] - Error - sendNotificationV2() - "channel" is invalid!'; + } + if (options.senderType === 0 && options.signer === undefined) { + throw '[Push SDK] - Error - sendNotificationV2() - "signer" is mandatory!'; + } + if (options.senderType === 1 && options.pgpPrivateKey === undefined) { + throw '[Push SDK] - Error - sendNotificationV2() - "pgpPrivateKey" is mandatory!'; + } + + /** + * Apart from IPFS, GRAPH use cases "notification", "payload" is mandatory + */ + if ( + options?.identityType === IDENTITY_TYPE.DIRECT_PAYLOAD || + options?.identityType === IDENTITY_TYPE.MINIMAL + ) { + if (!options.notification) { + throw '[Push SDK] - Error - sendNotificationV2() - "notification" mandatory for Identity Type: Direct Payload, Minimal!'; + } + if (!options.payload) { + throw '[Push SDK] - Error - sendNotificationV2() - "payload" mandatory for Identity Type: Direct Payload, Minimal!'; + } + } + + const isAdditionalMetaPayload = options.payload?.additionalMeta; + + const isVideoOrSpaceType = + typeof options.payload?.additionalMeta === 'object' && + (options.payload.additionalMeta.type === + `${VIDEO_CALL_TYPE.PUSH_VIDEO}+1` || + options.payload.additionalMeta.type === + `${VIDEO_CALL_TYPE.PUSH_SPACE}+1`); + + if ( + isAdditionalMetaPayload && + isVideoOrSpaceType && + !options.chatId && + !options.rules + ) { + throw new Error( + '[Push SDK] - Error - sendNotificationV2() - Either chatId or rules object is required to send a additional meta notification for video or spaces' + ); + } +} + +/** + * + * @param payloadOptions channel, recipient and type tp verify whether it is a simulate type + * @returns boolean + */ +async function checkSimulateNotification(payloadOptions: { + channelFound: boolean; + channelorAlias: string; + recipient: string | string[] | undefined; + type: NOTIFICATION_TYPE; + env: ENV | undefined; + senderType: 0 | 1; +}): Promise { + try { + const { channelFound, channelorAlias, recipient, type, env, senderType } = + payloadOptions || {}; + + // Video call notifications are not simulated + // If channel is found, then it is not a simulate type + if (senderType === 1 || channelFound) return false; + + // if no channel info found, check if channel address = recipient and notification type is targeted + const convertedRecipient = + typeof recipient == 'string' && recipient?.split(':').length == 3 + ? recipient.split(':')[2] + : recipient; + return ( + channelorAlias == convertedRecipient && + type == NOTIFICATION_TYPE.TARGETTED + ); + } catch (e) { + return true; + } +} + +export async function sendNotificationV2(options: ISendNotificationInputOptions) { + try { + const { + /* + senderType = 0 for channel notification (default) + senderType = 1 for chat notification + */ + senderType = 0, + signer, + type, + identityType, + payload, + recipients, + channel, + graph, + ipfsHash, + env = ENV.PROD, + chatId, + rules, + pgpPrivateKey, + channelFound = true, + } = options || {}; + + validateOptions(options); + + if ( + payload && + payload.additionalMeta && + typeof payload.additionalMeta === 'object' && + !payload.additionalMeta.domain + ) { + payload.additionalMeta.domain = DEFAULT_DOMAIN; + } + const _channelAddress = await getCAIPAddress(env, channel, 'Channel'); + const channelCAIPDetails = getCAIPDetails(_channelAddress); + + if (!channelCAIPDetails) throw Error('Invalid Channel CAIP!'); + + const uuid = getUUID(); + const chainId = parseInt(channelCAIPDetails.networkId, 10); + + const API_BASE_URL = getAPIBaseUrls(env); + let COMMUNICATOR_CONTRACT = ''; + if (senderType === 0) { + const { EPNS_COMMUNICATOR_CONTRACT } = getConfig(env, channelCAIPDetails); + COMMUNICATOR_CONTRACT = EPNS_COMMUNICATOR_CONTRACT; + } + + const _recipients = await getRecipients({ + env, + notificationType: type, + channel: _channelAddress, + recipients, + secretType: payload?.sectype, + }); + + const notificationPayload = getPayloadForAPIInputV2(options, _recipients); + + const verificationProof = await getVerificationProofV2({ + senderType, + signer, + chainId, + identityType, + notificationType: type, + verifyingContract: COMMUNICATOR_CONTRACT, + payload: notificationPayload, + graph, + ipfsHash, + uuid, + // for the pgpv2 verfication proof + chatId: + rules?.access.data.chatId ?? // for backwards compatibilty with 'chatId' param + chatId, + pgpPrivateKey, + }); + + const identity = getPayloadIdentity({ + identityType, + payload: notificationPayload, + notificationType: type, + graph, + ipfsHash, + }); + + const source = (await checkSimulateNotification({ + channelFound: channelFound, + channelorAlias: options.channel, + recipient: options.recipients, + type: options.type, + env: options.env, + senderType: options.senderType as 0 | 1, + })) + ? SOURCE_TYPES.SIMULATE + : getSource(chainId, identityType, senderType); + + const apiPayload = { + verificationProof, + identity, + sender: + senderType === 1 && !isValidNFTCAIP(_channelAddress) + ? `${channelCAIPDetails?.blockchain}:${channelCAIPDetails?.address}` + : _channelAddress, + source, + /** note this recipient key has a different expectation from the BE API, see the funciton for more */ + recipient: await getRecipientFieldForAPIPayload({ + env, + notificationType: type, + recipients: recipients || '', + channel: _channelAddress, + }), + /* + - If 'rules' is not provided, check if 'chatId' is available. + - If 'chatId' is available, create a new 'rules' object for backwards compatibility. + - If neither 'rules' nor 'chatId' is available, do not include 'rules' in the payload. + */ + ...(rules || chatId + ? { + rules: rules ?? { + access: { + data: { chatId }, + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + }, + }, + } + : {}), + }; + + const requestURL = `${API_BASE_URL}/v1/payloads/`; + return await axiosPost(requestURL, apiPayload, { + headers: { + 'Content-Type': 'application/json', + }, + }); + } catch (err) { + console.error( + '[Push SDK] - Error - sendNotificationV2() - ', + JSON.stringify(err) + ); + throw err; + } +} diff --git a/packages/restapi/src/lib/pushNotification/channel.ts b/packages/restapi/src/lib/pushNotification/channel.ts index cc2a48f8f..209717d8c 100644 --- a/packages/restapi/src/lib/pushNotification/channel.ts +++ b/packages/restapi/src/lib/pushNotification/channel.ts @@ -143,7 +143,7 @@ export class Channel extends PushNotificationBaseClass { channel: options.channel ?? this.account, channelInfo: channelInfo, }); - return await PUSH_PAYLOAD.sendNotification(lowLevelPayload); + return await PUSH_PAYLOAD.sendNotificationV2(lowLevelPayload); } catch (error) { throw new Error(`Push SDK Error: API : channel::send : ${error}`); }